diff --git a/examples/fontvector/main.go b/examples/fontvector/main.go index ce2da0c3d..6a2c26a8e 100644 --- a/examples/fontvector/main.go +++ b/examples/fontvector/main.go @@ -60,9 +60,13 @@ func (g *Game) Draw(screen *ebiten.Image) { op.Width = 2*(float32(math.Sin(float64(g.tick)*2*math.Pi/180))+1) + 1 op.LineJoin = vector.LineJoinRound op.LineCap = vector.LineCapRound - var geoM ebiten.GeoM - geoM.Translate(50, 0) - vector.StrokePath(screen, g.path.ApplyGeoM(geoM), color.White, true, op) + + op2 := &vector.AddPathOptions{} + op2.GeoM.Translate(50, 0) + var newPath vector.Path + newPath.AddPath(&g.path, op2) + + vector.StrokePath(screen, &newPath, color.White, true, op) } func (*Game) Layout(width, height int) (int, int) { diff --git a/examples/vector/main.go b/examples/vector/main.go index d10759518..e90830f92 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -149,15 +149,17 @@ func (g *Game) drawEbitenLogo(screen *ebiten.Image, x, y int, aa bool, line bool path.LineTo(unit, 4*unit) path.Close() - var geoM ebiten.GeoM - geoM.Translate(float64(x), float64(y)) + var newPath vector.Path + op := &vector.AddPathOptions{} + op.GeoM.Translate(float64(x), float64(y)) + newPath.AddPath(&path, op) if line { op := &vector.StrokeOptions{} op.Width = 5 op.LineJoin = vector.LineJoinRound - vector.StrokePath(screen, path.ApplyGeoM(geoM), color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, op) + vector.StrokePath(screen, &newPath, color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, op) } else { - vector.DrawFilledPath(screen, path.ApplyGeoM(geoM), color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, vector.FillRuleNonZero) + vector.DrawFilledPath(screen, &newPath, color.RGBA{0xdb, 0x56, 0x20, 0xff}, aa, vector.FillRuleNonZero) } } diff --git a/vector/path.go b/vector/path.go index abd2f0b19..b82169ce2 100644 --- a/vector/path.go +++ b/vector/path.go @@ -622,29 +622,64 @@ func appendVerticesAndIndicesForFilling[T ~uint16 | ~uint32](path *Path, vertice } // ApplyGeoM applies the given GeoM to the path and returns a new path. +// +// Deprecated: as of v2.9. Use [Path.AddPath] instead. func (p *Path) ApplyGeoM(geoM ebiten.GeoM) *Path { // Flat paths are not copied. - np := &Path{ - subPaths: make([]subPath, len(p.subPaths)), - } - for i, subPath := range p.subPaths { - sx, sy := geoM.Apply(float64(subPath.start.x), float64(subPath.start.y)) - np.subPaths[i].start = point{x: float32(sx), y: float32(sy)} - np.subPaths[i].closed = subPath.closed - np.subPaths[i].ops = make([]op, len(subPath.ops)) + var np Path + op := &AddPathOptions{} + op.GeoM = geoM + np.AddPath(p, op) + return &np +} - for j, o := range subPath.ops { - x1, y1 := geoM.Apply(float64(o.p1.x), float64(o.p1.y)) - x2, y2 := geoM.Apply(float64(o.p2.x), float64(o.p2.y)) - np.subPaths[i].ops[j] = op{ - typ: o.typ, - p1: point{x: float32(x1), y: float32(y1)}, - p2: point{x: float32(x2), y: float32(y2)}, - } +// AddPathOptions is options for [Path.AddPath]. +type AddPathOptions struct { + // GeoM is a transformation matrix to apply to the path. + // + // The default (zero) value is an identity matrix. + GeoM ebiten.GeoM +} + +// AddPath adds the given path src to this path p as a sub-path. +func (p *Path) AddPath(src *Path, options *AddPathOptions) { + p.resetFlatPaths() + + if options == nil { + options = &AddPathOptions{} + } + + origN := len(src.subPaths) + n := len(p.subPaths) + p.subPaths = append(p.subPaths, make([]subPath, len(src.subPaths))...) + // Note that p and src might be the same. + for i, origSubPath := range src.subPaths[:origN] { + sx, sy := options.GeoM.Apply(float64(origSubPath.start.x), float64(origSubPath.start.y)) + p.subPaths[n+i] = subPath{ + ops: make([]op, len(origSubPath.ops)), + start: point{x: float32(sx), y: float32(sy)}, + closed: origSubPath.closed, } + for j, o := range origSubPath.ops { + switch o.typ { + case opTypeLineTo: + x1, y1 := options.GeoM.Apply(float64(o.p1.x), float64(o.p1.y)) + p.subPaths[n+i].ops[j] = op{ + typ: o.typ, + p1: point{x: float32(x1), y: float32(y1)}, + } + case opTypeQuadTo: + x1, y1 := options.GeoM.Apply(float64(o.p1.x), float64(o.p1.y)) + x2, y2 := options.GeoM.Apply(float64(o.p2.x), float64(o.p2.y)) + p.subPaths[n+i].ops[j] = op{ + typ: o.typ, + p1: point{x: float32(x1), y: float32(y1)}, + p2: point{x: float32(x2), y: float32(y2)}, + } + } + } } - return np } // LineCap represents the way in which how the ends of the stroke are rendered. diff --git a/vector/path_test.go b/vector/path_test.go index 3ee70d7e8..dcd2cd896 100644 --- a/vector/path_test.go +++ b/vector/path_test.go @@ -154,3 +154,46 @@ func TestMoveToAndClose(t *testing.T) { t.Errorf("expected close count to be %d, got %d", want, got) } } + +func TestAddPath(t *testing.T) { + var path vector.Path + path.MoveTo(10, 20) + path.LineTo(30, 40) + path.Close() + + op := &vector.AddPathOptions{} + op.GeoM.Translate(100, 100) + var path2 vector.Path + path2.AddPath(&path, op) + + if p, ok := vector.CurrentPosition(&path); p != (vector.Point{10, 20}) || !ok { + t.Errorf("expected last position to be (10, 20), got %v", p) + } + if got, want := vector.SubPathCount(&path), 1; got != want { + t.Errorf("expected close count to be %d, got %d", want, got) + } + if p, ok := vector.CurrentPosition(&path2); p != (vector.Point{110, 120}) || !ok { + t.Errorf("expected last position to be (110, 120), got %v", p) + } + if got, want := vector.SubPathCount(&path2), 1; got != want { + t.Errorf("expected close count to be %d, got %d", want, got) + } +} + +func TestAddPathSelf(t *testing.T) { + var path vector.Path + path.MoveTo(10, 20) + path.LineTo(30, 40) + path.Close() + + op := &vector.AddPathOptions{} + op.GeoM.Translate(100, 100) + path.AddPath(&path, op) + + if p, ok := vector.CurrentPosition(&path); p != (vector.Point{110, 120}) || !ok { + t.Errorf("expected last position to be (110, 120), got %v", p) + } + if got, want := vector.SubPathCount(&path), 2; got != want { + t.Errorf("expected close count to be %d, got %d", want, got) + } +}