diff --git a/README.md b/README.md index 8c6ea14..7f0b084 100644 --- a/README.md +++ b/README.md @@ -120,17 +120,22 @@ These features *should* work just like their HTML5 counterparts, but there are l - shadowOffset(X/Y) - shadowBlur - isPointInPath +- self intersecting polygons # Missing features - globalCompositeOperation - isPointInStroke - textBaseline hanging and ideographic (currently work just like top and bottom) -- full self intersecting polygon support - image patterns with repeat and transform # Version history +v0.8.0 + +- Self intersecting polygon support +- isPointInPath implemented + v0.7.2 - Fixed build tags for macOS and iOS diff --git a/path2d.go b/path2d.go index 1fa36b9..1b3a26b 100644 --- a/path2d.go +++ b/path2d.go @@ -287,21 +287,21 @@ func (p *Path2D) Rect(x, y, w, h float64) { // func (p *Path2D) Ellipse(...) { // } -func runSubPaths(path *Path2D, fn func(subPath []pathPoint) bool) { +func runSubPaths(path []pathPoint, fn func(subPath []pathPoint) bool) { start := 0 - for i, p := range path.p { + for i, p := range path { if p.flags&pathMove == 0 { continue } if i >= start+3 { - if fn(path.p[start:i]) { + if fn(path[start:i]) { return } } start = i } - if len(path.p) >= start+3 { - fn(path.p[start:]) + if len(path) >= start+3 { + fn(path[start:]) } } @@ -318,7 +318,7 @@ const ( // to the given rule func (p *Path2D) IsPointInPath(x, y float64, rule pathRule) bool { inside := false - runSubPaths(p, func(sp []pathPoint) bool { + runSubPaths(p.p, func(sp []pathPoint) bool { num := 0 prev := sp[len(sp)-1].pos for _, pt := range p.p { diff --git a/paths.go b/paths.go index ae2a985..99ce9df 100644 --- a/paths.go +++ b/paths.go @@ -339,7 +339,7 @@ func (cv *Canvas) FillPath(path *Path2D) { var triBuf [500][2]float64 tris := triBuf[:0] - runSubPaths(path, func(sp []pathPoint) bool { + runSubPaths(path.p, func(sp []pathPoint) bool { tris = appendSubPathTriangles(tris, sp) return false }) @@ -364,8 +364,10 @@ func appendSubPathTriangles(tris [][2]float64, path []pathPoint) [][2]float64 { p1 = p2 } } else if last.flags&pathSelfIntersects != 0 { - path = cutIntersections(path) - tris = triangulatePath(path, tris) + selfIntersectingPathParts(path, func(sp []pathPoint) bool { + tris = triangulatePath(sp, tris) + return false + }) } else { tris = triangulatePath(path, tris) } @@ -385,7 +387,7 @@ func (cv *Canvas) clip(path *Path2D) { var triBuf [500][2]float64 tris := triBuf[:0] - runSubPaths(path, func(sp []pathPoint) bool { + runSubPaths(path.p, func(sp []pathPoint) bool { tris = appendSubPathTriangles(tris, sp) return false }) diff --git a/triangulation.go b/triangulation.go index f9ddf24..f3ae316 100644 --- a/triangulation.go +++ b/triangulation.go @@ -5,6 +5,8 @@ import ( "sort" ) +const samePointTolerance = 1e-20 + func pointIsRightOfLine(a, b, p vec) (bool, bool) { if a[1] == b[1] { return false, false @@ -23,6 +25,24 @@ func pointIsRightOfLine(a, b, p vec) (bool, bool) { return p[0] > x, dir } +func pointIsBelowLine(a, b, p vec) (bool, bool) { + if a[0] == b[0] { + return false, false + } + dir := false + if a[0] > b[0] { + a, b = b, a + dir = !dir + } + if p[0] < a[0] || p[0] > b[0] { + return false, false + } + v := b.sub(a) + r := (p[0] - a[0]) / v[0] + x := a[1] + r*v[1] + return p[1] > x, dir +} + func triangleContainsPoint(a, b, c, p vec) bool { // if point is outside triangle bounds, return false if p[0] < a[0] && p[0] < b[0] && p[0] < c[0] { @@ -104,11 +124,52 @@ func triangulatePath(path []pathPoint, target [][2]float64) [][2]float64 { return target } -func cutIntersections(path []pathPoint) []pathPoint { +/* +tesselation strategy: + +- cut the path at the intersections +- build a network of connected vertices and edges +- find out which side of each edge is inside the polygon +- pick an edge with only one side in the polygon, then follow it + along the side on which the polygon is, always picking the edge + with the smallest angle, until the path goes back to the starting + point. set each edge as no longer having the polygon on that side +- repeat until no more edges have a polygon on either side + +*/ + +type tessNet struct { + verts []tessVert + edges []tessEdge +} + +type tessVert struct { + pos vec + attached []int + count int +} + +type tessEdge struct { + a, b int + leftInside bool + rightInside bool +} + +func cutIntersections(path []pathPoint) tessNet { + // eliminate adjacent duplicate points + for i := 0; i < len(path); i++ { + a := path[i] + b := path[(i+1)%len(path)] + if isSamePoint(a.pos, b.pos, samePointTolerance) { + copy(path[i:], path[i+1:]) + path = path[:len(path)-1] + i-- + } + } + + // find all the cuts type cut struct { from, to int - j int - b bool ratio float64 point vec } @@ -116,8 +177,8 @@ func cutIntersections(path []pathPoint) []pathPoint { var cutBuf [50]cut cuts := cutBuf[:0] + ip := len(path) - 1 for i := 0; i < len(path); i++ { - ip := (i + len(path) - 1) % len(path) a0 := path[ip].pos a1 := path[i].pos for j := i + 1; j < len(path); j++ { @@ -136,21 +197,19 @@ func cutIntersections(path []pathPoint) []pathPoint { to: i, ratio: r1, point: p, - j: j, }) cuts = append(cuts, cut{ from: jp, to: j, ratio: r2, point: p, - j: i, - b: true, }) } + ip = i } if len(cuts) == 0 { - return path + return tessNet{} } sort.Slice(cuts, func(i, j int) bool { @@ -158,14 +217,245 @@ func cutIntersections(path []pathPoint) []pathPoint { return a.to > b.to || (a.to == b.to && a.ratio > b.ratio) }) - newPath := make([]pathPoint, len(path)+len(cuts)) - copy(newPath[:len(path)], path) - + // build vertex and edge lists + verts := make([]tessVert, len(path)+len(cuts)) + for i, pp := range path { + verts[i] = tessVert{ + pos: pp.pos, + count: 2, + } + } for _, cut := range cuts { - copy(newPath[cut.to+1:], newPath[cut.to:]) - newPath[cut.to].next = newPath[cut.to+1].pos - newPath[cut.to].pos = cut.point + copy(verts[cut.to+1:], verts[cut.to:]) + verts[cut.to].pos = cut.point + } + edges := make([]tessEdge, 0, len(path)+len(cuts)*2) + for i := range verts { + next := (i + 1) % len(verts) + edges = append(edges, tessEdge{a: i, b: next}) } - return newPath + // eliminate duplicate points + for i := 0; i < len(verts); i++ { + a := verts[i] + for j := i + 1; j < len(verts); j++ { + b := verts[j] + if isSamePoint(a.pos, b.pos, samePointTolerance) { + copy(verts[j:], verts[j+1:]) + verts = verts[:len(verts)-1] + for k, e := range edges { + if e.a == j { + edges[k].a = i + } else if e.a > j { + edges[k].a-- + } + if e.b == j { + edges[k].b = i + } else if e.b > j { + edges[k].b-- + } + } + verts[i].count += 2 + j-- + } + } + } + + // build the attached edge lists on all vertices + total := 0 + for _, v := range verts { + total += v.count + } + attachedBuf := make([]int, 0, total) + pos := 0 + for i := range verts { + for j, e := range edges { + if e.a == i || e.b == i { + attachedBuf = append(attachedBuf, j) + } + } + verts[i].attached = attachedBuf[pos:len(attachedBuf)] + pos = len(attachedBuf) + } + + return tessNet{verts: verts, edges: edges} +} + +func setPathLeftRightInside(net *tessNet) { + for i, e1 := range net.edges { + a1, b1 := net.verts[e1.a], net.verts[e1.b] + diff := b1.pos.sub(a1.pos) + mid := a1.pos.add(diff.mulf(0.5)) + num := 0 + + if math.Abs(diff[1]) < math.Abs(diff[0]) { + edir := diff[1] > 0 + + for j, e2 := range net.edges { + if i == j { + continue + } + a2, b2 := net.verts[e2.a], net.verts[e2.b] + r, dir := pointIsRightOfLine(a2.pos, b2.pos, mid) + if !r { + continue + } + if dir { + num++ + } else { + num-- + } + } + + if edir { + net.edges[i].leftInside = (num - 1) != 0 + net.edges[i].rightInside = num != 0 + } else { + net.edges[i].leftInside = num != 0 + net.edges[i].rightInside = (num + 1) != 0 + } + } else { + edir := diff[0] > 0 + + for j, e2 := range net.edges { + if i == j { + continue + } + a2, b2 := net.verts[e2.a], net.verts[e2.b] + b, dir := pointIsBelowLine(a2.pos, b2.pos, mid) + if !b { + continue + } + if dir { + num++ + } else { + num-- + } + } + + if edir { + net.edges[i].leftInside = num != 0 + net.edges[i].rightInside = (num - 1) != 0 + } else { + net.edges[i].leftInside = (num + 1) != 0 + net.edges[i].rightInside = num != 0 + } + } + } +} + +func selfIntersectingPathParts(p []pathPoint, partFn func(sp []pathPoint) bool) { + runSubPaths(p, func(sp1 []pathPoint) bool { + net := cutIntersections(sp1) + if net.verts == nil { + return false + } + + setPathLeftRightInside(&net) + + var sp2Buf [50]pathPoint + sp2 := sp2Buf[:0] + + for { + var start, from, cur, count int + var left bool + for i, e := range net.edges { + if e.leftInside != e.rightInside { + count++ + start = e.a + from = i + cur = e.b + if e.leftInside { + left = true + net.edges[i].leftInside = false + } else { + net.edges[i].rightInside = false + } + break + } + } + if count == 0 { + break + } + + // fmt.Println("start", start, from, cur, net.verts[cur], left) + + sp2 = append(sp2, pathPoint{ + pos: net.verts[cur].pos, + flags: pathMove, + }) + + for limit := 0; limit < len(net.edges); limit++ { + ecur := net.edges[from] + acur, bcur := net.verts[ecur.a], net.verts[ecur.b] + dir := bcur.pos.sub(acur.pos) + dirAngle := math.Atan2(dir[1], dir[0]) + minAngleDiff := math.Pi * 2 + var next, nextEdge int + any := false + for _, ei := range net.verts[cur].attached { + if ei == from { + continue + } + e := net.edges[ei] + if (left && !e.leftInside) || (!left && !e.rightInside) { + continue + } + na, nb := net.verts[e.a], net.verts[e.b] + if e.b == cur { + na, nb = nb, na + } + ndir := nb.pos.sub(na.pos) + nextAngle := math.Atan2(ndir[1], ndir[0]) + math.Pi + if nextAngle < dirAngle { + nextAngle += math.Pi * 2 + } else if nextAngle > dirAngle+math.Pi*2 { + nextAngle -= math.Pi * 2 + } + var angleDiff float64 + if left { + angleDiff = nextAngle - dirAngle + } else { + angleDiff = dirAngle - nextAngle + } + if angleDiff < minAngleDiff { + minAngleDiff = angleDiff + nextEdge = ei + if e.a == cur { + next = e.b + } else { + next = e.a + } + any = true + // fmt.Println("-", e, nextEdge, next) + } + } + if !any { + break + } + // fmt.Println(start, from, cur, net.verts[cur], nextEdge, next, net.verts[next]) + if left { + net.edges[nextEdge].leftInside = false + } else { + net.edges[nextEdge].rightInside = false + } + sp2 = append(sp2, pathPoint{ + pos: net.verts[next].pos, + }) + from = nextEdge + cur = next + if next == start { + break + } + } + + stop := partFn(sp2) + if stop { + return true + } + sp2 = sp2[:0] + } + + return false + }) }