moved math code to backendbase package so that backends can also use it

This commit is contained in:
Thomas Friedel 2020-03-22 10:07:18 +01:00
parent 066f4f55bb
commit cc9247c627
9 changed files with 364 additions and 357 deletions

140
backend/backendbase/math.go Normal file
View file

@ -0,0 +1,140 @@
package backendbase
import (
"fmt"
"math"
)
type Vec [2]float64
func (v Vec) String() string {
return fmt.Sprintf("[%f,%f]", v[0], v[1])
}
func (v Vec) Add(v2 Vec) Vec {
return Vec{v[0] + v2[0], v[1] + v2[1]}
}
func (v Vec) Sub(v2 Vec) Vec {
return Vec{v[0] - v2[0], v[1] - v2[1]}
}
func (v Vec) Mul(v2 Vec) Vec {
return Vec{v[0] * v2[0], v[1] * v2[1]}
}
func (v Vec) Mulf(f float64) Vec {
return Vec{v[0] * f, v[1] * f}
}
func (v Vec) MulMat(m Mat) Vec {
return Vec{
m[0]*v[0] + m[2]*v[1] + m[4],
m[1]*v[0] + m[3]*v[1] + m[5]}
}
func (v Vec) MulMat2(m Mat2) Vec {
return Vec{m[0]*v[0] + m[2]*v[1], m[1]*v[0] + m[3]*v[1]}
}
func (v Vec) Div(v2 Vec) Vec {
return Vec{v[0] / v2[0], v[1] / v2[1]}
}
func (v Vec) Divf(f float64) Vec {
return Vec{v[0] / f, v[1] / f}
}
func (v Vec) Dot(v2 Vec) float64 {
return v[0]*v2[0] + v[1]*v2[1]
}
func (v Vec) Len() float64 {
return math.Sqrt(v[0]*v[0] + v[1]*v[1])
}
func (v Vec) LenSqr() float64 {
return v[0]*v[0] + v[1]*v[1]
}
func (v Vec) Norm() Vec {
return v.Mulf(1.0 / v.Len())
}
func (v Vec) Atan2() float64 {
return math.Atan2(v[1], v[0])
}
func (v Vec) Angle() float64 {
return math.Pi*0.5 - math.Atan2(v[1], v[0])
}
func (v Vec) AngleTo(v2 Vec) float64 {
return math.Acos(v.Norm().Dot(v2.Norm()))
}
type Mat [6]float64
func (m *Mat) String() string {
return fmt.Sprintf("[%f,%f,0,\n %f,%f,0,\n %f,%f,1,]", m[0], m[2], m[4], m[1], m[3], m[5])
}
var MatIdentity = Mat{
1, 0,
0, 1,
0, 0}
func MatTranslate(v Vec) Mat {
return Mat{
1, 0,
0, 1,
v[0], v[1]}
}
func MatScale(v Vec) Mat {
return Mat{
v[0], 0,
0, v[1],
0, 0}
}
func MatRotate(radians float64) Mat {
s, c := math.Sincos(radians)
return Mat{
c, s,
-s, c,
0, 0}
}
func (m Mat) Mul(m2 Mat) Mat {
return Mat{
m[0]*m2[0] + m[1]*m2[2],
m[0]*m2[1] + m[1]*m2[3],
m[2]*m2[0] + m[3]*m2[2],
m[2]*m2[1] + m[3]*m2[3],
m[4]*m2[0] + m[5]*m2[2] + m2[4],
m[4]*m2[1] + m[5]*m2[3] + m2[5]}
}
func (m Mat) Invert() Mat {
identity := 1.0 / (m[0]*m[3] - m[2]*m[1])
return Mat{
m[3] * identity,
-m[1] * identity,
-m[2] * identity,
m[0] * identity,
(m[2]*m[5] - m[3]*m[4]) * identity,
(m[1]*m[4] - m[0]*m[5]) * identity,
}
}
type Mat2 [4]float64
func (m Mat) Mat2() Mat2 {
return Mat2{m[0], m[1], m[2], m[3]}
}
func (m *Mat2) String() string {
return fmt.Sprintf("[%f,%f,\n %f,%f]", m[0], m[2], m[1], m[3])
}

View file

