From d2f6d8593b4645979281b4bc9b998a62b9f38b68 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 15 Oct 2022 00:23:48 +0900 Subject: [PATCH] vector: add LineCap Closes #1843 --- examples/lines/main.go | 29 ++++++++++------ vector/path.go | 75 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/examples/lines/main.go b/examples/lines/main.go index 5d2221377..419c28e1a 100644 --- a/examples/lines/main.go +++ b/examples/lines/main.go @@ -94,23 +94,31 @@ func (g *Game) Draw(screen *ebiten.Image) { target = g.offscreen } + joins := []vector.LineJoin{ + vector.LineJoinMiter, + vector.LineJoinMiter, + vector.LineJoinBevel, + vector.LineJoinRound, + } + caps := []vector.LineCap{ + vector.LineCapButt, + vector.LineCapRound, + vector.LineCapSquare, + } + ow, oh := target.Size() - size := min(ow/5, oh/4) - offsetX, offsetY := (ow-size*4)/2, (oh-size*3)/2 + size := min(ow/(len(joins)+1), oh/(len(caps)+1)) + offsetX, offsetY := (ow-size*len(joins))/2, (oh-size*len(caps))/2 // Render the lines on the target. - for j := 0; j < 3; j++ { - for i, join := range []vector.LineJoin{ - vector.LineJoinMiter, - vector.LineJoinMiter, - vector.LineJoinBevel, - vector.LineJoinRound} { + for j, cap := range caps { + for i, join := range joins { r := image.Rect(i*size+offsetX, j*size+offsetY, (i+1)*size+offsetX, (j+1)*size+offsetY) miterLimit := float32(5) if i == 1 { miterLimit = 10 } - g.drawLine(target, r, join, miterLimit) + g.drawLine(target, r, cap, join, miterLimit) } } @@ -127,7 +135,7 @@ Press C to switch to draw the center lines.` ebitenutil.DebugPrint(screen, msg) } -func (g *Game) drawLine(screen *ebiten.Image, region image.Rectangle, join vector.LineJoin, miterLimit float32) { +func (g *Game) drawLine(screen *ebiten.Image, region image.Rectangle, cap vector.LineCap, join vector.LineJoin, miterLimit float32) { c0x := float64(region.Min.X + region.Dx()/4) c0y := float64(region.Min.Y + region.Dy()/4) c1x := float64(region.Max.X - region.Dx()/4) @@ -146,6 +154,7 @@ func (g *Game) drawLine(screen *ebiten.Image, region image.Rectangle, join vecto // Draw the main line in white. op := &vector.StrokeOptions{} + op.LineCap = cap op.LineJoin = join op.MiterLimit = miterLimit op.Width = float32(r / 2) diff --git a/vector/path.go b/vector/path.go index c195335ab..66f67b1db 100644 --- a/vector/path.go +++ b/vector/path.go @@ -348,6 +348,15 @@ func (p *Path) AppendVerticesAndIndicesForFilling(vertices []ebiten.Vertex, indi return vertices, indices } +// LineCap represents the way in which how the ends of the stroke are rendered. +type LineCap int + +const ( + LineCapButt LineCap = iota + LineCapRound + LineCapSquare +) + // LineJoin represents the way in which how two segments are joined. type LineJoin int @@ -362,6 +371,10 @@ type StrokeOptions struct { // Width is the stroke width in pixels. Width float32 + // LineCap is the way in which how the ends of the stroke are rendered. + // The default (zero) value is LineCapButt. + LineCap LineCap + // LineJoin is the way in which how two segments are joined. // The default (zero) value is LineJoiMiter. LineJoin LineJoin @@ -511,6 +524,68 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic vertices, indices = arc.AppendVerticesAndIndicesForFilling(vertices, indices) } } + + if len(rects) == 0 { + continue + } + + switch op.LineCap { + case LineCapButt: + // Do nothing. + + case LineCapRound: + startR, endR := rects[0], rects[len(rects)-1] + { + c := point{ + x: (startR[0].x + startR[2].x) / 2, + y: (startR[0].y + startR[2].y) / 2, + } + a := float32(math.Atan2(float64(startR[0].y-startR[2].y), float64(startR[0].x-startR[2].x))) + var arc Path + arc.MoveTo(startR[0].x, startR[0].y) + arc.Arc(c.x, c.y, op.Width/2, a, a+math.Pi, CounterClockwise) + vertices, indices = arc.AppendVerticesAndIndicesForFilling(vertices, indices) + } + { + c := point{ + x: (endR[1].x + endR[3].x) / 2, + y: (endR[1].y + endR[3].y) / 2, + } + a := float32(math.Atan2(float64(endR[1].y-endR[3].y), float64(endR[1].x-endR[3].x))) + var arc Path + arc.MoveTo(endR[1].x, endR[1].y) + arc.Arc(c.x, c.y, op.Width/2, a, a+math.Pi, Clockwise) + vertices, indices = arc.AppendVerticesAndIndicesForFilling(vertices, indices) + } + + case LineCapSquare: + startR, endR := rects[0], rects[len(rects)-1] + { + a := math.Atan2(float64(startR[0].y-startR[1].y), float64(startR[0].x-startR[1].x)) + s, c := math.Sincos(a) + dx, dy := float32(c)*op.Width/2, float32(s)*op.Width/2 + + var quad Path + quad.MoveTo(startR[0].x, startR[0].y) + quad.LineTo(startR[0].x+dx, startR[0].y+dy) + quad.LineTo(startR[2].x+dx, startR[2].y+dy) + quad.LineTo(startR[2].x, startR[2].y) + vertices, indices = quad.AppendVerticesAndIndicesForFilling(vertices, indices) + } + { + a := math.Atan2(float64(endR[1].y-endR[0].y), float64(endR[1].x-endR[0].x)) + s, c := math.Sincos(a) + dx, dy := float32(c)*op.Width/2, float32(s)*op.Width/2 + + var quad Path + quad.MoveTo(endR[1].x, endR[1].y) + quad.LineTo(endR[1].x+dx, endR[1].y+dy) + quad.LineTo(endR[3].x+dx, endR[3].y+dy) + quad.LineTo(endR[3].x, endR[3].y) + vertices, indices = quad.AppendVerticesAndIndicesForFilling(vertices, indices) + } + } } + return vertices, indices }