503 lines
12 KiB
Go
503 lines
12 KiB
Go
package canvas
|
|
|
|
import (
|
|
"math"
|
|
|
|
"git.mstar.dev/mstar/canvas/backend/backendbase"
|
|
)
|
|
|
|
// Path2D is a type that holds a predefined path which can be drawn
|
|
// with a single call
|
|
type Path2D struct {
|
|
cv *Canvas
|
|
p []pathPoint
|
|
move backendbase.Vec
|
|
cwSum float64
|
|
|
|
standalone bool
|
|
fillCache []backendbase.Vec
|
|
|
|
noSelfIntersection bool
|
|
}
|
|
|
|
type pathPoint struct {
|
|
pos backendbase.Vec
|
|
next backendbase.Vec
|
|
flags pathPointFlag
|
|
}
|
|
|
|
type pathPointFlag uint8
|
|
|
|
const (
|
|
pathMove pathPointFlag = 1 << iota
|
|
pathAttach
|
|
pathIsRect
|
|
pathIsConvex
|
|
pathIsClockwise
|
|
pathSelfIntersects
|
|
)
|
|
|
|
// NewPath2D creates a new Path2D and returns it
|
|
func (cv *Canvas) NewPath2D() *Path2D {
|
|
return &Path2D{cv: cv, p: make([]pathPoint, 0, 20), standalone: true}
|
|
}
|
|
|
|
func (p *Path2D) clearCache() {
|
|
p.fillCache = nil
|
|
}
|
|
|
|
// func (p *Path2D) AddPath(p2 *Path2D) {
|
|
// }
|
|
|
|
// MoveTo (see equivalent function on canvas type)
|
|
func (p *Path2D) MoveTo(x, y float64) {
|
|
if len(p.p) > 0 && isSamePoint(p.p[len(p.p)-1].pos, backendbase.Vec{x, y}, 0.1) {
|
|
return
|
|
}
|
|
p.clearCache()
|
|
p.p = append(p.p, pathPoint{pos: backendbase.Vec{x, y}, flags: pathMove | pathIsConvex})
|
|
p.cwSum = 0
|
|
p.move = backendbase.Vec{x, y}
|
|
}
|
|
|
|
// LineTo (see equivalent function on canvas type)
|
|
func (p *Path2D) LineTo(x, y float64) {
|
|
p.lineTo(x, y, true)
|
|
}
|
|
|
|
func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) {
|
|
count := len(p.p)
|
|
if count > 0 && isSamePoint(p.p[len(p.p)-1].pos, backendbase.Vec{x, y}, 0.1) {
|
|
return
|
|
}
|
|
p.clearCache()
|
|
if count == 0 {
|
|
p.MoveTo(x, y)
|
|
return
|
|
}
|
|
prev := &p.p[count-1]
|
|
prev.next = backendbase.Vec{x, y}
|
|
prev.flags |= pathAttach
|
|
p.p = append(p.p, pathPoint{pos: backendbase.Vec{x, y}})
|
|
newp := &p.p[count]
|
|
|
|
if prev.flags&pathIsConvex > 0 {
|
|
px, py := prev.pos[0], prev.pos[1]
|
|
p.cwSum += (x - px) * (y + py)
|
|
cwTotal := p.cwSum
|
|
cwTotal += (p.move[0] - x) * (p.move[1] + y)
|
|
if cwTotal <= 0 {
|
|
newp.flags |= pathIsClockwise
|
|
}
|
|
}
|
|
|
|
if len(p.p) < 4 || Performance.AssumeConvex {
|
|
newp.flags |= pathIsConvex
|
|
} else if prev.flags&pathIsConvex > 0 {
|
|
prev2 := &p.p[count-2]
|
|
cw := (prev.flags & pathIsClockwise) > 0
|
|
|
|
ln := prev.pos.Sub(prev2.pos)
|
|
lo := backendbase.Vec{ln[1], -ln[0]}
|
|
dot := newp.pos.Sub(prev2.pos).Dot(lo)
|
|
|
|
if (cw && dot <= 0) || (!cw && dot >= 0) {
|
|
newp.flags |= pathIsConvex
|
|
}
|
|
}
|
|
|
|
csi := checkSelfIntersection && !Performance.IgnoreSelfIntersections && !p.noSelfIntersection
|
|
|
|
if prev.flags&pathSelfIntersects > 0 {
|
|
newp.flags |= pathSelfIntersects
|
|
} else if newp.flags&pathIsConvex == 0 && newp.flags&pathSelfIntersects == 0 && csi {
|
|
|
|
cuts := false
|
|
var cutPoint backendbase.Vec
|
|
b0, b1 := prev.pos, backendbase.Vec{x, y}
|
|
for i := 1; i < count; i++ {
|
|
a0, a1 := p.p[i-1].pos, p.p[i].pos
|
|
var r1, r2 float64
|
|
cutPoint, r1, r2 = lineIntersection(a0, a1, b0, b1)
|
|
if r1 > 0 && r1 < 1 && r2 > 0 && r2 < 1 {
|
|
cuts = true
|
|
break
|
|
}
|
|
}
|
|
if cuts && !isSamePoint(cutPoint, backendbase.Vec{x, y}, samePointTolerance) {
|
|
newp.flags |= pathSelfIntersects
|
|
}
|
|
}
|
|
}
|
|
|
|
// Arc (see equivalent function on canvas type)
|
|
func (p *Path2D) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) {
|
|
p.arc(x, y, radius, startAngle, endAngle, anticlockwise, backendbase.MatIdentity, true)
|
|
}
|
|
|
|
func (p *Path2D) arc(
|
|
x, y, radius, startAngle, endAngle float64,
|
|
anticlockwise bool,
|
|
m backendbase.Mat,
|
|
ident bool,
|
|
) {
|
|
checkSelfIntersection := len(p.p) > 0
|
|
|
|
lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0
|
|
|
|
if endAngle == startAngle {
|
|
s, c := math.Sincos(endAngle)
|
|
pt := backendbase.Vec{x + radius*c, y + radius*s}
|
|
if !ident {
|
|
pt = pt.MulMat(m)
|
|
}
|
|
p.lineTo(pt[0], pt[1], checkSelfIntersection)
|
|
|
|
if lastWasMove {
|
|
p.p[len(p.p)-1].flags |= pathIsConvex
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if !anticlockwise && endAngle < startAngle {
|
|
endAngle = startAngle + (2*math.Pi - math.Mod(startAngle-endAngle, math.Pi*2))
|
|
} else if anticlockwise && endAngle > startAngle {
|
|
endAngle = startAngle - (2*math.Pi - math.Mod(endAngle-startAngle, math.Pi*2))
|
|
}
|
|
|
|
if !anticlockwise {
|
|
diff := endAngle - startAngle
|
|
if diff >= math.Pi*4 {
|
|
diff = math.Mod(diff, math.Pi*2) + math.Pi*2
|
|
endAngle = startAngle + diff
|
|
}
|
|
} else {
|
|
diff := startAngle - endAngle
|
|
if diff >= math.Pi*4 {
|
|
diff = math.Mod(diff, math.Pi*2) + math.Pi*2
|
|
endAngle = startAngle - diff
|
|
}
|
|
}
|
|
|
|
const step = math.Pi * 2 / 90
|
|
if !anticlockwise {
|
|
for a := startAngle; a < endAngle; a += step {
|
|
s, c := math.Sincos(a)
|
|
pt := backendbase.Vec{x + radius*c, y + radius*s}
|
|
if !ident {
|
|
pt = pt.MulMat(m)
|
|
}
|
|
p.lineTo(pt[0], pt[1], checkSelfIntersection)
|
|
}
|
|
} else {
|
|
for a := startAngle; a > endAngle; a -= step {
|
|
s, c := math.Sincos(a)
|
|
pt := backendbase.Vec{x + radius*c, y + radius*s}
|
|
if !ident {
|
|
pt = pt.MulMat(m)
|
|
}
|
|
p.lineTo(pt[0], pt[1], checkSelfIntersection)
|
|
}
|
|
}
|
|
s, c := math.Sincos(endAngle)
|
|
pt := backendbase.Vec{x + radius*c, y + radius*s}
|
|
if !ident {
|
|
pt = pt.MulMat(m)
|
|
}
|
|
p.lineTo(pt[0], pt[1], checkSelfIntersection)
|
|
|
|
if lastWasMove {
|
|
p.p[len(p.p)-1].flags |= pathIsConvex
|
|
}
|
|
}
|
|
|
|
// ArcTo (see equivalent function on canvas type)
|
|
func (p *Path2D) ArcTo(x1, y1, x2, y2, radius float64) {
|
|
p.arcTo(x1, y1, x2, y2, radius, backendbase.MatIdentity, true)
|
|
}
|
|
|
|
func (p *Path2D) arcTo(x1, y1, x2, y2, radius float64, m backendbase.Mat, ident bool) {
|
|
if len(p.p) == 0 {
|
|
return
|
|
}
|
|
p0, p1, p2 := p.p[len(p.p)-1].pos, backendbase.Vec{x1, y1}, backendbase.Vec{x2, y2}
|
|
if !ident {
|
|
p0 = p0.MulMat(m.Invert())
|
|
}
|
|
v0, v1 := p0.Sub(p1).Norm(), p2.Sub(p1).Norm()
|
|
angle := math.Acos(v0.Dot(v1))
|
|
// should be in the range [0-pi]. if parallel, use a straight line
|
|
if angle <= 0 || angle >= math.Pi {
|
|
p.LineTo(x2, y2)
|
|
return
|
|
}
|
|
// cv0 and cv1 are vectors that point to the center of the circle
|
|
cv0 := backendbase.Vec{-v0[1], v0[0]}
|
|
cv1 := backendbase.Vec{v1[1], -v1[0]}
|
|
x := cv1.Sub(cv0).Div(v0.Sub(v1))[0] * radius
|
|
if x < 0 {
|
|
cv0 = cv0.Mulf(-1)
|
|
cv1 = cv1.Mulf(-1)
|
|
}
|
|
center := p1.Add(v0.Mulf(math.Abs(x))).Add(cv0.Mulf(radius))
|
|
a0, a1 := cv0.Mulf(-1).Atan2(), cv1.Mulf(-1).Atan2()
|
|
if x > 0 {
|
|
if a1-a0 > 0 {
|
|
a0 += math.Pi * 2
|
|
}
|
|
} else {
|
|
if a0-a1 > 0 {
|
|
a1 += math.Pi * 2
|
|
}
|
|
}
|
|
p.arc(center[0], center[1], radius, a0, a1, x > 0, m, ident)
|
|
}
|
|
|
|
// QuadraticCurveTo (see equivalent function on canvas type)
|
|
func (p *Path2D) QuadraticCurveTo(x1, y1, x2, y2 float64) {
|
|
if len(p.p) == 0 {
|
|
return
|
|
}
|
|
p0 := p.p[len(p.p)-1].pos
|
|
p1 := backendbase.Vec{x1, y1}
|
|
p2 := backendbase.Vec{x2, y2}
|
|
v0 := p1.Sub(p0)
|
|
v1 := p2.Sub(p1)
|
|
|
|
const step = 0.01
|
|
|
|
for r := 0.0; r < 1; r += step {
|
|
i0 := v0.Mulf(r).Add(p0)
|
|
i1 := v1.Mulf(r).Add(p1)
|
|
pt := i1.Sub(i0).Mulf(r).Add(i0)
|
|
p.LineTo(pt[0], pt[1])
|
|
}
|
|
p.LineTo(x2, y2)
|
|
}
|
|
|
|
// BezierCurveTo (see equivalent function on canvas type)
|
|
func (p *Path2D) BezierCurveTo(x1, y1, x2, y2, x3, y3 float64) {
|
|
if len(p.p) == 0 {
|
|
return
|
|
}
|
|
p0 := p.p[len(p.p)-1].pos
|
|
p1 := backendbase.Vec{x1, y1}
|
|
p2 := backendbase.Vec{x2, y2}
|
|
p3 := backendbase.Vec{x3, y3}
|
|
v0 := p1.Sub(p0)
|
|
v1 := p2.Sub(p1)
|
|
v2 := p3.Sub(p2)
|
|
|
|
const step = 0.01
|
|
|
|
for r := 0.0; r < 1; r += step {
|
|
i0 := v0.Mulf(r).Add(p0)
|
|
i1 := v1.Mulf(r).Add(p1)
|
|
i2 := v2.Mulf(r).Add(p2)
|
|
iv0 := i1.Sub(i0)
|
|
iv1 := i2.Sub(i1)
|
|
j0 := iv0.Mulf(r).Add(i0)
|
|
j1 := iv1.Mulf(r).Add(i1)
|
|
pt := j1.Sub(j0).Mulf(r).Add(j0)
|
|
p.LineTo(pt[0], pt[1])
|
|
}
|
|
p.LineTo(x3, y3)
|
|
}
|
|
|
|
// Ellipse (see equivalent function on canvas type)
|
|
func (p *Path2D) Ellipse(
|
|
x, y, radiusX, radiusY, rotation, startAngle, endAngle float64,
|
|
anticlockwise bool,
|
|
) {
|
|
checkSelfIntersection := len(p.p) > 0
|
|
|
|
rs, rc := math.Sincos(rotation)
|
|
|
|
lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0
|
|
|
|
if endAngle == startAngle {
|
|
s, c := math.Sincos(endAngle)
|
|
rx, ry := radiusX*c, radiusY*s
|
|
rx, ry = rx*rc-ry*rs, rx*rs+ry*rc
|
|
p.lineTo(x+rx, y+ry, checkSelfIntersection)
|
|
|
|
if lastWasMove {
|
|
p.p[len(p.p)-1].flags |= pathIsConvex
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if !anticlockwise && endAngle < startAngle {
|
|
endAngle = startAngle + (2*math.Pi - math.Mod(startAngle-endAngle, math.Pi*2))
|
|
} else if anticlockwise && endAngle > startAngle {
|
|
endAngle = startAngle - (2*math.Pi - math.Mod(endAngle-startAngle, math.Pi*2))
|
|
}
|
|
|
|
if !anticlockwise {
|
|
diff := endAngle - startAngle
|
|
if diff >= math.Pi*4 {
|
|
diff = math.Mod(diff, math.Pi*2) + math.Pi*2
|
|
endAngle = startAngle + diff
|
|
}
|
|
} else {
|
|
diff := startAngle - endAngle
|
|
if diff >= math.Pi*4 {
|
|
diff = math.Mod(diff, math.Pi*2) + math.Pi*2
|
|
endAngle = startAngle - diff
|
|
}
|
|
}
|
|
|
|
const step = math.Pi * 2 / 90
|
|
if !anticlockwise {
|
|
for a := startAngle; a < endAngle; a += step {
|
|
s, c := math.Sincos(a)
|
|
rx, ry := radiusX*c, radiusY*s
|
|
rx, ry = rx*rc-ry*rs, rx*rs+ry*rc
|
|
p.lineTo(x+rx, y+ry, checkSelfIntersection)
|
|
}
|
|
} else {
|
|
for a := startAngle; a > endAngle; a -= step {
|
|
s, c := math.Sincos(a)
|
|
rx, ry := radiusX*c, radiusY*s
|
|
rx, ry = rx*rc-ry*rs, rx*rs+ry*rc
|
|
p.lineTo(x+rx, y+ry, checkSelfIntersection)
|
|
}
|
|
}
|
|
s, c := math.Sincos(endAngle)
|
|
rx, ry := radiusX*c, radiusY*s
|
|
rx, ry = rx*rc-ry*rs, rx*rs+ry*rc
|
|
p.lineTo(x+rx, y+ry, checkSelfIntersection)
|
|
|
|
if lastWasMove {
|
|
p.p[len(p.p)-1].flags |= pathIsConvex
|
|
}
|
|
}
|
|
|
|
// ClosePath (see equivalent function on canvas type)
|
|
func (p *Path2D) ClosePath() {
|
|
if len(p.p) < 2 {
|
|
return
|
|
}
|
|
closeIdx := 0
|
|
for i := len(p.p) - 1; i >= 0; i-- {
|
|
if p.p[i].flags&pathMove != 0 {
|
|
closeIdx = i
|
|
break
|
|
}
|
|
}
|
|
if !isSamePoint(p.p[len(p.p)-1].pos, p.p[0].pos, 0.1) {
|
|
p.LineTo(p.p[closeIdx].pos[0], p.p[closeIdx].pos[1])
|
|
}
|
|
p.p[len(p.p)-1].next = p.p[closeIdx].next
|
|
p.p[len(p.p)-1].flags |= pathAttach
|
|
}
|
|
|
|
// Rect (see equivalent function on canvas type)
|
|
func (p *Path2D) Rect(x, y, w, h float64) {
|
|
lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0
|
|
p.MoveTo(x, y)
|
|
p.LineTo(x+w, y)
|
|
p.LineTo(x+w, y+h)
|
|
p.LineTo(x, y+h)
|
|
p.LineTo(x, y)
|
|
if lastWasMove {
|
|
p.p[len(p.p)-1].flags |= pathIsRect
|
|
p.p[len(p.p)-1].flags |= pathIsConvex
|
|
}
|
|
}
|
|
|
|
func runSubPaths(path []pathPoint, close bool, fn func(subPath []pathPoint) bool) {
|
|
start := 0
|
|
for i, p := range path {
|
|
if p.flags&pathMove == 0 {
|
|
continue
|
|
}
|
|
if i >= start+3 {
|
|
if runSubPath(path[start:i], close, fn) {
|
|
return
|
|
}
|
|
}
|
|
start = i
|
|
}
|
|
if len(path) >= start+3 {
|
|
runSubPath(path[start:], close, fn)
|
|
}
|
|
}
|
|
|
|
func runSubPath(path []pathPoint, close bool, fn func(subPath []pathPoint) bool) bool {
|
|
if !close || path[0].pos == path[len(path)-1].pos {
|
|
return fn(path)
|
|
}
|
|
|
|
var buf [64]pathPoint
|
|
path2 := Path2D{
|
|
p: append(buf[:0], path...),
|
|
move: path[0].pos,
|
|
}
|
|
path2.lineTo(path[0].pos[0], path[0].pos[1], true)
|
|
return fn(path2.p)
|
|
}
|
|
|
|
type pathRule uint8
|
|
|
|
// Path rule constants. See https://en.wikipedia.org/wiki/Nonzero-rule
|
|
// and https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule
|
|
const (
|
|
NonZero pathRule = iota
|
|
EvenOdd
|
|
)
|
|
|
|
// IsPointInPath returns true if the point is in the path according
|
|
// to the given rule
|
|
func (p *Path2D) IsPointInPath(x, y float64, rule pathRule) bool {
|
|
inside := false
|
|
runSubPaths(p.p, false, func(sp []pathPoint) bool {
|
|
num := 0
|
|
prev := sp[len(sp)-1].pos
|
|
for _, pt := range sp {
|
|
r, dir := pointIsRightOfLine(prev, pt.pos, backendbase.Vec{x, y})
|
|
prev = pt.pos
|
|
if !r {
|
|
continue
|
|
}
|
|
if dir {
|
|
num++
|
|
} else {
|
|
num--
|
|
}
|
|
}
|
|
|
|
if rule == NonZero {
|
|
inside = num != 0
|
|
} else {
|
|
inside = num%2 == 0
|
|
}
|
|
|
|
return inside
|
|
})
|
|
return inside
|
|
}
|
|
|
|
// IsPointInStroke returns true if the point is in the stroke
|
|
func (p *Path2D) IsPointInStroke(x, y float64) bool {
|
|
if len(p.p) == 0 {
|
|
return false
|
|
}
|
|
|
|
var triBuf [500]backendbase.Vec
|
|
tris := p.cv.strokeTris(p, p.cv.state.transform, backendbase.Mat{}, false, triBuf[:0])
|
|
|
|
pt := backendbase.Vec{x, y}
|
|
|
|
for i := 0; i < len(tris); i += 3 {
|
|
a := backendbase.Vec{tris[i][0], tris[i][1]}
|
|
b := backendbase.Vec{tris[i+1][0], tris[i+1][1]}
|
|
c := backendbase.Vec{tris[i+2][0], tris[i+2][1]}
|
|
if triangleContainsPoint(a, b, c, pt) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|