diff --git a/examples/vector/main.go b/examples/vector/main.go index 836c7b71c..7c0368a65 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -163,6 +163,30 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int) { screen.DrawTriangles(vs, is, emptySubImage, op) } +func drawArc(screen *ebiten.Image, count int) { + var path vector.Path + + path.MoveTo(350, 100) + const cx, cy, r = 450, 100, 70 + theta := math.Pi * float64(count) / 180 + x := cx + r*math.Cos(theta) + y := cy + r*math.Sin(theta) + path.ArcTo(450, 100, float32(x), float32(y), 30) + + op := &ebiten.DrawTrianglesOptions{ + EvenOdd: true, + } + vs, is := path.AppendVerticesAndIndicesForFilling(nil, nil) + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = 0x33 / float32(0xff) + vs[i].ColorG = 0xcc / float32(0xff) + vs[i].ColorB = 0x66 / float32(0xff) + } + screen.DrawTriangles(vs, is, emptySubImage, op) +} + func maxCounter(index int) int { return 128 + (17*index+32)%64 } @@ -219,6 +243,7 @@ func (g *Game) Draw(screen *ebiten.Image) { screen.Fill(color.White) drawEbitenText(screen) drawEbitenLogo(screen, 20, 90) + drawArc(screen, g.counter) drawWave(screen, g.counter) ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f\nFPS: %0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS())) diff --git a/vector/path.go b/vector/path.go index c3686b967..4b4402ca3 100644 --- a/vector/path.go +++ b/vector/path.go @@ -18,6 +18,8 @@ package vector import ( + "math" + "github.com/hajimehoshi/ebiten/v2" ) @@ -45,7 +47,10 @@ func (p *Path) LineTo(x, y float32) { if len(p.segs) == 0 { p.segs = append(p.segs, []point{p.cur}) } - p.segs[len(p.segs)-1] = append(p.segs[len(p.segs)-1], point{x: x, y: y}) + seg := p.segs[len(p.segs)-1] + if seg[len(seg)-1].x != x || seg[len(seg)-1].y != y { + p.segs[len(p.segs)-1] = append(seg, point{x: x, y: y}) + } p.cur = point{x: x, y: y} } @@ -127,6 +132,56 @@ func (p *Path) cubicTo(x1, y1, x2, y2, x3, y3 float32, level int) { p.cubicTo(x123, y123, x23, y23, x3, y3, level+1) } +func normalize(x, y float32) (float32, float32) { + len := float32(math.Hypot(float64(x), float64(y))) + return x / len, y / len +} + +// ArcTo adds an arc curve to the path. (x1, y1) is the control point, and (x2, y2) is the destination. +// +// ArcTo updates the current position to (x2, y2). +func (p *Path) ArcTo(x1, y1, x2, y2, radius float32) { + x0 := p.cur.x + y0 := p.cur.y + dx0 := x0 - x1 + dy0 := y0 - y1 + dx1 := x2 - x1 + dy1 := y2 - y1 + dx0, dy0 = normalize(dx0, dy0) + dx1, dy1 = normalize(dx1, dy1) + + // theta is the angle between two vectors (dx0, dy0) and (dx1, dy1). + theta := math.Acos(float64(dx0*dx1 + dy0*dy1)) + // TODO: When theta is bigger than π/2, the arc should be split into two. + + // dist is the distance between the control point and the arc's begenning and ending points. + dist := radius / float32(math.Tan(theta/2)) + + // TODO: What if dist is too big? + + // (ax0, ay0) is the start of the arc. + ax0 := x1 + dx0*dist + ay0 := y1 + dy0*dist + + // (ax1, ay1) is the end of the arc. + ax1 := x1 + dx1*dist + ay1 := y1 + dy1*dist + + p.LineTo(ax0, ay0) + + // Calculate the control points for an approximated Bézier curve. + // See https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/curves/beziers. + alpha := math.Pi - theta + l := radius * float32(math.Tan(alpha/4)*4/3) + cx0 := ax0 + l*(-dx0) + cy0 := ay0 + l*(-dy0) + cx1 := ax1 + l*(-dx1) + cy1 := ay1 + l*(-dy1) + p.CubicTo(cx0, cy0, cx1, cy1, ax1, ay1) + + p.LineTo(x2, y2) +} + // AppendVerticesAndIndicesForFilling appends vertices and indices to fill this path and returns them. // AppendVerticesAndIndicesForFilling works in a similar way to the built-in append function. // If the arguments are nils, AppendVerticesAndIndices returns new slices.