@ -35,7 +35,7 @@ type Canvas struct {
} }
type drawState struct { type drawState struct {
transform mat transform backendbase.Mat
fill drawStyle fill drawStyle
stroke drawStyle stroke drawStyle
font *Font font *Font
@ -150,7 +150,7 @@ func New(backend backendbase.Backend) *Canvas {
cv.state.globalAlpha = 1 cv.state.globalAlpha = 1
cv.state.fill.color = color.RGBA{A: 255} cv.state.fill.color = color.RGBA{A: 255}
cv.state.stroke.color = color.RGBA{A: 255} cv.state.stroke.color = color.RGBA{A: 255}
cv.state.transform = matIdentity cv.state.transform = backendbase.MatIdentity
cv.path.cv = cv cv.path.cv = cv
return cv return cv
} }
@ -170,8 +170,8 @@ func (cv *Canvas) Height() int {
// Size returns the internal width and height of the canvas // Size returns the internal width and height of the canvas
func (cv *Canvas) Size() (int, int) { return cv.b.Size() } func (cv *Canvas) Size() (int, int) { return cv.b.Size() }
func (cv *Canvas) tf(v vec) vec { func (cv *Canvas) tf(v backendbase.Vec) backendbase.Vec {
return v.mulMat(cv.state.transform) return v.MulMat(cv.state.transform)
} }
const alphaTexSize = 2048 const alphaTexSize = 2048
@ -380,7 +380,7 @@ func (cv *Canvas) Restore() {
cv.b.ClearClip() cv.b.ClearClip()
for _, st := range cv.stateStack { for _, st := range cv.stateStack {
if len(st.clip.p) > 0 { if len(st.clip.p) > 0 {
cv.clip(&st.clip, matIdentity) cv.clip(&st.clip, backendbase.MatIdentity)
} }
} }
cv.state = cv.stateStack[l-1] cv.state = cv.stateStack[l-1]
@ -389,27 +389,27 @@ func (cv *Canvas) Restore() {
// Scale updates the current transformation with a scaling by the given values // Scale updates the current transformation with a scaling by the given values
func (cv *Canvas) Scale(x, y float64) { func (cv *Canvas) Scale(x, y float64) {
cv.state.transform = matScale(vec{x, y}).mul(cv.state.transform) cv.state.transform = backendbase.MatScale(backendbase.Vec{x, y}).Mul(cv.state.transform)
} }
// Translate updates the current transformation with a translation by the given values // Translate updates the current transformation with a translation by the given values
func (cv *Canvas) Translate(x, y float64) { func (cv *Canvas) Translate(x, y float64) {
cv.state.transform = matTranslate(vec{x, y}).mul(cv.state.transform) cv.state.transform = backendbase.MatTranslate(backendbase.Vec{x, y}).Mul(cv.state.transform)
} }
// Rotate updates the current transformation with a rotation by the given angle // Rotate updates the current transformation with a rotation by the given angle
func (cv *Canvas) Rotate(angle float64) { func (cv *Canvas) Rotate(angle float64) {
cv.state.transform = matRotate(angle).mul(cv.state.transform) cv.state.transform = backendbase.MatRotate(angle).Mul(cv.state.transform)
} }
// Transform updates the current transformation with the given matrix // Transform updates the current transformation with the given matrix
func (cv *Canvas) Transform(a, b, c, d, e, f float64) { func (cv *Canvas) Transform(a, b, c, d, e, f float64) {
cv.state.transform = mat{a, b, c, d, e, f}.mul(cv.state.transform) cv.state.transform = backendbase.Mat{a, b, c, d, e, f}.Mul(cv.state.transform)
} }
// SetTransform replaces the current transformation with the given matrix // SetTransform replaces the current transformation with the given matrix
func (cv *Canvas) SetTransform(a, b, c, d, e, f float64) { func (cv *Canvas) SetTransform(a, b, c, d, e, f float64) {
cv.state.transform = mat{a, b, c, d, e, f} cv.state.transform = backendbase.Mat{a, b, c, d, e, f}
} }
// SetShadowColor sets the color of the shadow. If it is fully transparent (default) // SetShadowColor sets the color of the shadow. If it is fully transparent (default)
@ -456,14 +456,14 @@ func (cv *Canvas) IsPointInStroke(x, y float64) bool {
} }
var triBuf [500][2]float64 var triBuf [500][2]float64
tris := cv.strokeTris(&cv.path, cv.state.transform.invert(), true, triBuf[:0]) tris := cv.strokeTris(&cv.path, cv.state.transform.Invert(), true, triBuf[:0])
pt := vec{x, y} pt := backendbase.Vec{x, y}
for i := 0; i < len(tris); i += 3 { for i := 0; i < len(tris); i += 3 {
a := vec{tris[i][0], tris[i][1]} a := backendbase.Vec{tris[i][0], tris[i][1]}
b := vec{tris[i+1][0], tris[i+1][1]} b := backendbase.Vec{tris[i+1][0], tris[i+1][1]}
c := vec{tris[i+2][0], tris[i+2][1]} c := backendbase.Vec{tris[i+2][0], tris[i+2][1]}
if triangleContainsPoint(a, b, c, pt) { if triangleContainsPoint(a, b, c, pt) {
return true return true
} }

View file

@ -13,7 +13,7 @@ import (
// will correspond to a straight line // will correspond to a straight line
type LinearGradient struct { type LinearGradient struct {
cv *Canvas cv *Canvas
from, to vec from, to backendbase.Vec
created bool created bool
loaded bool loaded bool
opaque bool opaque bool
@ -27,7 +27,7 @@ type LinearGradient struct {
// will correspond to a circle // will correspond to a circle
type RadialGradient struct { type RadialGradient struct {
cv *Canvas cv *Canvas
from, to vec from, to backendbase.Vec
radFrom float64 radFrom float64
radTo float64 radTo float64
created bool created bool
@ -44,8 +44,8 @@ func (cv *Canvas) CreateLinearGradient(x0, y0, x1, y1 float64) *LinearGradient {
lg := &LinearGradient{ lg := &LinearGradient{
cv: cv, cv: cv,
opaque: true, opaque: true,
from: vec{x0, y0}, from: backendbase.Vec{x0, y0},
to: vec{x1, y1}, to: backendbase.Vec{x1, y1},
data: make(backendbase.Gradient, 0, 20), data: make(backendbase.Gradient, 0, 20),
} }
runtime.SetFinalizer(lg, func(*LinearGradient) { runtime.SetFinalizer(lg, func(*LinearGradient) {
@ -62,8 +62,8 @@ func (cv *Canvas) CreateRadialGradient(x0, y0, r0, x1, y1, r1 float64) *RadialGr
rg := &RadialGradient{ rg := &RadialGradient{
cv: cv, cv: cv,
opaque: true, opaque: true,
from: vec{x0, y0}, from: backendbase.Vec{x0, y0},
to: vec{x1, y1}, to: backendbase.Vec{x1, y1},
radFrom: r0, radFrom: r0,
radTo: r1, radTo: r1,
data: make(backendbase.Gradient, 0, 20), data: make(backendbase.Gradient, 0, 20),

View file

@ -182,10 +182,10 @@ func (cv *Canvas) DrawImage(image interface{}, coords ...float64) {
} }
var data [4][2]float64 var data [4][2]float64
data[0] = cv.tf(vec{dx, dy}) data[0] = cv.tf(backendbase.Vec{dx, dy})
data[1] = cv.tf(vec{dx, dy + dh}) data[1] = cv.tf(backendbase.Vec{dx, dy + dh})
data[2] = cv.tf(vec{dx + dw, dy + dh}) data[2] = cv.tf(backendbase.Vec{dx + dw, dy + dh})
data[3] = cv.tf(vec{dx + dw, dy}) data[3] = cv.tf(backendbase.Vec{dx + dw, dy})
cv.drawShadow(data[:], nil, false) cv.drawShadow(data[:], nil, false)
@ -207,7 +207,7 @@ func (cv *Canvas) PutImageData(img *image.RGBA, x, y int) {
type ImagePattern struct { type ImagePattern struct {
cv *Canvas cv *Canvas
img *Image img *Image
tf mat tf backendbase.Mat
rep imagePatternRepeat rep imagePatternRepeat
ip backendbase.ImagePattern ip backendbase.ImagePattern
} }
@ -222,8 +222,8 @@ const (
NoRepeat = imagePatternRepeat(backendbase.NoRepeat) NoRepeat = imagePatternRepeat(backendbase.NoRepeat)
) )
func (ip *ImagePattern) data(tf mat) backendbase.ImagePatternData { func (ip *ImagePattern) data(tf backendbase.Mat) backendbase.ImagePatternData {
m := tf.invert().mul(ip.tf.invert()) m := tf.Invert().Mul(ip.tf.Invert())
return backendbase.ImagePatternData{ return backendbase.ImagePatternData{
Image: ip.img.img, Image: ip.img.img,
Transform: [9]float64{ Transform: [9]float64{
@ -239,7 +239,7 @@ func (ip *ImagePattern) data(tf mat) backendbase.ImagePatternData {
// to the given matrix. The matrix is a 3x3 matrix, but three // to the given matrix. The matrix is a 3x3 matrix, but three
// of the values are always identity values // of the values are always identity values
func (ip *ImagePattern) SetTransform(tf [6]float64) { func (ip *ImagePattern) SetTransform(tf [6]float64) {
ip.tf = mat(tf) ip.tf = backendbase.Mat(tf)
} }
// CreatePattern creates a new image pattern with the specified // CreatePattern creates a new image pattern with the specified
@ -249,7 +249,7 @@ func (cv *Canvas) CreatePattern(src interface{}, repeat imagePatternRepeat) *Ima
cv: cv, cv: cv,
img: cv.getImage(src), img: cv.getImage(src),
rep: repeat, rep: repeat,
tf: mat{1, 0, 0, 1, 0, 0}, tf: backendbase.Mat{1, 0, 0, 1, 0, 0},
} }
if ip.img != nil { if ip.img != nil {
ip.ip = cv.b.LoadImagePattern(ip.data(cv.state.transform)) ip.ip = cv.b.LoadImagePattern(ip.data(cv.state.transform))

140
math.go
View file

@ -1,140 +0,0 @@
package canvas
import (
"fmt"
"math"
)
type vec [2]float64
func (v vec) String() string {
return fmt.Sprintf("[%f,%f]", v[0], v[1])
}
func (v vec) add(v2 vec) vec {
return vec{v[0] + v2[0], v[1] + v2[1]}
}
func (v vec) sub(v2 vec) vec {
return vec{v[0] - v2[0], v[1] - v2[1]}
}
func (v vec) mul(v2 vec) vec {
return vec{v[0] * v2[0], v[1] * v2[1]}
}
func (v vec) mulf(f float64) vec {
return vec{v[0] * f, v[1] * f}
}
func (v vec) mulMat(m mat) vec {
return vec{
m[0]*v[0] + m[2]*v[1] + m[4],
m[1]*v[0] + m[3]*v[1] + m[5]}
}
func (v vec) mulMat2(m mat2) vec {
return vec{m[0]*v[0] + m[2]*v[1], m[1]*v[0] + m[3]*v[1]}
}
func (v vec) div(v2 vec) vec {
return vec{v[0] / v2[0], v[1] / v2[1]}
}
func (v vec) divf(f float64) vec {
return vec{v[0] / f, v[1] / f}
}
func (v vec) dot(v2 vec) float64 {
return v[0]*v2[0] + v[1]*v2[1]
}
func (v vec) len() float64 {
return math.Sqrt(v[0]*v[0] + v[1]*v[1])
}
func (v vec) lenSqr() float64 {
return v[0]*v[0] + v[1]*v[1]
}
func (v vec) norm() vec {
return v.mulf(1.0 / v.len())
}
func (v vec) atan2() float64 {
return math.Atan2(v[1], v[0])
}
func (v vec) angle() float64 {
return math.Pi*0.5 - math.Atan2(v[1], v[0])
}
func (v vec) angleTo(v2 vec) float64 {
return math.Acos(v.norm().dot(v2.norm()))
}
type mat [6]float64
func (m *mat) String() string {
return fmt.Sprintf("[%f,%f,0,\n %f,%f,0,\n %f,%f,1,]", m[0], m[2], m[4], m[1], m[3], m[5])
}
var matIdentity = mat{
1, 0,
0, 1,
0, 0}
func matTranslate(v vec) mat {
return mat{
1, 0,
0, 1,
v[0], v[1]}
}
func matScale(v vec) mat {
return mat{
v[0], 0,
0, v[1],
0, 0}
}
func matRotate(radians float64) mat {
s, c := math.Sincos(radians)
return mat{
c, s,
-s, c,
0, 0}
}
func (m mat) mul(m2 mat) mat {
return mat{
m[0]*m2[0] + m[1]*m2[2],
m[0]*m2[1] + m[1]*m2[3],
m[2]*m2[0] + m[3]*m2[2],
m[2]*m2[1] + m[3]*m2[3],
m[4]*m2[0] + m[5]*m2[2] + m2[4],
m[4]*m2[1] + m[5]*m2[3] + m2[5]}
}
func (m mat) invert() mat {
identity := 1.0 / (m[0]*m[3] - m[2]*m[1])
return mat{
m[3] * identity,
-m[1] * identity,
-m[2] * identity,
m[0] * identity,
(m[2]*m[5] - m[3]*m[4]) * identity,
(m[1]*m[4] - m[0]*m[5]) * identity,
}
}
type mat2 [4]float64
func (m mat) mat2() mat2 {
return mat2{m[0], m[1], m[2], m[3]}
}
func (m *mat2) String() string {
return fmt.Sprintf("[%f,%f,\n %f,%f]", m[0], m[2], m[1], m[3])
}

132
path2d.go
View file

@ -2,6 +2,8 @@ package canvas
import ( import (
"math" "math"
"github.com/tfriedel6/canvas/backend/backendbase"
) )
// Path2D is a type that holds a predefined path which can be drawn // Path2D is a type that holds a predefined path which can be drawn
@ -9,13 +11,13 @@ import (
type Path2D struct { type Path2D struct {
cv *Canvas cv *Canvas
p []pathPoint p []pathPoint
move vec move backendbase.Vec
cwSum float64 cwSum float64
} }
type pathPoint struct { type pathPoint struct {
pos vec pos backendbase.Vec
next vec next backendbase.Vec
flags pathPointFlag flags pathPointFlag
} }
@ -40,12 +42,12 @@ func (cv *Canvas) NewPath2D() *Path2D {
// MoveTo (see equivalent function on canvas type) // MoveTo (see equivalent function on canvas type)
func (p *Path2D) MoveTo(x, y float64) { 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) { if len(p.p) > 0 && isSamePoint(p.p[len(p.p)-1].pos, backendbase.Vec{x, y}, 0.1) {
return return
} }
p.p = append(p.p, pathPoint{pos: vec{x, y}, flags: pathMove | pathIsConvex}) p.p = append(p.p, pathPoint{pos: backendbase.Vec{x, y}, flags: pathMove | pathIsConvex})
p.cwSum = 0 p.cwSum = 0
p.move = vec{x, y} p.move = backendbase.Vec{x, y}
} }
// LineTo (see equivalent function on canvas type) // LineTo (see equivalent function on canvas type)
@ -55,7 +57,7 @@ func (p *Path2D) LineTo(x, y float64) {
func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) { func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) {
count := len(p.p) count := len(p.p)
if count > 0 && isSamePoint(p.p[len(p.p)-1].pos, vec{x, y}, 0.1) { if count > 0 && isSamePoint(p.p[len(p.p)-1].pos, backendbase.Vec{x, y}, 0.1) {
return return
} }
if count == 0 { if count == 0 {
@ -63,9 +65,9 @@ func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) {
return return
} }
prev := &p.p[count-1] prev := &p.p[count-1]
prev.next = vec{x, y} prev.next = backendbase.Vec{x, y}
prev.flags |= pathAttach prev.flags |= pathAttach
p.p = append(p.p, pathPoint{pos: vec{x, y}}) p.p = append(p.p, pathPoint{pos: backendbase.Vec{x, y}})
newp := &p.p[count] newp := &p.p[count]
if prev.flags&pathIsConvex > 0 { if prev.flags&pathIsConvex > 0 {
@ -84,9 +86,9 @@ func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) {
prev2 := &p.p[count-2] prev2 := &p.p[count-2]
cw := (prev.flags & pathIsClockwise) > 0 cw := (prev.flags & pathIsClockwise) > 0
ln := prev.pos.sub(prev2.pos) ln := prev.pos.Sub(prev2.pos)
lo := vec{ln[1], -ln[0]} lo := backendbase.Vec{ln[1], -ln[0]}
dot := newp.pos.sub(prev2.pos).dot(lo) dot := newp.pos.Sub(prev2.pos).Dot(lo)
if (cw && dot <= 0) || (!cw && dot >= 0) { if (cw && dot <= 0) || (!cw && dot >= 0) {
newp.flags |= pathIsConvex newp.flags |= pathIsConvex
@ -97,8 +99,8 @@ func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) {
newp.flags |= pathSelfIntersects newp.flags |= pathSelfIntersects
} else if newp.flags&pathIsConvex == 0 && newp.flags&pathSelfIntersects == 0 && checkSelfIntersection && !Performance.IgnoreSelfIntersections { } else if newp.flags&pathIsConvex == 0 && newp.flags&pathSelfIntersects == 0 && checkSelfIntersection && !Performance.IgnoreSelfIntersections {
cuts := false cuts := false
var cutPoint vec var cutPoint backendbase.Vec
b0, b1 := prev.pos, vec{x, y} b0, b1 := prev.pos, backendbase.Vec{x, y}
for i := 1; i < count; i++ { for i := 1; i < count; i++ {
a0, a1 := p.p[i-1].pos, p.p[i].pos a0, a1 := p.p[i-1].pos, p.p[i].pos
var r1, r2 float64 var r1, r2 float64
@ -108,7 +110,7 @@ func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) {
break break
} }
} }
if cuts && !isSamePoint(cutPoint, vec{x, y}, samePointTolerance) { if cuts && !isSamePoint(cutPoint, backendbase.Vec{x, y}, samePointTolerance) {
newp.flags |= pathSelfIntersects newp.flags |= pathSelfIntersects
} }
} }
@ -116,19 +118,19 @@ func (p *Path2D) lineTo(x, y float64, checkSelfIntersection bool) {
// Arc (see equivalent function on canvas type) // Arc (see equivalent function on canvas type)
func (p *Path2D) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) { func (p *Path2D) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) {
p.arc(x, y, radius, startAngle, endAngle, anticlockwise, matIdentity, true) p.arc(x, y, radius, startAngle, endAngle, anticlockwise, backendbase.MatIdentity, true)
} }
func (p *Path2D) arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool, m mat, ident bool) { func (p *Path2D) arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool, m backendbase.Mat, ident bool) {
checkSelfIntersection := len(p.p) > 0 checkSelfIntersection := len(p.p) > 0
lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0 lastWasMove := len(p.p) == 0 || p.p[len(p.p)-1].flags&pathMove != 0
if endAngle == startAngle { if endAngle == startAngle {
s, c := math.Sincos(endAngle) s, c := math.Sincos(endAngle)
pt := vec{x + radius*c, y + radius*s} pt := backendbase.Vec{x + radius*c, y + radius*s}
if !ident { if !ident {
pt = pt.mulMat(m) pt = pt.MulMat(m)
} }
p.lineTo(pt[0], pt[1], checkSelfIntersection) p.lineTo(pt[0], pt[1], checkSelfIntersection)
@ -163,26 +165,26 @@ func (p *Path2D) arc(x, y, radius, startAngle, endAngle float64, anticlockwise b
if !anticlockwise { if !anticlockwise {
for a := startAngle; a < endAngle; a += step { for a := startAngle; a < endAngle; a += step {
s, c := math.Sincos(a) s, c := math.Sincos(a)
pt := vec{x + radius*c, y + radius*s} pt := backendbase.Vec{x + radius*c, y + radius*s}
if !ident { if !ident {
pt = pt.mulMat(m) pt = pt.MulMat(m)
} }
p.lineTo(pt[0], pt[1], checkSelfIntersection) p.lineTo(pt[0], pt[1], checkSelfIntersection)
} }
} else { } else {
for a := startAngle; a > endAngle; a -= step { for a := startAngle; a > endAngle; a -= step {
s, c := math.Sincos(a) s, c := math.Sincos(a)
pt := vec{x + radius*c, y + radius*s} pt := backendbase.Vec{x + radius*c, y + radius*s}
if !ident { if !ident {
pt = pt.mulMat(m) pt = pt.MulMat(m)
} }
p.lineTo(pt[0], pt[1], checkSelfIntersection) p.lineTo(pt[0], pt[1], checkSelfIntersection)
} }
} }
s, c := math.Sincos(endAngle) s, c := math.Sincos(endAngle)
pt := vec{x + radius*c, y + radius*s} pt := backendbase.Vec{x + radius*c, y + radius*s}
if !ident { if !ident {
pt = pt.mulMat(m) pt = pt.MulMat(m)
} }
p.lineTo(pt[0], pt[1], checkSelfIntersection) p.lineTo(pt[0], pt[1], checkSelfIntersection)
@ -193,34 +195,34 @@ func (p *Path2D) arc(x, y, radius, startAngle, endAngle float64, anticlockwise b
// ArcTo (see equivalent function on canvas type) // ArcTo (see equivalent function on canvas type)
func (p *Path2D) ArcTo(x1, y1, x2, y2, radius float64) { func (p *Path2D) ArcTo(x1, y1, x2, y2, radius float64) {
p.arcTo(x1, y1, x2, y2, radius, matIdentity, true) p.arcTo(x1, y1, x2, y2, radius, backendbase.MatIdentity, true)
} }
func (p *Path2D) arcTo(x1, y1, x2, y2, radius float64, m mat, ident bool) { func (p *Path2D) arcTo(x1, y1, x2, y2, radius float64, m backendbase.Mat, ident bool) {
if len(p.p) == 0 { if len(p.p) == 0 {
return return
} }
p0, p1, p2 := p.p[len(p.p)-1].pos, vec{x1, y1}, vec{x2, y2} p0, p1, p2 := p.p[len(p.p)-1].pos, backendbase.Vec{x1, y1}, backendbase.Vec{x2, y2}
if !ident { if !ident {
p0 = p0.mulMat(m.invert()) p0 = p0.MulMat(m.Invert())
} }
v0, v1 := p0.sub(p1).norm(), p2.sub(p1).norm() v0, v1 := p0.Sub(p1).Norm(), p2.Sub(p1).Norm()
angle := math.Acos(v0.dot(v1)) angle := math.Acos(v0.Dot(v1))
// should be in the range [0-pi]. if parallel, use a straight line // should be in the range [0-pi]. if parallel, use a straight line
if angle <= 0 || angle >= math.Pi { if angle <= 0 || angle >= math.Pi {
p.LineTo(x2, y2) p.LineTo(x2, y2)
return return
} }
// cv0 and cv1 are vectors that point to the center of the circle // cv0 and cv1 are vectors that point to the center of the circle
cv0 := vec{-v0[1], v0[0]} cv0 := backendbase.Vec{-v0[1], v0[0]}
cv1 := vec{v1[1], -v1[0]} cv1 := backendbase.Vec{v1[1], -v1[0]}
x := cv1.sub(cv0).div(v0.sub(v1))[0] * radius x := cv1.Sub(cv0).Div(v0.Sub(v1))[0] * radius
if x < 0 { if x < 0 {
cv0 = cv0.mulf(-1) cv0 = cv0.Mulf(-1)
cv1 = cv1.mulf(-1) cv1 = cv1.Mulf(-1)
} }
center := p1.add(v0.mulf(math.Abs(x))).add(cv0.mulf(radius)) center := p1.Add(v0.Mulf(math.Abs(x))).Add(cv0.Mulf(radius))
a0, a1 := cv0.mulf(-1).atan2(), cv1.mulf(-1).atan2() a0, a1 := cv0.Mulf(-1).Atan2(), cv1.Mulf(-1).Atan2()
if x > 0 { if x > 0 {
if a1-a0 > 0 { if a1-a0 > 0 {
a0 += math.Pi * 2 a0 += math.Pi * 2
@ -239,17 +241,17 @@ func (p *Path2D) QuadraticCurveTo(x1, y1, x2, y2 float64) {
return return
} }
p0 := p.p[len(p.p)-1].pos p0 := p.p[len(p.p)-1].pos
p1 := vec{x1, y1} p1 := backendbase.Vec{x1, y1}
p2 := vec{x2, y2} p2 := backendbase.Vec{x2, y2}
v0 := p1.sub(p0) v0 := p1.Sub(p0)
v1 := p2.sub(p1) v1 := p2.Sub(p1)
const step = 0.01 const step = 0.01
for r := 0.0; r < 1; r += step { for r := 0.0; r < 1; r += step {
i0 := v0.mulf(r).add(p0) i0 := v0.Mulf(r).Add(p0)
i1 := v1.mulf(r).add(p1) i1 := v1.Mulf(r).Add(p1)
pt := i1.sub(i0).mulf(r).add(i0) pt := i1.Sub(i0).Mulf(r).Add(i0)
p.LineTo(pt[0], pt[1]) p.LineTo(pt[0], pt[1])
} }
p.LineTo(x2, y2) p.LineTo(x2, y2)
@ -261,24 +263,24 @@ func (p *Path2D) BezierCurveTo(x1, y1, x2, y2, x3, y3 float64) {
return return
} }
p0 := p.p[len(p.p)-1].pos p0 := p.p[len(p.p)-1].pos
p1 := vec{x1, y1} p1 := backendbase.Vec{x1, y1}
p2 := vec{x2, y2} p2 := backendbase.Vec{x2, y2}
p3 := vec{x3, y3} p3 := backendbase.Vec{x3, y3}
v0 := p1.sub(p0) v0 := p1.Sub(p0)
v1 := p2.sub(p1) v1 := p2.Sub(p1)
v2 := p3.sub(p2) v2 := p3.Sub(p2)
const step = 0.01 const step = 0.01
for r := 0.0; r < 1; r += step { for r := 0.0; r < 1; r += step {
i0 := v0.mulf(r).add(p0) i0 := v0.Mulf(r).Add(p0)
i1 := v1.mulf(r).add(p1) i1 := v1.Mulf(r).Add(p1)
i2 := v2.mulf(r).add(p2) i2 := v2.Mulf(r).Add(p2)
iv0 := i1.sub(i0) iv0 := i1.Sub(i0)
iv1 := i2.sub(i1) iv1 := i2.Sub(i1)
j0 := iv0.mulf(r).add(i0) j0 := iv0.Mulf(r).Add(i0)
j1 := iv1.mulf(r).add(i1) j1 := iv1.Mulf(r).Add(i1)
pt := j1.sub(j0).mulf(r).add(j0) pt := j1.Sub(j0).Mulf(r).Add(j0)
p.LineTo(pt[0], pt[1]) p.LineTo(pt[0], pt[1])
} }
p.LineTo(x3, y3) p.LineTo(x3, y3)
@ -434,7 +436,7 @@ func (p *Path2D) IsPointInPath(x, y float64, rule pathRule) bool {
num := 0 num := 0
prev := sp[len(sp)-1].pos prev := sp[len(sp)-1].pos
for _, pt := range sp { for _, pt := range sp {
r, dir := pointIsRightOfLine(prev, pt.pos, vec{x, y}) r, dir := pointIsRightOfLine(prev, pt.pos, backendbase.Vec{x, y})
prev = pt.pos prev = pt.pos
if !r { if !r {
continue continue
@ -464,14 +466,14 @@ func (p *Path2D) IsPointInStroke(x, y float64) bool {
} }
var triBuf [500][2]float64 var triBuf [500][2]float64
tris := p.cv.strokeTris(p, mat{}, false, triBuf[:0]) tris := p.cv.strokeTris(p, backendbase.Mat{}, false, triBuf[:0])
pt := vec{x, y} pt := backendbase.Vec{x, y}
for i := 0; i < len(tris); i += 3 { for i := 0; i < len(tris); i += 3 {
a := vec{tris[i][0], tris[i][1]} a := backendbase.Vec{tris[i][0], tris[i][1]}
b := vec{tris[i+1][0], tris[i+1][1]} b := backendbase.Vec{tris[i+1][0], tris[i+1][1]}
c := vec{tris[i+2][0], tris[i+2][1]} c := backendbase.Vec{tris[i+2][0], tris[i+2][1]}
if triangleContainsPoint(a, b, c, pt) { if triangleContainsPoint(a, b, c, pt) {
return true return true
} }

184
paths.go
View file

@ -2,6 +2,8 @@ package canvas
import ( import (
"math" "math"
"github.com/tfriedel6/canvas/backend/backendbase"
) )
// BeginPath clears the current path and starts a new one // BeginPath clears the current path and starts a new one
@ -12,19 +14,19 @@ func (cv *Canvas) BeginPath() {
cv.path.p = cv.path.p[:0] cv.path.p = cv.path.p[:0]
} }
func isSamePoint(a, b vec, maxDist float64) bool { func isSamePoint(a, b backendbase.Vec, maxDist float64) bool {
return math.Abs(b[0]-a[0]) <= maxDist && math.Abs(b[1]-a[1]) <= maxDist return math.Abs(b[0]-a[0]) <= maxDist && math.Abs(b[1]-a[1]) <= maxDist
} }
// MoveTo adds a gap and moves the end of the path to x/y // MoveTo adds a gap and moves the end of the path to x/y
func (cv *Canvas) MoveTo(x, y float64) { func (cv *Canvas) MoveTo(x, y float64) {
tf := cv.tf(vec{x, y}) tf := cv.tf(backendbase.Vec{x, y})
cv.path.MoveTo(tf[0], tf[1]) cv.path.MoveTo(tf[0], tf[1])
} }
// LineTo adds a line to the end of the path // LineTo adds a line to the end of the path
func (cv *Canvas) LineTo(x, y float64) { func (cv *Canvas) LineTo(x, y float64) {
tf := cv.tf(vec{x, y}) tf := cv.tf(backendbase.Vec{x, y})
cv.path.LineTo(tf[0], tf[1]) cv.path.LineTo(tf[0], tf[1])
} }
@ -33,7 +35,7 @@ func (cv *Canvas) LineTo(x, y float64) {
// means that the line is added anticlockwise // means that the line is added anticlockwise
func (cv *Canvas) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) { func (cv *Canvas) Arc(x, y, radius, startAngle, endAngle float64, anticlockwise bool) {
ax, ay := math.Sincos(startAngle) ax, ay := math.Sincos(startAngle)
startAngle2 := vec{ay, ax}.mulMat2(cv.state.transform.mat2()).atan2() startAngle2 := backendbase.Vec{ay, ax}.MulMat2(cv.state.transform.Mat2()).Atan2()
endAngle2 := startAngle2 + (endAngle - startAngle) endAngle2 := startAngle2 + (endAngle - startAngle)
cv.path.arc(x, y, radius, startAngle2, endAngle2, anticlockwise, cv.state.transform, false) cv.path.arc(x, y, radius, startAngle2, endAngle2, anticlockwise, cv.state.transform, false)
} }
@ -49,17 +51,17 @@ func (cv *Canvas) ArcTo(x1, y1, x2, y2, radius float64) {
// QuadraticCurveTo adds a quadratic curve to the path. It uses the current end // 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 // 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) { func (cv *Canvas) QuadraticCurveTo(x1, y1, x2, y2 float64) {
tf1 := cv.tf(vec{x1, y1}) tf1 := cv.tf(backendbase.Vec{x1, y1})
tf2 := cv.tf(vec{x2, y2}) tf2 := cv.tf(backendbase.Vec{x2, y2})
cv.path.QuadraticCurveTo(tf1[0], tf1[1], tf2[0], tf2[1]) 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 // 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 // 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) { func (cv *Canvas) BezierCurveTo(x1, y1, x2, y2, x3, y3 float64) {
tf1 := cv.tf(vec{x1, y1}) tf1 := cv.tf(backendbase.Vec{x1, y1})
tf2 := cv.tf(vec{x2, y2}) tf2 := cv.tf(backendbase.Vec{x2, y2})
tf3 := cv.tf(vec{x3, y3}) tf3 := cv.tf(backendbase.Vec{x3, y3})
cv.path.BezierCurveTo(tf1[0], tf1[1], tf2[0], tf2[1], tf3[0], tf3[1]) cv.path.BezierCurveTo(tf1[0], tf1[1], tf2[0], tf2[1], tf3[0], tf3[1])
} }
@ -69,9 +71,9 @@ func (cv *Canvas) BezierCurveTo(x1, y1, x2, y2, x3, y3 float64) {
// are angles in radians, and anticlockwise means that the line is added // are angles in radians, and anticlockwise means that the line is added
// anticlockwise // anticlockwise
func (cv *Canvas) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, anticlockwise bool) { func (cv *Canvas) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, anticlockwise bool) {
tf := cv.tf(vec{x, y}) tf := cv.tf(backendbase.Vec{x, y})
ax, ay := math.Sincos(startAngle) ax, ay := math.Sincos(startAngle)
startAngle2 := vec{ay, ax}.mulMat2(cv.state.transform.mat2()).atan2() startAngle2 := backendbase.Vec{ay, ax}.MulMat2(cv.state.transform.Mat2()).Atan2()
endAngle2 := startAngle2 + (endAngle - startAngle) endAngle2 := startAngle2 + (endAngle - startAngle)
cv.path.Ellipse(tf[0], tf[1], radiusX, radiusY, rotation, startAngle2, endAngle2, anticlockwise) cv.path.Ellipse(tf[0], tf[1], radiusX, radiusY, rotation, startAngle2, endAngle2, anticlockwise)
} }
@ -84,7 +86,7 @@ func (cv *Canvas) ClosePath() {
// Stroke uses the current StrokeStyle to draw the current path // Stroke uses the current StrokeStyle to draw the current path
func (cv *Canvas) Stroke() { func (cv *Canvas) Stroke() {
cv.strokePath(&cv.path, cv.state.transform.invert(), true) cv.strokePath(&cv.path, cv.state.transform.Invert(), true)
} }
// StrokePath uses the current StrokeStyle to draw the given path // StrokePath uses the current StrokeStyle to draw the given path
@ -94,10 +96,10 @@ func (cv *Canvas) StrokePath(path *Path2D) {
p: make([]pathPoint, len(path.p)), p: make([]pathPoint, len(path.p)),
} }
copy(path2.p, path.p) copy(path2.p, path.p)
cv.strokePath(&path2, mat{}, false) cv.strokePath(&path2, backendbase.Mat{}, false)
} }
func (cv *Canvas) strokePath(path *Path2D, inv mat, doInv bool) { func (cv *Canvas) strokePath(path *Path2D, inv backendbase.Mat, doInv bool) {
if len(path.p) == 0 { if len(path.p) == 0 {
return return
} }
@ -111,7 +113,7 @@ func (cv *Canvas) strokePath(path *Path2D, inv mat, doInv bool) {
cv.b.Fill(&stl, tris, true) cv.b.Fill(&stl, tris, true)
} }
func (cv *Canvas) strokeTris(path *Path2D, inv mat, doInv bool, target [][2]float64) [][2]float64 { func (cv *Canvas) strokeTris(path *Path2D, inv backendbase.Mat, doInv bool, target [][2]float64) [][2]float64 {
if len(path.p) == 0 { if len(path.p) == 0 {
return target return target
} }
@ -126,8 +128,8 @@ func (cv *Canvas) strokeTris(path *Path2D, inv mat, doInv bool, target [][2]floa
} }
for i, pt := range path.p { for i, pt := range path.p {
pt.pos = pt.pos.mulMat(inv) pt.pos = pt.pos.MulMat(inv)
pt.next = pt.next.mulMat(inv) pt.next = pt.next.MulMat(inv)
pcopy.p[i] = pt pcopy.p[i] = pt
} }
path = &pcopy path = &pcopy
@ -136,7 +138,7 @@ func (cv *Canvas) strokeTris(path *Path2D, inv mat, doInv bool, target [][2]floa
dashedPath := cv.applyLineDash(path.p) dashedPath := cv.applyLineDash(path.p)
start := true start := true
var p0 vec var p0 backendbase.Vec
for _, p := range dashedPath { for _, p := range dashedPath {
if p.flags&pathMove != 0 { if p.flags&pathMove != 0 {
p0 = p.pos p0 = p.pos
@ -145,22 +147,22 @@ func (cv *Canvas) strokeTris(path *Path2D, inv mat, doInv bool, target [][2]floa
} }
p1 := p.pos p1 := p.pos
v0 := p1.sub(p0).norm() v0 := p1.Sub(p0).Norm()
v1 := vec{v0[1], -v0[0]}.mulf(cv.state.lineWidth * 0.5) v1 := backendbase.Vec{v0[1], -v0[0]}.Mulf(cv.state.lineWidth * 0.5)
v0 = v0.mulf(cv.state.lineWidth * 0.5) v0 = v0.Mulf(cv.state.lineWidth * 0.5)
lp0 := p0.add(v1) lp0 := p0.Add(v1)
lp1 := p1.add(v1) lp1 := p1.Add(v1)
lp2 := p0.sub(v1) lp2 := p0.Sub(v1)
lp3 := p1.sub(v1) lp3 := p1.Sub(v1)
if start { if start {
switch cv.state.lineCap { switch cv.state.lineCap {
case Butt: case Butt:
// no need to do anything // no need to do anything
case Square: case Square:
lp0 = lp0.sub(v0) lp0 = lp0.Sub(v0)
lp2 = lp2.sub(v0) lp2 = lp2.Sub(v0)
case Round: case Round:
target = cv.addCircleTris(p0, cv.state.lineWidth*0.5, target) target = cv.addCircleTris(p0, cv.state.lineWidth*0.5, target)
} }
@ -171,8 +173,8 @@ func (cv *Canvas) strokeTris(path *Path2D, inv mat, doInv bool, target [][2]floa
case Butt: case Butt:
// no need to do anything // no need to do anything
case Square: case Square:
lp1 = lp1.add(v0) lp1 = lp1.Add(v0)
lp3 = lp3.add(v0) lp3 = lp3.Add(v0)
case Round: case Round:
target = cv.addCircleTris(p1, cv.state.lineWidth*0.5, target) target = cv.addCircleTris(p1, cv.state.lineWidth*0.5, target)
} }
@ -209,8 +211,8 @@ func (cv *Canvas) applyLineDash(path []pathPoint) []pathPoint {
continue continue
} }
v := pp.pos.sub(lp.pos) v := pp.pos.Sub(lp.pos)
vl := v.len() vl := v.Len()
prev := ldo prev := ldo
for vl > 0 { for vl > 0 {
draw := ldp%2 == 0 draw := ldp%2 == 0
@ -220,7 +222,7 @@ func (cv *Canvas) applyLineDash(path []pathPoint) []pathPoint {
ldo = 0 ldo = 0
dl := cv.state.lineDash[ldp] - prev dl := cv.state.lineDash[ldp] - prev
dist := dl / vl dist := dl / vl
newp.pos = lp.pos.add(v.mulf(dist)) newp.pos = lp.pos.Add(v.Mulf(dist))
vl -= dl vl -= dl
ldp++ ldp++
ldp %= len(cv.state.lineDash) ldp %= len(cv.state.lineDash)
@ -239,7 +241,7 @@ func (cv *Canvas) applyLineDash(path []pathPoint) []pathPoint {
} }
lp = newp lp = newp
v = pp.pos.sub(lp.pos) v = pp.pos.Sub(lp.pos)
} }
lp = pp lp = pp
} }
@ -247,50 +249,50 @@ func (cv *Canvas) applyLineDash(path []pathPoint) []pathPoint {
return path2 return path2
} }
func (cv *Canvas) lineJoint(p0, p1, p2, l0p0, l0p1, l0p2, l0p3 vec, tris [][2]float64) [][2]float64 { func (cv *Canvas) lineJoint(p0, p1, p2, l0p0, l0p1, l0p2, l0p3 backendbase.Vec, tris [][2]float64) [][2]float64 {
v2 := p1.sub(p2).norm() v2 := p1.Sub(p2).Norm()
v3 := vec{v2[1], -v2[0]}.mulf(cv.state.lineWidth * 0.5) v3 := backendbase.Vec{v2[1], -v2[0]}.Mulf(cv.state.lineWidth * 0.5)
switch cv.state.lineJoin { switch cv.state.lineJoin {
case Miter: case Miter:
l1p0 := p2.sub(v3) l1p0 := p2.Sub(v3)
l1p1 := p1.sub(v3) l1p1 := p1.Sub(v3)
l1p2 := p2.add(v3) l1p2 := p2.Add(v3)
l1p3 := p1.add(v3) l1p3 := p1.Add(v3)
var ip0, ip1 vec var ip0, ip1 backendbase.Vec
if l0p1.sub(l1p1).lenSqr() < 0.000000001 { if l0p1.Sub(l1p1).LenSqr() < 0.000000001 {
ip0 = l0p1.sub(l1p1).mulf(0.5).add(l1p1) ip0 = l0p1.Sub(l1p1).Mulf(0.5).Add(l1p1)
} else { } else {
var q float64 var q float64
ip0, _, q = lineIntersection(l0p0, l0p1, l1p1, l1p0) ip0, _, q = lineIntersection(l0p0, l0p1, l1p1, l1p0)
if q >= 1 { if q >= 1 {
ip0 = l0p1.add(l1p1).mulf(0.5) ip0 = l0p1.Add(l1p1).Mulf(0.5)
} }
} }
if dist := ip0.sub(l0p1).lenSqr(); dist > cv.state.miterLimitSqr { if dist := ip0.Sub(l0p1).LenSqr(); dist > cv.state.miterLimitSqr {
l1p1 := p1.sub(v3) l1p1 := p1.Sub(v3)
l1p3 := p1.add(v3) l1p3 := p1.Add(v3)
tris = append(tris, cv.tf(p1), cv.tf(l0p1), cv.tf(l1p1), tris = append(tris, cv.tf(p1), cv.tf(l0p1), cv.tf(l1p1),
cv.tf(p1), cv.tf(l1p3), cv.tf(l0p3)) cv.tf(p1), cv.tf(l1p3), cv.tf(l0p3))
return tris return tris
} }
if l0p3.sub(l1p3).lenSqr() < 0.000000001 { if l0p3.Sub(l1p3).LenSqr() < 0.000000001 {
ip1 = l0p3.sub(l1p3).mulf(0.5).add(l1p3) ip1 = l0p3.Sub(l1p3).Mulf(0.5).Add(l1p3)
} else { } else {
var q float64 var q float64
ip1, _, q = lineIntersection(l0p2, l0p3, l1p3, l1p2) ip1, _, q = lineIntersection(l0p2, l0p3, l1p3, l1p2)
if q >= 1 { if q >= 1 {
ip1 = l0p3.add(l1p3).mulf(0.5) ip1 = l0p3.Add(l1p3).Mulf(0.5)
} }
} }
if dist := ip1.sub(l1p1).lenSqr(); dist > cv.state.miterLimitSqr { if dist := ip1.Sub(l1p1).LenSqr(); dist > cv.state.miterLimitSqr {
l1p1 := p1.sub(v3) l1p1 := p1.Sub(v3)
l1p3 := p1.add(v3) l1p3 := p1.Add(v3)
tris = append(tris, cv.tf(p1), cv.tf(l0p1), cv.tf(l1p1), tris = append(tris, cv.tf(p1), cv.tf(l0p1), cv.tf(l1p1),
cv.tf(p1), cv.tf(l1p3), cv.tf(l0p3)) cv.tf(p1), cv.tf(l1p3), cv.tf(l0p3))
@ -302,8 +304,8 @@ func (cv *Canvas) lineJoint(p0, p1, p2, l0p0, l0p1, l0p2, l0p3 vec, tris [][2]fl
cv.tf(p1), cv.tf(l1p3), cv.tf(ip1), cv.tf(p1), cv.tf(l1p3), cv.tf(ip1),
cv.tf(p1), cv.tf(ip1), cv.tf(l0p3)) cv.tf(p1), cv.tf(ip1), cv.tf(l0p3))
case Bevel: case Bevel:
l1p1 := p1.sub(v3) l1p1 := p1.Sub(v3)
l1p3 := p1.add(v3) l1p3 := p1.Add(v3)
tris = append(tris, cv.tf(p1), cv.tf(l0p1), cv.tf(l1p1), tris = append(tris, cv.tf(p1), cv.tf(l0p1), cv.tf(l1p1),
cv.tf(p1), cv.tf(l1p3), cv.tf(l0p3)) cv.tf(p1), cv.tf(l1p3), cv.tf(l0p3))
@ -314,7 +316,7 @@ func (cv *Canvas) lineJoint(p0, p1, p2, l0p0, l0p1, l0p2, l0p3 vec, tris [][2]fl
return tris return tris
} }
func (cv *Canvas) addCircleTris(center vec, radius float64, tris [][2]float64) [][2]float64 { func (cv *Canvas) addCircleTris(center backendbase.Vec, radius float64, tris [][2]float64) [][2]float64 {
step := 6 / radius step := 6 / radius
if step > 0.8 { if step > 0.8 {
step = 0.8 step = 0.8
@ -322,26 +324,26 @@ func (cv *Canvas) addCircleTris(center vec, radius float64, tris [][2]float64) [
step = 0.05 step = 0.05
} }
centertf := cv.tf(center) centertf := cv.tf(center)
p0 := cv.tf(vec{center[0], center[1] + radius}) p0 := cv.tf(backendbase.Vec{center[0], center[1] + radius})
for angle := step; angle <= math.Pi*2+step; angle += step { for angle := step; angle <= math.Pi*2+step; angle += step {
s, c := math.Sincos(angle) s, c := math.Sincos(angle)
p1 := cv.tf(vec{center[0] + s*radius, center[1] + c*radius}) p1 := cv.tf(backendbase.Vec{center[0] + s*radius, center[1] + c*radius})
tris = append(tris, centertf, p0, p1) tris = append(tris, centertf, p0, p1)
p0 = p1 p0 = p1
} }
return tris return tris
} }
func lineIntersection(a0, a1, b0, b1 vec) (vec, float64, float64) { func lineIntersection(a0, a1, b0, b1 backendbase.Vec) (backendbase.Vec, float64, float64) {
va := a1.sub(a0) va := a1.Sub(a0)
vb := b1.sub(b0) vb := b1.Sub(b0)
if (va[0] == 0 && vb[0] == 0) || (va[1] == 0 && vb[1] == 0) || (va[0] == 0 && va[1] == 0) || (vb[0] == 0 && vb[1] == 0) { if (va[0] == 0 && vb[0] == 0) || (va[1] == 0 && vb[1] == 0) || (va[0] == 0 && va[1] == 0) || (vb[0] == 0 && vb[1] == 0) {
return vec{}, float64(math.Inf(1)), float64(math.Inf(1)) return backendbase.Vec{}, float64(math.Inf(1)), float64(math.Inf(1))
} }
d := va[1]*vb[0] - va[0]*vb[1] d := va[1]*vb[0] - va[0]*vb[1]
if d == 0 { if d == 0 {
return vec{}, float64(math.Inf(1)), float64(math.Inf(1)) return backendbase.Vec{}, float64(math.Inf(1)), float64(math.Inf(1))
} }
p := (vb[1]*(a0[0]-b0[0]) - a0[1]*vb[0] + b0[1]*vb[0]) / d p := (vb[1]*(a0[0]-b0[0]) - a0[1]*vb[0] + b0[1]*vb[0]) / d
var q float64 var q float64
@ -351,21 +353,21 @@ func lineIntersection(a0, a1, b0, b1 vec) (vec, float64, float64) {
q = (a0[0] + p*va[0] - b0[0]) / vb[0] q = (a0[0] + p*va[0] - b0[0]) / vb[0]
} }
return a0.add(va.mulf(p)), p, q return a0.Add(va.Mulf(p)), p, q
} }
func linePointDistSqr(a, b, p vec) float64 { func linePointDistSqr(a, b, p backendbase.Vec) float64 {
v := b.sub(a) v := b.Sub(a)
vl := v.len() vl := v.Len()
vn := v.divf(vl) vn := v.Divf(vl)
d := p.sub(a).dot(vn) d := p.Sub(a).Dot(vn)
c := a.add(vn.mulf(d)) c := a.Add(vn.Mulf(d))
return p.sub(c).lenSqr() return p.Sub(c).LenSqr()
} }
// Fill fills the current path with the current FillStyle // Fill fills the current path with the current FillStyle
func (cv *Canvas) Fill() { func (cv *Canvas) Fill() {
cv.fillPath(&cv.path, matIdentity) cv.fillPath(&cv.path, backendbase.MatIdentity)
} }
// FillPath fills the given path with the current FillStyle // FillPath fills the given path with the current FillStyle
@ -374,7 +376,7 @@ func (cv *Canvas) FillPath(path *Path2D) {
} }
// FillPath fills the given path with the current FillStyle // FillPath fills the given path with the current FillStyle
func (cv *Canvas) fillPath(path *Path2D, tf mat) { func (cv *Canvas) fillPath(path *Path2D, tf backendbase.Mat) {
if len(path.p) < 3 { if len(path.p) < 3 {
return return
} }
@ -396,16 +398,16 @@ func (cv *Canvas) fillPath(path *Path2D, tf mat) {
cv.b.Fill(&stl, tris, false) cv.b.Fill(&stl, tris, false)
} }
func appendSubPathTriangles(tris [][2]float64, mat mat, path []pathPoint) [][2]float64 { func appendSubPathTriangles(tris [][2]float64, mat backendbase.Mat, path []pathPoint) [][2]float64 {
last := path[len(path)-1] last := path[len(path)-1]
if last.flags&pathIsConvex != 0 { if last.flags&pathIsConvex != 0 {
p0, p1 := path[0].pos.mulMat(mat), path[1].pos.mulMat(mat) p0, p1 := path[0].pos.MulMat(mat), path[1].pos.MulMat(mat)
lastIdx := len(path) lastIdx := len(path)
if path[0].pos == path[lastIdx-1].pos { if path[0].pos == path[lastIdx-1].pos {
lastIdx-- lastIdx--
} }
for i := 2; i < lastIdx; i++ { for i := 2; i < lastIdx; i++ {
p2 := path[i].pos.mulMat(mat) p2 := path[i].pos.MulMat(mat)
tris = append(tris, p0, p1, p2) tris = append(tris, p0, p1, p2)
p1 = p2 p1 = p2
} }
@ -423,10 +425,10 @@ func appendSubPathTriangles(tris [][2]float64, mat mat, path []pathPoint) [][2]f
// Clip uses the current path to clip any further drawing. Use Save/Restore to // Clip uses the current path to clip any further drawing. Use Save/Restore to
// remove the clipping again // remove the clipping again
func (cv *Canvas) Clip() { func (cv *Canvas) Clip() {
cv.clip(&cv.path, matIdentity) cv.clip(&cv.path, backendbase.MatIdentity)
} }
func (cv *Canvas) clip(path *Path2D, tf mat) { func (cv *Canvas) clip(path *Path2D, tf backendbase.Mat) {
if len(path.p) < 3 { if len(path.p) < 3 {
return return
} }
@ -476,10 +478,10 @@ func (cv *Canvas) Rect(x, y, w, h float64) {
// StrokeRect draws a rectangle using the current stroke style // StrokeRect draws a rectangle using the current stroke style
func (cv *Canvas) StrokeRect(x, y, w, h float64) { func (cv *Canvas) StrokeRect(x, y, w, h float64) {
v0 := vec{x, y} v0 := backendbase.Vec{x, y}
v1 := vec{x + w, y} v1 := backendbase.Vec{x + w, y}
v2 := vec{x + w, y + h} v2 := backendbase.Vec{x + w, y + h}
v3 := vec{x, y + h} v3 := backendbase.Vec{x, y + h}
var p [5]pathPoint var p [5]pathPoint
p[0] = pathPoint{pos: v0, flags: pathMove | pathAttach, next: v1} p[0] = pathPoint{pos: v0, flags: pathMove | pathAttach, next: v1}
p[1] = pathPoint{pos: v1, next: v2, flags: pathAttach} p[1] = pathPoint{pos: v1, next: v2, flags: pathAttach}
@ -487,15 +489,15 @@ func (cv *Canvas) StrokeRect(x, y, w, h float64) {
p[3] = pathPoint{pos: v3, next: v0, flags: pathAttach} p[3] = pathPoint{pos: v3, next: v0, flags: pathAttach}
p[4] = pathPoint{pos: v0, next: v1, flags: pathAttach} p[4] = pathPoint{pos: v0, next: v1, flags: pathAttach}
path := Path2D{p: p[:]} path := Path2D{p: p[:]}
cv.strokePath(&path, mat{}, false) cv.strokePath(&path, backendbase.Mat{}, false)
} }
// FillRect fills a rectangle with the active fill style // FillRect fills a rectangle with the active fill style
func (cv *Canvas) FillRect(x, y, w, h float64) { func (cv *Canvas) FillRect(x, y, w, h float64) {
p0 := cv.tf(vec{x, y}) p0 := cv.tf(backendbase.Vec{x, y})
p1 := cv.tf(vec{x, y + h}) p1 := cv.tf(backendbase.Vec{x, y + h})
p2 := cv.tf(vec{x + w, y + h}) p2 := cv.tf(backendbase.Vec{x + w, y + h})
p3 := cv.tf(vec{x + w, y}) p3 := cv.tf(backendbase.Vec{x + w, y})
data := [4][2]float64{{p0[0], p0[1]}, {p1[0], p1[1]}, {p2[0], p2[1]}, {p3[0], p3[1]}} data := [4][2]float64{{p0[0], p0[1]}, {p1[0], p1[1]}, {p2[0], p2[1]}, {p3[0], p3[1]}}
@ -507,10 +509,10 @@ func (cv *Canvas) FillRect(x, y, w, h float64) {
// ClearRect sets the color of the rectangle to transparent black // ClearRect sets the color of the rectangle to transparent black
func (cv *Canvas) ClearRect(x, y, w, h float64) { func (cv *Canvas) ClearRect(x, y, w, h float64) {
p0 := cv.tf(vec{x, y}) p0 := cv.tf(backendbase.Vec{x, y})
p1 := cv.tf(vec{x, y + h}) p1 := cv.tf(backendbase.Vec{x, y + h})
p2 := cv.tf(vec{x + w, y + h}) p2 := cv.tf(backendbase.Vec{x + w, y + h})
p3 := cv.tf(vec{x + w, y}) p3 := cv.tf(backendbase.Vec{x + w, y})
data := [4][2]float64{{p0[0], p0[1]}, {p1[0], p1[1]}, {p2[0], p2[1]}, {p3[0], p3[1]}} data := [4][2]float64{{p0[0], p0[1]}, {p1[0], p1[1]}, {p2[0], p2[1]}, {p3[0], p3[1]}}
cv.b.Clear(data) cv.b.Clear(data)

25
text.go
View file

@ -12,6 +12,7 @@ import (
"github.com/golang/freetype" "github.com/golang/freetype"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/tfriedel6/canvas/backend/backendbase"
"golang.org/x/image/font" "golang.org/x/image/font"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
) )
@ -117,8 +118,8 @@ func (cv *Canvas) FillText(str string, x, y float64) {
return return
} }
scaleX := vec{cv.state.transform[0], cv.state.transform[1]}.len() scaleX := backendbase.Vec{cv.state.transform[0], cv.state.transform[1]}.Len()
scaleY := vec{cv.state.transform[2], cv.state.transform[3]}.len() scaleY := backendbase.Vec{cv.state.transform[2], cv.state.transform[3]}.Len()
scale := (scaleX + scaleY) * 0.5 scale := (scaleX + scaleY) * 0.5
fontSize := fixed.Int26_6(math.Round(float64(cv.state.fontSize) * scale)) fontSize := fixed.Int26_6(math.Round(float64(cv.state.fontSize) * scale))
@ -213,10 +214,10 @@ func (cv *Canvas) FillText(str string, x, y float64) {
w, h := cv.b.Size() w, h := cv.b.Size()
fw, fh := float64(w), float64(h) fw, fh := float64(w), float64(h)
p0 := cv.tf(vec{float64(bounds.Min.X)/scale + curX, float64(bounds.Min.Y)/scale + y}) p0 := cv.tf(backendbase.Vec{float64(bounds.Min.X)/scale + curX, float64(bounds.Min.Y)/scale + y})
p1 := cv.tf(vec{float64(bounds.Min.X)/scale + curX, float64(bounds.Max.Y)/scale + y}) p1 := cv.tf(backendbase.Vec{float64(bounds.Min.X)/scale + curX, float64(bounds.Max.Y)/scale + y})
p2 := cv.tf(vec{float64(bounds.Max.X)/scale + curX, float64(bounds.Max.Y)/scale + y}) p2 := cv.tf(backendbase.Vec{float64(bounds.Max.X)/scale + curX, float64(bounds.Max.Y)/scale + y})
p3 := cv.tf(vec{float64(bounds.Max.X)/scale + curX, float64(bounds.Min.Y)/scale + y}) p3 := cv.tf(backendbase.Vec{float64(bounds.Max.X)/scale + curX, float64(bounds.Min.Y)/scale + y})
inside := (p0[0] >= 0 || p1[0] >= 0 || p2[0] >= 0 || p3[0] >= 0) && inside := (p0[0] >= 0 || p1[0] >= 0 || p2[0] >= 0 || p3[0] >= 0) &&
(p0[1] >= 0 || p1[1] >= 0 || p2[1] >= 0 || p3[1] >= 0) && (p0[1] >= 0 || p1[1] >= 0 || p2[1] >= 0 || p3[1] >= 0) &&
(p0[0] < fw || p1[0] < fw || p2[0] < fw || p3[0] < fw) && (p0[0] < fw || p1[0] < fw || p2[0] < fw || p3[0] < fw) &&
@ -299,10 +300,10 @@ func (cv *Canvas) FillText(str string, x, y float64) {
// render textImage to the screen // render textImage to the screen
var pts [4][2]float64 var pts [4][2]float64
pts[0] = cv.tf(vec{float64(textOffset.X)/scale + x, float64(textOffset.Y)/scale + y}) pts[0] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + x, float64(textOffset.Y)/scale + y})
pts[1] = cv.tf(vec{float64(textOffset.X)/scale + x, float64(textOffset.Y)/scale + fstrHeight + y}) pts[1] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + x, float64(textOffset.Y)/scale + fstrHeight + y})
pts[2] = cv.tf(vec{float64(textOffset.X)/scale + fstrWidth + x, float64(textOffset.Y)/scale + fstrHeight + y}) pts[2] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + fstrWidth + x, float64(textOffset.Y)/scale + fstrHeight + y})
pts[3] = cv.tf(vec{float64(textOffset.X)/scale + fstrWidth + x, float64(textOffset.Y)/scale + y}) pts[3] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + fstrWidth + x, float64(textOffset.Y)/scale + y})
mask := textImage.SubImage(image.Rect(0, 0, strWidth, strHeight)).(*image.Alpha) mask := textImage.SubImage(image.Rect(0, 0, strWidth, strHeight)).(*image.Alpha)
@ -345,7 +346,7 @@ func (cv *Canvas) StrokeText(str string, x, y float64) {
continue continue
} }
cv.runePath(rn, vec{x, y}) cv.runePath(rn, backendbase.Vec{x, y})
x += float64(advance) / 64 x += float64(advance) / 64
} }
@ -354,7 +355,7 @@ func (cv *Canvas) StrokeText(str string, x, y float64) {
cv.path = prevPath cv.path = prevPath
} }
func (cv *Canvas) runePath(rn rune, pos vec) { func (cv *Canvas) runePath(rn rune, pos backendbase.Vec) {
gb := &truetype.GlyphBuf{} gb := &truetype.GlyphBuf{}
gb.Load(cv.state.font.font, cv.state.fontSize, cv.state.font.font.Index(rn), font.HintingNone) gb.Load(cv.state.font.font, cv.state.fontSize, cv.state.font.font.Index(rn), font.HintingNone)

View file

@ -3,11 +3,13 @@ package canvas
import ( import (
"math" "math"
"sort" "sort"
"github.com/tfriedel6/canvas/backend/backendbase"
) )
const samePointTolerance = 1e-20 const samePointTolerance = 1e-20
func pointIsRightOfLine(a, b, p vec) (bool, bool) { func pointIsRightOfLine(a, b, p backendbase.Vec) (bool, bool) {
if a[1] == b[1] { if a[1] == b[1] {
return false, false return false, false
} }
@ -19,13 +21,13 @@ func pointIsRightOfLine(a, b, p vec) (bool, bool) {
if p[1] < a[1] || p[1] >= b[1] { if p[1] < a[1] || p[1] >= b[1] {
return false, false return false, false
} }
v := b.sub(a) v := b.Sub(a)
r := (p[1] - a[1]) / v[1] r := (p[1] - a[1]) / v[1]
x := a[0] + r*v[0] x := a[0] + r*v[0]
return p[0] > x, dir return p[0] > x, dir
} }
func triangleContainsPoint(a, b, c, p vec) bool { func triangleContainsPoint(a, b, c, p backendbase.Vec) bool {
// if point is outside triangle bounds, return false // if point is outside triangle bounds, return false
if p[0] < a[0] && p[0] < b[0] && p[0] < c[0] { if p[0] < a[0] && p[0] < b[0] && p[0] < c[0] {
return false return false
@ -56,12 +58,12 @@ func triangleContainsPoint(a, b, c, p vec) bool {
const parallelTolerance = 1e-10 const parallelTolerance = 1e-10
func parallel(a1, b1, a2, b2 vec) bool { func parallel(a1, b1, a2, b2 backendbase.Vec) bool {
ang := b1.sub(a1).angleTo(b2.sub(a2)) ang := b1.Sub(a1).AngleTo(b2.Sub(a2))
return math.Abs(ang) < parallelTolerance || math.Abs(ang-math.Pi) < parallelTolerance return math.Abs(ang) < parallelTolerance || math.Abs(ang-math.Pi) < parallelTolerance
} }
func polygonContainsLine(polygon []vec, ia, ib int, a, b vec) bool { func polygonContainsLine(polygon []backendbase.Vec, ia, ib int, a, b backendbase.Vec) bool {
for i := range polygon { for i := range polygon {
if i == ia || i == ib { if i == ia || i == ib {
continue continue
@ -80,7 +82,7 @@ func polygonContainsLine(polygon []vec, ia, ib int, a, b vec) bool {
const onLineToleranceSqr = 1e-20 const onLineToleranceSqr = 1e-20
func polygonContainsPoint(polygon []vec, p vec) bool { func polygonContainsPoint(polygon []backendbase.Vec, p backendbase.Vec) bool {
a := polygon[len(polygon)-1] a := polygon[len(polygon)-1]
count := 0 count := 0
for _, b := range polygon { for _, b := range polygon {
@ -95,15 +97,15 @@ func polygonContainsPoint(polygon []vec, p vec) bool {
return count%2 == 1 return count%2 == 1
} }
func triangulatePath(path []pathPoint, mat mat, target [][2]float64) [][2]float64 { func triangulatePath(path []pathPoint, mat backendbase.Mat, target [][2]float64) [][2]float64 {
if path[0].pos == path[len(path)-1].pos { if path[0].pos == path[len(path)-1].pos {
path = path[:len(path)-1] path = path[:len(path)-1]
} }
var buf [500]vec var buf [500]backendbase.Vec
polygon := buf[:0] polygon := buf[:0]
for _, p := range path { for _, p := range path {
polygon = append(polygon, p.pos.mulMat(mat)) polygon = append(polygon, p.pos.MulMat(mat))
} }
for len(polygon) > 3 { for len(polygon) > 3 {
@ -117,7 +119,7 @@ func triangulatePath(path []pathPoint, mat mat, target [][2]float64) [][2]float6
if isSamePoint(a, c, math.SmallestNonzeroFloat64) { if isSamePoint(a, c, math.SmallestNonzeroFloat64) {
break break
} }
if len(polygon) > 3 && !polygonContainsPoint(polygon, a.add(c).divf(2)) { if len(polygon) > 3 && !polygonContainsPoint(polygon, a.Add(c).Divf(2)) {
continue continue
} }
if !polygonContainsLine(polygon, i, ic, a, c) { if !polygonContainsLine(polygon, i, ic, a, c) {
@ -156,7 +158,7 @@ type tessNet struct {
} }
type tessVert struct { type tessVert struct {
pos vec pos backendbase.Vec
attached []int attached []int
count int count int
} }
@ -183,7 +185,7 @@ func cutIntersections(path []pathPoint) tessNet {
type cut struct { type cut struct {
from, to int from, to int
ratio float64 ratio float64
point vec point backendbase.Vec
} }
var cutBuf [50]cut var cutBuf [50]cut
@ -296,8 +298,8 @@ func cutIntersections(path []pathPoint) tessNet {
func setPathLeftRightInside(net *tessNet) { func setPathLeftRightInside(net *tessNet) {
for i, e1 := range net.edges { for i, e1 := range net.edges {
a1, b1 := net.verts[e1.a], net.verts[e1.b] a1, b1 := net.verts[e1.a], net.verts[e1.b]
diff := b1.pos.sub(a1.pos) diff := b1.pos.Sub(a1.pos)
mid := a1.pos.add(diff.mulf(0.5)) mid := a1.pos.Add(diff.Mulf(0.5))
left, right := 0, 0 left, right := 0, 0
if math.Abs(diff[1]) > math.Abs(diff[0]) { if math.Abs(diff[1]) > math.Abs(diff[0]) {
@ -315,7 +317,7 @@ func setPathLeftRightInside(net *tessNet) {
if mid[1] < a2[1] || mid[1] > b2[1] { if mid[1] < a2[1] || mid[1] > b2[1] {
continue continue
} }
v := b2.sub(a2) v := b2.Sub(a2)
r := (mid[1] - a2[1]) / v[1] r := (mid[1] - a2[1]) / v[1]
x := a2[0] + r*v[0] x := a2[0] + r*v[0]
if mid[0] > x { if mid[0] > x {
@ -342,7 +344,7 @@ func setPathLeftRightInside(net *tessNet) {
if mid[0] < a2[0] || mid[0] > b2[0] { if mid[0] < a2[0] || mid[0] > b2[0] {
continue continue
} }
v := b2.sub(a2) v := b2.Sub(a2)
r := (mid[0] - a2[0]) / v[0] r := (mid[0] - a2[0]) / v[0]
y := a2[1] + r*v[1] y := a2[1] + r*v[1]
if mid[1] > y { if mid[1] > y {
@ -404,7 +406,7 @@ func selfIntersectingPathParts(p []pathPoint, partFn func(sp []pathPoint) bool)
for limit := 0; limit < len(net.edges); limit++ { for limit := 0; limit < len(net.edges); limit++ {
ecur := net.edges[from] ecur := net.edges[from]
acur, bcur := net.verts[ecur.a], net.verts[ecur.b] acur, bcur := net.verts[ecur.a], net.verts[ecur.b]
dir := bcur.pos.sub(acur.pos) dir := bcur.pos.Sub(acur.pos)
dirAngle := math.Atan2(dir[1], dir[0]) dirAngle := math.Atan2(dir[1], dir[0])
minAngleDiff := math.Pi * 2 minAngleDiff := math.Pi * 2
var next, nextEdge int var next, nextEdge int
@ -421,7 +423,7 @@ func selfIntersectingPathParts(p []pathPoint, partFn func(sp []pathPoint) bool)
if e.b == cur { if e.b == cur {
na, nb = nb, na na, nb = nb, na
} }
ndir := nb.pos.sub(na.pos) ndir := nb.pos.Sub(na.pos)
nextAngle := math.Atan2(ndir[1], ndir[0]) + math.Pi nextAngle := math.Atan2(ndir[1], ndir[0]) + math.Pi
if nextAngle < dirAngle { if nextAngle < dirAngle {
nextAngle += math.Pi * 2 nextAngle += math.Pi * 2