diff --git a/canvas.go b/canvas.go index d230252..835f58a 100644 --- a/canvas.go +++ b/canvas.go @@ -19,7 +19,7 @@ type Canvas struct { x, y, w, h int fx, fy, fw, fh float64 - path path + path Path2D convex bool rect bool @@ -52,7 +52,7 @@ type drawState struct { lineDashOffset float64 scissor scissor - clip path + clip Path2D shadowColor glColor shadowOffsetX float64 diff --git a/go.mod b/go.mod index 61d5dff..53f1e41 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,5 @@ require ( golang.org/x/exp v0.0.0-20181106170214-d68db9428509 golang.org/x/image v0.0.0-20181109002202-aa35264064ba golang.org/x/mobile v0.0.0-20181026062114-a27dd33d354d - golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 // indirect + golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect ) diff --git a/go.sum b/go.sum index 62aaa66..dd19333 100644 --- a/go.sum +++ b/go.sum @@ -16,5 +16,5 @@ golang.org/x/image v0.0.0-20181109002202-aa35264064ba h1:tKfAeDKyjJZwxAJ8TPBZaf6 golang.org/x/image v0.0.0-20181109002202-aa35264064ba/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/mobile v0.0.0-20181026062114-a27dd33d354d h1:DuZZDdMFwDrzmycNhCaWSve7Vh+BIrjm7ttgb4fD3Os= golang.org/x/mobile v0.0.0-20181026062114-a27dd33d354d/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/path2d.go b/path2d.go new file mode 100644 index 0000000..ce16412 --- /dev/null +++ b/path2d.go @@ -0,0 +1,254 @@ +package canvas + +import "math" + +type Path2D struct { + p []pathPoint + move vec + cwSum float64 +} + +type pathPoint struct { + pos vec + tf vec + next 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 NewPath2D() *Path2D { + return &Path2D{p: make([]pathPoint, 0, 20)} +} + +// 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, vec{x, y}, 0.1) { + return + } + p.p = append(p.p, pathPoint{pos: vec{x, y}, tf: vec{x, y}, flags: pathMove}) // todo more flags probably + p.cwSum = 0 + p.move = vec{x, y} +} + +// LineTo (see equivalent function on canvas type) +func (p *Path2D) LineTo(x, y float64) { + count := len(p.p) + if count > 0 && isSamePoint(p.p[len(p.p)-1].pos, vec{x, y}, 0.1) { + return + } + if count == 0 { + p.MoveTo(x, y) + return + } + prev := &p.p[count-1] + prev.next = vec{x, y} + prev.flags |= pathAttach + p.p = append(p.p, pathPoint{pos: vec{x, y}, tf: vec{x, y}}) + newp := &p.p[count] + + 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 prev.flags&pathSelfIntersects > 0 { + newp.flags |= pathSelfIntersects + } + + if len(p.p) < 4 { + newp.flags |= pathIsConvex + } else if prev.flags&pathIsConvex > 0 { + cuts := false + b0, b1 := prev.pos, vec{x, y} + for i := 1; i < count; i++ { + a0, a1 := p.p[i-1].pos, p.p[i].pos + _, r1, r2 := lineIntersection(a0, a1, b0, b1) + if r1 > 0 && r1 < 1 && r2 > 0 && r2 < 1 { + cuts = true + break + } + } + if cuts { + newp.flags |= pathSelfIntersects + } else { + prev2 := &p.p[len(p.p)-3] + cw := (newp.flags & pathIsClockwise) > 0 + + ln := prev.pos.sub(prev2.pos) + lo := vec{ln[1], -ln[0]} + dot := newp.pos.sub(prev2.pos).dot(lo) + + if (cw && dot <= 0) || (!cw && dot >= 0) { + newp.flags |= pathIsConvex + } + } + } +} + +// Arc (see equivalent function on canvas type) +func (p *Path2D) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) { + lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0 + + startAngle = math.Mod(startAngle, math.Pi*2) + if startAngle < 0 { + startAngle += math.Pi * 2 + } + endAngle = math.Mod(endAngle, math.Pi*2) + if endAngle < 0 { + endAngle += math.Pi * 2 + } + if !anticlockwise && endAngle <= startAngle { + endAngle += math.Pi * 2 + } else if anticlockwise && endAngle >= startAngle { + endAngle -= math.Pi * 2 + } + const step = math.Pi * 2 / 360 + if anticlockwise { + for a := startAngle; a > endAngle; a -= step { + s, c := math.Sincos(a) + p.LineTo(x+radius*c, y+radius*s) + } + } else { + for a := startAngle; a < endAngle; a += step { + s, c := math.Sincos(a) + p.LineTo(x+radius*c, y+radius*s) + } + } + s, c := math.Sincos(endAngle) + p.LineTo(x+radius*c, y+radius*s) + + 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) { + if len(p.p) == 0 { + return + } + p0, p1, p2 := p.p[len(p.p)-1].pos, vec{x1, y1}, vec{x2, y2} + 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 + } + // cv are the vectors orthogonal to the lines that point to the center of the circle + cv0 := vec{-v0[1], v0[0]} + cv1 := 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() + p.Arc(center[0], center[1], radius, a0, a1, x > 0) +} + +// 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 := vec{x1, y1} + p2 := 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 := vec{x1, y1} + p2 := vec{x2, y2} + p3 := 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) +} + +// ClosePath (see equivalent function on canvas type) +func (p *Path2D) ClosePath() { + if len(p.p) < 2 { + return + } + if isSamePoint(p.p[len(p.p)-1].pos, p.p[0].pos, 0.1) { + return + } + closeIdx := 0 + for i := len(p.p) - 1; i >= 0; i-- { + if p.p[i].flags&pathMove != 0 { + closeIdx = i + break + } + } + 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 (p *Path2D) Ellipse(...) { +// } diff --git a/paths.go b/paths.go index 6b4d079..7c0f405 100644 --- a/paths.go +++ b/paths.go @@ -5,30 +5,6 @@ import ( "unsafe" ) -type path struct { - p []pathPoint - move vec - cwSum float64 -} - -type pathPoint struct { - pos vec - tf vec - next vec - flags pathPointFlag -} - -type pathPointFlag uint8 - -const ( - pathMove pathPointFlag = 1 << iota - pathAttach - pathIsRect - pathIsConvex - pathIsClockwise - pathSelfIntersects -) - // BeginPath clears the current path and starts a new one func (cv *Canvas) BeginPath() { if cv.path.p == nil { @@ -44,116 +20,21 @@ func isSamePoint(a, b vec, maxDist float64) bool { // MoveTo adds a gap and moves the end of the path to x/y func (cv *Canvas) MoveTo(x, y float64) { tf := cv.tf(vec{x, y}) - if len(cv.path.p) > 0 && isSamePoint(cv.path.p[len(cv.path.p)-1].tf, tf, 0.1) { - return - } - cv.path.p = append(cv.path.p, pathPoint{pos: vec{x, y}, tf: tf, flags: pathMove}) - cv.path.cwSum = 0 - cv.path.move = vec{x, y} + cv.path.MoveTo(tf[0], tf[1]) } // LineTo adds a line to the end of the path func (cv *Canvas) LineTo(x, y float64) { - count := len(cv.path.p) - if count > 0 && isSamePoint(cv.path.p[len(cv.path.p)-1].tf, cv.tf(vec{x, y}), 0.1) { - return - } - if count == 0 { - cv.path.p = append(cv.path.p, pathPoint{pos: vec{x, y}, tf: cv.tf(vec{x, y}), flags: pathMove}) - return - } - prev := &cv.path.p[count-1] tf := cv.tf(vec{x, y}) - prev.next = tf - prev.flags |= pathAttach - cv.path.p = append(cv.path.p, pathPoint{pos: vec{x, y}, tf: tf}) - newp := &cv.path.p[count] - - px, py := prev.pos[0], prev.pos[1] - cv.path.cwSum += (x - px) * (y + py) - cwTotal := cv.path.cwSum - cwTotal += (cv.path.move[0] - x) * (cv.path.move[1] + y) - if cwTotal <= 0 { - newp.flags |= pathIsClockwise - } - - if prev.flags&pathSelfIntersects > 0 { - newp.flags |= pathSelfIntersects - } - - if len(cv.path.p) < 4 { - newp.flags |= pathIsConvex - } else if prev.flags&pathIsConvex > 0 { - cuts := false - b0, b1 := prev.pos, vec{x, y} - for i := 1; i < count; i++ { - a0, a1 := cv.path.p[i-1].pos, cv.path.p[i].pos - _, r1, r2 := lineIntersection(a0, a1, b0, b1) - if r1 > 0 && r1 < 1 && r2 > 0 && r2 < 1 { - cuts = true - break - } - } - if cuts { - newp.flags |= pathSelfIntersects - } else { - prev2 := &cv.path.p[len(cv.path.p)-3] - cw := (newp.flags & pathIsClockwise) > 0 - - ln := prev.pos.sub(prev2.pos) - lo := vec{ln[1], -ln[0]} - dot := newp.pos.sub(prev2.pos).dot(lo) - - if (cw && dot <= 0) || (!cw && dot >= 0) { - newp.flags |= pathIsConvex - } - } - } + cv.path.LineTo(tf[0], tf[1]) } // Arc adds a circle segment to the end of the path. x/y is the center, radius // is the radius, startAngle and endAngle are angles in radians, anticlockwise // means that the line is added anticlockwise func (cv *Canvas) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) { - lastWasMove := len(cv.path.p) == 0 || cv.path.p[len(cv.path.p)-1].flags&pathMove != 0 - - startAngle = math.Mod(startAngle, math.Pi*2) - if startAngle < 0 { - startAngle += math.Pi * 2 - } - endAngle = math.Mod(endAngle, math.Pi*2) - if endAngle < 0 { - endAngle += math.Pi * 2 - } - if !anticlockwise && endAngle <= startAngle { - endAngle += math.Pi * 2 - } else if anticlockwise && endAngle >= startAngle { - endAngle -= math.Pi * 2 - } - tr := cv.tf(vec{radius, radius}) - step := 6 / math.Max(tr[0], tr[1]) - if step > 0.8 { - step = 0.8 - } else if step < 0.05 { - step = 0.05 - } - if anticlockwise { - for a := startAngle; a > endAngle; a -= step { - s, c := math.Sincos(a) - cv.LineTo(x+radius*c, y+radius*s) - } - } else { - for a := startAngle; a < endAngle; a += step { - s, c := math.Sincos(a) - cv.LineTo(x+radius*c, y+radius*s) - } - } - s, c := math.Sincos(endAngle) - cv.LineTo(x+radius*c, y+radius*s) - - if lastWasMove { - cv.path.p[len(cv.path.p)-1].flags |= pathIsConvex - } + tf := cv.tf(vec{x, y}) + cv.path.Arc(tf[0], tf[1], radius, startAngle, endAngle, anticlockwise) } // ArcTo adds to the current path by drawing a line toward x1/y1 and a circle @@ -161,123 +42,45 @@ func (cv *Canvas) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise // lines from the end of the path to x1/y1, and from x1/y1 to x2/y2. The line // will only go to where the circle segment would touch the latter line func (cv *Canvas) ArcTo(x1, y1, x2, y2, radius float64) { - if len(cv.path.p) == 0 { - return - } - p0, p1, p2 := cv.path.p[len(cv.path.p)-1].pos, vec{x1, y1}, vec{x2, y2} - 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 { - cv.LineTo(x2, y2) - return - } - // cv are the vectors orthogonal to the lines that point to the center of the circle - cv0 := vec{-v0[1], v0[0]} - cv1 := 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() - cv.Arc(center[0], center[1], radius, a0, a1, x > 0) + tf1 := cv.tf(vec{x1, y1}) + tf2 := cv.tf(vec{x2, y2}) + cv.path.ArcTo(tf1[0], tf1[1], tf2[0], tf2[1], radius) } // QuadraticCurveTo adds a quadratic curve to the path. It uses the current end // point of the path, x1/y1 defines the curve, and x2/y2 is the end point func (cv *Canvas) QuadraticCurveTo(x1, y1, x2, y2 float64) { - if len(cv.path.p) == 0 { - return - } - p0 := cv.path.p[len(cv.path.p)-1].pos - p1 := vec{x1, y1} - p2 := vec{x2, y2} - v0 := p1.sub(p0) - v1 := p2.sub(p1) - - tp0, tp1, tp2 := cv.tf(p0), cv.tf(p1), cv.tf(p2) - tv0 := tp1.sub(tp0) - tv1 := tp2.sub(tp1) - - step := 1 / math.Max(math.Max(tv0[0], tv0[1]), math.Max(tv1[0], tv1[1])) - if step > 0.1 { - step = 0.1 - } else if step < 0.005 { - step = 0.005 - } - - for r := 0.0; r < 1; r += step { - i0 := v0.mulf(r).add(p0) - i1 := v1.mulf(r).add(p1) - p := i1.sub(i0).mulf(r).add(i0) - cv.LineTo(p[0], p[1]) - } + tf1 := cv.tf(vec{x1, y1}) + tf2 := cv.tf(vec{x2, y2}) + cv.path.QuadraticCurveTo(tf1[0], tf1[1], tf2[0], tf2[1]) } // BezierCurveTo adds a bezier curve to the path. It uses the current end point // of the path, x1/y1 and x2/y2 define the curve, and x3/y3 is the end point func (cv *Canvas) BezierCurveTo(x1, y1, x2, y2, x3, y3 float64) { - if len(cv.path.p) == 0 { - return - } - p0 := cv.path.p[len(cv.path.p)-1].pos - p1 := vec{x1, y1} - p2 := vec{x2, y2} - p3 := vec{x3, y3} - v0 := p1.sub(p0) - v1 := p2.sub(p1) - v2 := p3.sub(p2) - - tp0, tp1, tp2, tp3 := cv.tf(p0), cv.tf(p1), cv.tf(p2), cv.tf(p3) - tv0 := tp1.sub(tp0) - tv1 := tp2.sub(tp1) - tv2 := tp3.sub(tp2) - - step := 1 / math.Max(math.Max(math.Max(tv0[0], tv0[1]), math.Max(tv1[0], tv1[1])), math.Max(tv2[0], tv2[1])) - if step > 0.1 { - step = 0.1 - } else if step < 0.005 { - step = 0.005 - } - - 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) - p := j1.sub(j0).mulf(r).add(j0) - cv.LineTo(p[0], p[1]) - } + tf1 := cv.tf(vec{x1, y1}) + tf2 := cv.tf(vec{x2, y2}) + tf3 := cv.tf(vec{x3, y3}) + cv.path.BezierCurveTo(tf1[0], tf1[1], tf2[0], tf2[1], tf3[0], tf3[1]) } // ClosePath closes the path to the beginning of the path or the last point // from a MoveTo call func (cv *Canvas) ClosePath() { - if len(cv.path.p) < 2 { - return - } - if isSamePoint(cv.path.p[len(cv.path.p)-1].tf, cv.path.p[0].tf, 0.1) { - return - } - closeIdx := 0 - for i := len(cv.path.p) - 1; i >= 0; i-- { - if cv.path.p[i].flags&pathMove != 0 { - closeIdx = i - break - } - } - cv.LineTo(cv.path.p[closeIdx].pos[0], cv.path.p[closeIdx].pos[1]) - cv.path.p[len(cv.path.p)-1].next = cv.path.p[closeIdx].next - cv.path.p[len(cv.path.p)-1].flags |= pathAttach + cv.path.ClosePath() } // Stroke uses the current StrokeStyle to draw the path -func (cv *Canvas) Stroke() { +func (cv *Canvas) Stroke(params ...interface{}) { + if len(params) > 0 { + if p, ok := params[0].(*Path2D); ok { + for i := range p.p { + p.p[i].tf = cv.tf(p.p[i].pos) + } + cv.stroke(p.p) + return + } + } cv.stroke(cv.path.p) } diff --git a/testdata/Alpha.png b/testdata/Alpha.png index 225c463..96bdd8b 100755 Binary files a/testdata/Alpha.png and b/testdata/Alpha.png differ diff --git a/testdata/Convex.png b/testdata/Convex.png index 4535321..4151bb9 100755 Binary files a/testdata/Convex.png and b/testdata/Convex.png differ diff --git a/testdata/Curves.png b/testdata/Curves.png index 0f5fb76..e8a4f22 100755 Binary files a/testdata/Curves.png and b/testdata/Curves.png differ diff --git a/testdata/Text.png b/testdata/Text.png index e5ba6b3..8dac94d 100755 Binary files a/testdata/Text.png and b/testdata/Text.png differ