diff --git a/examples/vector/main.go b/examples/vector/main.go index d2a1bee70..2bcefc41f 100644 --- a/examples/vector/main.go +++ b/examples/vector/main.go @@ -33,66 +33,22 @@ func drawEbitenText(screen *ebiten.Image) { var path vector.Path // E - path.MoveTo(60, 20) - path.LineTo(20, 20) - path.LineTo(20, 60) - path.LineTo(60, 60) - path.MoveTo(20, 40) - path.LineTo(60, 40) + path.MoveTo(20, 20) + path.LineTo(20, 70) + path.LineTo(70, 70) + path.LineTo(70, 60) + path.LineTo(30, 60) + path.LineTo(30, 50) + path.LineTo(70, 50) + path.LineTo(70, 40) + path.LineTo(30, 40) + path.LineTo(30, 30) + path.LineTo(70, 30) + path.LineTo(70, 20) - // B - path.MoveTo(110, 20) - path.LineTo(80, 20) - path.LineTo(80, 60) - path.LineTo(110, 60) - path.LineTo(120, 50) - path.LineTo(110, 40) - path.LineTo(120, 30) - path.LineTo(110, 20) - path.LineTo(80, 20) - path.MoveTo(80, 40) - path.LineTo(110, 40) + // TODO: Draw other letters like B, I, T, E, N - // I - path.MoveTo(140, 20) - path.LineTo(140, 60) - - // T - path.MoveTo(160, 20) - path.LineTo(200, 20) - path.MoveTo(180, 20) - path.LineTo(180, 60) - - // E - path.MoveTo(260, 20) - path.LineTo(220, 20) - path.LineTo(220, 60) - path.LineTo(260, 60) - path.MoveTo(220, 40) - path.LineTo(260, 40) - - // N - path.MoveTo(280, 60) - path.LineTo(280, 20) - path.LineTo(320, 60) - path.LineTo(320, 20) - - op := &vector.DrawPathOptions{} - op.LineWidth = 8 - op.StrokeColor = color.RGBA{0xdb, 0x56, 0x20, 0xff} - path.Draw(screen, op) -} - -func drawLines(screen *ebiten.Image) { - var path vector.Path - path.MoveTo(20, 80+float32(counter%320)/4) - path.LineTo(60, 120) - path.LineTo(20, 160-float32(counter%320)/4) - - op := &vector.DrawPathOptions{} - op.LineWidth = 16 - op.StrokeColor = color.White - path.Draw(screen, op) + path.Fill(screen, color.White) } var counter = 0 @@ -104,7 +60,6 @@ func update(screen *ebiten.Image) error { } drawEbitenText(screen) - drawLines(screen) return nil } diff --git a/vector/internal/math/math.go b/vector/internal/math/math.go index 9a8e27fa0..ad0ebf0ff 100644 --- a/vector/internal/math/math.go +++ b/vector/internal/math/math.go @@ -18,39 +18,14 @@ import ( "math" ) +var nan32 = float32(math.NaN()) + type Point struct { X float32 Y float32 } -type Vec2 struct { +type Vector struct { X float32 Y float32 } - -func (p Vec2) Cross(other Vec2) float32 { - return p.X*other.Y - p.Y*other.X -} - -type Segment struct { - P0 Point - P1 Point -} - -func (s Segment) Translate(offset float32) Segment { - a := math.Atan2(float64(s.P1.Y-s.P0.Y), float64(s.P1.X-s.P0.X)) - si, co := math.Sincos(a + math.Pi/2) - dx, dy := float32(co)*offset, float32(si)*offset - return Segment{ - P0: Point{s.P0.X + dx, s.P0.Y + dy}, - P1: Point{s.P1.X + dx, s.P1.Y + dy}, - } -} - -func (s Segment) IntersectionAsLines(other Segment) Point { - v1 := Vec2{other.P1.X - other.P0.X, other.P1.Y - other.P0.Y} - d0 := v1.Cross(Vec2{s.P0.X - other.P0.X, s.P0.Y - other.P0.Y}) - d1 := -v1.Cross(Vec2{s.P1.X - other.P1.X, s.P1.Y - other.P1.Y}) - t := d0 / (d0 + d1) - return Point{s.P0.X + (s.P1.X-s.P0.X)*t, s.P0.Y + (s.P1.Y-s.P0.Y)*t} -} diff --git a/vector/internal/math/math_test.go b/vector/internal/math/math_test.go deleted file mode 100644 index 81eb72379..000000000 --- a/vector/internal/math/math_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2019 The Ebiten Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package math_test - -import ( - "testing" - - . "github.com/hajimehoshi/ebiten/vector/internal/math" -) - -func TestIntersectionAsLine(t *testing.T) { - cases := []struct { - S0 Segment - S1 Segment - Want Point - }{ - { - S0: Segment{Point{0.5, 0}, Point{0.5, 0.5}}, - S1: Segment{Point{1, 1}, Point{2, 1}}, - Want: Point{0.5, 1}, - }, - { - S0: Segment{Point{0.5, 0}, Point{0.5, 1.5}}, - S1: Segment{Point{1, 1}, Point{2, 1}}, - Want: Point{0.5, 1}, - }, - } - for _, c := range cases { - got := c.S0.IntersectionAsLines(c.S1) - want := c.Want - if got != want { - t.Errorf("got: %v, want: %v", got, want) - } - } -} diff --git a/vector/internal/math/triangulate.go b/vector/internal/math/triangulate.go new file mode 100644 index 000000000..53cc0b66a --- /dev/null +++ b/vector/internal/math/triangulate.go @@ -0,0 +1,140 @@ +// Copyright 2019 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "fmt" +) + +func cross(v0, v1 Vector) float32 { + return v0.X*v1.Y - v0.Y*v1.X +} + +func triangleCross(pt0, pt1, pt2 Point) float32 { + return cross(Vector{pt1.X - pt0.X, pt1.Y - pt0.Y}, Vector{pt2.X - pt1.X, pt2.Y - pt1.Y}) +} + +func adjacentIndices(indices []uint16, idx int) (uint16, uint16, uint16) { + return indices[(idx+len(indices)-1)%len(indices)], indices[idx], indices[(idx+1)%len(indices)] +} + +func InTriangle(pt, pt0, pt1, pt2 Point) bool { + c0 := cross(Vector{pt.X - pt0.X, pt.Y - pt0.Y}, Vector{pt1.X - pt0.X, pt1.Y - pt0.Y}) + c1 := cross(Vector{pt.X - pt1.X, pt.Y - pt1.Y}, Vector{pt2.X - pt1.X, pt2.Y - pt1.Y}) + c2 := cross(Vector{pt.X - pt2.X, pt.Y - pt2.Y}, Vector{pt0.X - pt2.X, pt0.Y - pt2.Y}) + return (c0 < 0 && c1 < 0 && c2 < 0) || (c0 > 0 && c1 > 0 && c2 > 0) +} + +func Triangulate(pts []Point) []uint16 { + if len(pts) < 3 { + return nil + } + + var currentIndices []uint16 + + // Remove duplicated points +dup: + for i := range pts { + for j := 0; j < i; j++ { + if pts[i] == pts[j] { + continue dup + } + } + currentIndices = append(currentIndices, uint16(i)) + } + if len(currentIndices) < 3 { + return nil + } + + // Determine the direction of the polygon from the upper-left point. + var upperLeft int + for _, i := range currentIndices { + if pts[upperLeft].X < pts[i].X { + upperLeft = int(i) + } + if pts[upperLeft].X == pts[i].X && pts[upperLeft].Y < pts[i].Y { + upperLeft = int(i) + } + } + i0, i1, i2 := adjacentIndices(currentIndices, upperLeft) + pt0 := pts[i0] + pt1 := pts[i1] + pt2 := pts[i2] + clockwise := triangleCross(pt0, pt1, pt2) < 0 + + var indices []uint16 + + // Triangulation by Ear Clipping. + // https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf + for len(currentIndices) >= 3 { + // Calculate cross-products and remove unneeded vertices. + cs := make([]float32, len(currentIndices)) + idx := -1 + for i := range currentIndices { + i0, i1, i2 := adjacentIndices(currentIndices, i) + pt0 := pts[i0] + pt1 := pts[i1] + pt2 := pts[i2] + c := triangleCross(pt0, pt1, pt2) + if c == 0 { + idx = i + break + } + cs[i] = c + } + if idx != -1 { + currentIndices = append(currentIndices[:idx], currentIndices[idx+1:]...) + continue + } + + idx = -1 + index: + for i := range currentIndices { + i0, i1, i2 := adjacentIndices(currentIndices, i) + pt0 := pts[i0] + pt1 := pts[i1] + pt2 := pts[i2] + + c := cs[i] + if c == 0 { + panic("math: cross value must not be 0") + } + if c < 0 && !clockwise || c > 0 && clockwise { + // The angle is more than 180 degrees. This is not an ear. + continue + } + for j := range currentIndices { + if l := len(currentIndices); j == (i+l-1)%l || j == i || j == (i+1)%l { + continue + } + if InTriangle(pts[currentIndices[j]], pt0, pt1, pt2) { + // If the triangle includes another point, the triangle is not an ear. + continue index + } + } + // The angle is less than 180 degrees. This is an ear. + idx = i + break + } + if idx < 0 { + // TODO: This happens when there is self-crossing. + panic(fmt.Sprintf("math: there is no ear in the polygon: %v", pts)) + } + i0, i1, i2 := adjacentIndices(currentIndices, idx) + indices = append(indices, i0, i1, i2) + currentIndices = append(currentIndices[:idx], currentIndices[idx+1:]...) + } + return indices +} diff --git a/vector/internal/math/triangulate_test.go b/vector/internal/math/triangulate_test.go new file mode 100644 index 000000000..f3d2f1d4f --- /dev/null +++ b/vector/internal/math/triangulate_test.go @@ -0,0 +1,182 @@ +// Copyright 2019 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math_test + +import ( + "reflect" + "testing" + + . "github.com/hajimehoshi/ebiten/vector/internal/math" +) + +func TestIsInTriangle(t *testing.T) { + tests := []struct { + Tri []Point + Pt Point + Out bool + }{ + { + Tri: []Point{ + {0, 0}, + {0, 10}, + {10, 10}, + }, + Pt: Point{1, 9}, + Out: true, + }, + { + Tri: []Point{ + {0, 0}, + {0, 10}, + {10, 10}, + }, + Pt: Point{8, 9}, + Out: true, + }, + { + Tri: []Point{ + {0, 0}, + {0, 10}, + {10, 10}, + }, + Pt: Point{10, 9}, + Out: false, + }, + { + Tri: []Point{ + {3, 5}, + {2, 7}, + {7, 7}, + }, + Pt: Point{3, 6}, + Out: true, + }, + { + Tri: []Point{ + {3, 5}, + {2, 7}, + {7, 7}, + }, + Pt: Point{7, 6}, + Out: false, + }, + } + for _, tc := range tests { + got := InTriangle(tc.Pt, tc.Tri[0], tc.Tri[1], tc.Tri[2]) + want := tc.Out + if got != want { + t.Errorf("InTriangle(%v, %v, %v, %v): got: %t, want: %t", tc.Pt, tc.Tri[0], tc.Tri[1], tc.Tri[2], got, want) + } + } +} + +func TestTriangulate(t *testing.T) { + tests := []struct { + In []Point + Out []uint16 + }{ + { + In: []Point{}, + Out: nil, + }, + { + In: []Point{ + {0, 0}, + }, + Out: nil, + }, + { + In: []Point{ + {0, 0}, + {0, 1}, + }, + Out: nil, + }, + { + In: []Point{ + {0, 0}, + {0, 0}, + {1, 1}, + }, + Out: nil, + }, + { + In: []Point{ + {0, 0}, + {0.5, 0.5}, + {1, 1}, + }, + Out: nil, + }, + { + In: []Point{ + {0, 0}, + {0, 1}, + {1, 1}, + }, + Out: []uint16{2, 0, 1}, + }, + { + In: []Point{ + {0, 0}, + {1, 1}, + {0, 1}, + }, + Out: []uint16{2, 0, 1}, + }, + { + In: []Point{ + {0, 0}, + {0, 1}, + {1, 1}, + {1, 0}, + }, + Out: []uint16{3, 0, 1, 3, 1, 2}, + }, + { + In: []Point{ + {2, 2}, + {2, 7}, + {7, 7}, + {7, 6}, + {3, 6}, + {3, 5}, + }, + Out: []uint16{5, 0, 1, 1, 2, 3, 1, 3, 4, 5, 1, 4}, + }, + { + In: []Point{ + {2, 2}, + {2, 7}, + {7, 7}, + {7, 6}, + {3, 6}, + {3, 5}, + {7, 5}, + {7, 4}, + {3, 4}, + {3, 3}, + }, + Out: []uint16{9, 0, 1, 1, 2, 3, 1, 3, 4, 9, 1, 4, 8, 5, 6, 8, 6, 7}, + }, + } + for _, tc := range tests { + got := Triangulate(tc.In) + want := tc.Out + if !reflect.DeepEqual(got, want) { + t.Errorf("Triangulate(%v): got: %v, want: %v", tc.In, got, want) + } + } +} diff --git a/vector/path.go b/vector/path.go index f8e8fafde..a21ad5aee 100644 --- a/vector/path.go +++ b/vector/path.go @@ -27,190 +27,64 @@ import ( var emptyImage *ebiten.Image func init() { - const w, h = 16, 16 - emptyImage, _ = ebiten.NewImage(w, h, ebiten.FilterDefault) + emptyImage, _ = ebiten.NewImage(1, 1, ebiten.FilterDefault) emptyImage.Fill(color.White) } // Path represents a collection of path segments. type Path struct { - segs [][]math.Segment + segs [][]math.Point cur math.Point } // MoveTo skips the current position of the path to the given position (x, y) without adding any strokes. func (p *Path) MoveTo(x, y float32) { p.cur = math.Point{X: x, Y: y} - if len(p.segs) > 0 && len(p.segs[len(p.segs)-1]) == 0 { - return - } - p.segs = append(p.segs, []math.Segment{}) + p.segs = append(p.segs, []math.Point{p.cur}) } -// LineTo adds a math.Segment to the path, which starts from the current position and ends to the given position (x, y). +// 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, []math.Segment{}) + p.segs = append(p.segs, []math.Point{{X: 0, Y: 0}}) } - p.segs[len(p.segs)-1] = append(p.segs[len(p.segs)-1], math.Segment{P0: p.cur, P1: math.Point{X: x, Y: y}}) + p.segs[len(p.segs)-1] = append(p.segs[len(p.segs)-1], math.Point{X: x, Y: y}) p.cur = math.Point{X: x, Y: y} } -func (p *Path) strokeVertices(lineWidth float32, clr color.Color) (vertices []ebiten.Vertex, indices []uint16) { - const miterLimit = 10 - - if len(p.segs) == 0 { - return nil, nil - } +func (p *Path) Fill(dst *ebiten.Image, clr color.Color) { + var vertices []ebiten.Vertex + var indices []uint16 r, g, b, a := clr.RGBA() - rf, gf, bf, af := float32(r)/0xffff, float32(g)/0xffff, float32(b)/0xffff, float32(a)/0xffff - for _, ss := range p.segs { - idx := uint16(len(vertices)) - for i, s := range ss { - s0 := s.Translate(-lineWidth / 2) - s1 := s.Translate(lineWidth / 2) + var rf, gf, bf, af float32 + if a > 0 { + rf = float32(r) / float32(a) + gf = float32(g) / float32(a) + bf = float32(b) / float32(a) + af = float32(a) / 0xffff + } - if i == 0 { - v0 := s0.P0 - v1 := s1.P0 - vertices = append(vertices, - ebiten.Vertex{ - DstX: v0.X, - DstY: v0.Y, - SrcX: v0.X, - SrcY: v0.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }, - ebiten.Vertex{ - DstX: v1.X, - DstY: v1.Y, - SrcX: v1.X, - SrcY: v1.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }) - } - - v2 := s0.P1 - v3 := s1.P1 - cut := false - if i != len(ss)-1 { - ns := ss[i+1] - nv2 := ns.Translate(-lineWidth / 2).IntersectionAsLines(s0) - nv3 := ns.Translate(lineWidth / 2).IntersectionAsLines(s1) - l := lineWidth / 2 * miterLimit - if (nv2.X-nv3.X)*(nv2.X-nv3.X)+(nv2.Y-nv3.Y)*(nv2.Y-nv3.Y) < l*l { - v2 = nv2 - v3 = nv3 - } else { - cut = true - } - } - - if cut { - ns := ss[i+1] - s2 := ns.Translate(-lineWidth / 2) - s3 := ns.Translate(lineWidth / 2) - vertices = append(vertices, - ebiten.Vertex{ - DstX: s0.P1.X, - DstY: s0.P1.Y, - SrcX: s0.P1.X, - SrcY: s0.P1.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }, - ebiten.Vertex{ - DstX: s1.P1.X, - DstY: s1.P1.Y, - SrcX: s1.P1.X, - SrcY: s1.P1.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }, - ebiten.Vertex{ - DstX: s2.P0.X, - DstY: s2.P0.Y, - SrcX: s2.P0.X, - SrcY: s2.P0.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }, - ebiten.Vertex{ - DstX: s3.P0.X, - DstY: s3.P0.Y, - SrcX: s3.P0.X, - SrcY: s3.P0.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }) - indices = append(indices, idx, idx+1, idx+2, idx+1, idx+2, idx+3, - idx+2, idx+3, idx+4, idx+3, idx+4, idx+5) - idx += 4 - } else { - vertices = append(vertices, - ebiten.Vertex{ - DstX: v2.X, - DstY: v2.Y, - SrcX: v2.X, - SrcY: v2.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }, - ebiten.Vertex{ - DstX: v3.X, - DstY: v3.Y, - SrcX: v3.X, - SrcY: v3.Y, - ColorR: rf, - ColorG: gf, - ColorB: bf, - ColorA: af, - }) - indices = append(indices, idx, idx+1, idx+2, idx+1, idx+2, idx+3) - idx += 2 - } + var base uint16 + for _, seg := range p.segs { + for _, pt := range seg { + vertices = append(vertices, ebiten.Vertex{ + DstX: pt.X, + DstY: pt.Y, + SrcX: 0, + SrcY: 0, + ColorR: rf, + ColorG: gf, + ColorB: bf, + ColorA: af, + }) } + for _, idx := range math.Triangulate(seg) { + indices = append(indices, idx+base) + } + base += uint16(len(seg)) } - - return -} - -// DrawPathOptions is the options specified at (*Path).Draw. -type DrawPathOptions struct { - LineWidth float32 - StrokeColor color.Color -} - -// Draw draws the path by rendering its stroke or filling. -func (p *Path) Draw(target *ebiten.Image, op *DrawPathOptions) { - if op == nil { - return - } - - // TODO: Implement filling - if op.StrokeColor != nil { - vs, is := p.strokeVertices(op.LineWidth, op.StrokeColor) - op := &ebiten.DrawTrianglesOptions{} - op.Address = ebiten.AddressRepeat - target.DrawTriangles(vs, is, emptyImage, op) - } + dst.DrawTriangles(vertices, indices, emptyImage, nil) }