diff --git a/examples/vector/main.go b/examples/vector/main.go index f2de23181..c71f54702 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -63,6 +63,7 @@ func drawEbitenText(screen *ebiten.Image, x, y int, aa bool, line bool) { path.LineTo(30, 30) path.LineTo(70, 30) path.LineTo(70, 20) + path.Close() // B path.MoveTo(80, 20) @@ -70,12 +71,14 @@ func drawEbitenText(screen *ebiten.Image, x, y int, aa bool, line bool) { path.LineTo(100, 70) path.QuadTo(150, 57.5, 100, 45) path.QuadTo(150, 32.5, 100, 20) + path.Close() // I path.MoveTo(140, 20) path.LineTo(140, 70) path.LineTo(150, 70) path.LineTo(150, 20) + path.Close() // T path.MoveTo(160, 20) @@ -86,6 +89,7 @@ func drawEbitenText(screen *ebiten.Image, x, y int, aa bool, line bool) { path.LineTo(190, 30) path.LineTo(210, 30) path.LineTo(210, 20) + path.Close() // E path.MoveTo(220, 20) @@ -100,6 +104,7 @@ func drawEbitenText(screen *ebiten.Image, x, y int, aa bool, line bool) { path.LineTo(230, 30) path.LineTo(270, 30) path.LineTo(270, 20) + path.Close() // N path.MoveTo(280, 20) @@ -112,6 +117,7 @@ func drawEbitenText(screen *ebiten.Image, x, y int, aa bool, line bool) { path.LineTo(320, 20) path.LineTo(320, 55) path.LineTo(290, 20) + path.Close() var vs []ebiten.Vertex var is []uint16 @@ -166,6 +172,7 @@ func drawEbitenLogo(screen *ebiten.Image, x, y int, aa bool, line bool) { path.LineTo(2*unit, 3*unit) path.LineTo(unit, 3*unit) path.LineTo(unit, 4*unit) + path.Close() var vs []ebiten.Vertex var is []uint16 @@ -209,6 +216,7 @@ func drawArc(screen *ebiten.Image, count int, aa bool, line bool) { theta2 := math.Pi * float64(count) / 180 / 3 path.MoveTo(550, 100) path.Arc(550, 100, 50, float32(theta1), float32(theta2), vector.Clockwise) + path.Close() var vs []ebiten.Vertex var is []uint16 diff --git a/vector/path.go b/vector/path.go index b2a186967..dcad3c678 100644 --- a/vector/path.go +++ b/vector/path.go @@ -36,31 +36,67 @@ type point struct { y float32 } -// Path represents a collection of path segments. -type Path struct { - segs [][]point - cur point +type subpath struct { + points []point + closed bool } -// MoveTo skips the current position of the path to the given position (x, y) without adding any strokes. +func (s *subpath) pointCount() int { + return len(s.points) +} + +func (s *subpath) lastPoint() point { + return s.points[len(s.points)-1] +} + +func (s *subpath) appendPoint(pt point) { + if s.closed { + panic("vector: a closed subpathment cannot append a new point") + } + if s.lastPoint() == pt { + return + } + s.points = append(s.points, pt) +} + +func (s *subpath) close() { + if s.closed { + return + } + + s.appendPoint(s.points[0]) + s.closed = true +} + +// Path represents a collection of path subpathments. +type Path struct { + subpaths []*subpath + cur point +} + +// MoveTo starts a new subpath with the given position (x, y) without adding any strokes, +// +// MoveTo updates the current position to (x, y). func (p *Path) MoveTo(x, y float32) { p.cur = point{x: x, y: y} - p.segs = append(p.segs, []point{p.cur}) + p.subpaths = append(p.subpaths, &subpath{ + points: []point{p.cur}, + }) } // LineTo adds a line segument to the path, which starts from the current position and ends to the given position (x, y). // // LineTo updates the current position to (x, y). func (p *Path) LineTo(x, y float32) { - if len(p.segs) == 0 { - p.segs = append(p.segs, []point{{x: x, y: y}}) + if len(p.subpaths) == 0 || p.subpaths[len(p.subpaths)-1].closed { + p.subpaths = append(p.subpaths, &subpath{ + points: []point{{x: x, y: y}}, + }) p.cur = point{x: x, y: y} return } - 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.subpaths[len(p.subpaths)-1].appendPoint(point{x: x, y: y}) p.cur = point{x: x, y: y} } @@ -311,6 +347,19 @@ func (p *Path) Arc(x, y, radius, startAngle, endAngle float32, dir Direction) { p.CubicTo(cx0, cy0, cx1, cy1, x1, y1) } +// Close adds a new line from the current position to the first position of the current subpath, +// and marks the current subpath closed. +// Following operations for this path will start with a new subpath. +// +// Close updates the current position to the first position of the current subpath. +func (p *Path) Close() { + if len(p.subpaths) == 0 { + return + } + subpath := p.subpaths[len(p.subpaths)-1] + subpath.close() +} + // 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, AppendVerticesAndIndicesForFilling returns new slices. @@ -326,11 +375,11 @@ func (p *Path) AppendVerticesAndIndicesForFilling(vertices []ebiten.Vertex, indi // TODO: Add tests. base := uint16(len(vertices)) - for _, seg := range p.segs { - if len(seg) < 3 { + for _, subpath := range p.subpaths { + if subpath.pointCount() < 3 { continue } - for i, pt := range seg { + for i, pt := range subpath.points { vertices = append(vertices, ebiten.Vertex{ DstX: pt.x, DstY: pt.y, @@ -346,7 +395,7 @@ func (p *Path) AppendVerticesAndIndicesForFilling(vertices []ebiten.Vertex, indi } indices = append(indices, base, base+uint16(i-1), base+uint16(i)) } - base += uint16(len(seg)) + base += uint16(subpath.pointCount()) } return vertices, indices } @@ -372,13 +421,18 @@ const ( // StokeOptions is options to render a stroke. type StrokeOptions struct { // Width is the stroke width in pixels. + // + // The default (zero) value is 0. Width float32 // LineCap is the way in which how the ends of the stroke are rendered. + // Line caps are not rendered when the subpath is marked as closed. + // // 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 @@ -401,19 +455,16 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic return vertices, indices } - for _, seg := range p.segs { - if len(seg) < 2 { + for _, subpath := range p.subpaths { + if subpath.pointCount() < 2 { continue } var rects [][4]point - for i := 0; i < len(seg)-1; i++ { - pt := seg[i] - if seg[i+1] == pt { - continue - } + for i := 0; i < subpath.pointCount()-1; i++ { + pt := subpath.points[i] - nextPt := seg[i+1] + nextPt := subpath.points[i+1] dx := nextPt.x - pt.x dy := nextPt.y - pt.y dist := float32(math.Sqrt(float64(dx*dx + dy*dy))) @@ -456,13 +507,16 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic } indices = append(indices, idx, idx+1, idx+2, idx+1, idx+2, idx+3) - if i >= len(rects)-1 { + // Add line joints. + var nextRect [4]point + if i < len(rects)-1 { + nextRect = rects[i+1] + } else if subpath.closed { + nextRect = rects[0] + } else { continue } - // Add line joints. - nextRect := rects[i+1] - // c is the center of the 'end' edge of the current rect (= the second point of the segment). c := point{ x: (rect[1].x + rect[3].x) / 2, @@ -532,6 +586,11 @@ func (p *Path) AppendVerticesAndIndicesForStroke(vertices []ebiten.Vertex, indic continue } + // If the subpath is closed, do not render line caps. + if subpath.closed { + continue + } + switch op.LineCap { case LineCapButt: // Do nothing.