op/clip,gpu,internal/scene: encode cubic bézier curves natively

The compute renderer supports cubic curves, so encode them as such.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-03-11 18:34:45 +01:00
parent f1ae923a89
commit 9e79cee447
4 changed files with 130 additions and 70 deletions
+7
View File
@@ -706,6 +706,13 @@ func encodePath(pathData []byte, stroke clip.StrokeStyle, dashes dashOp) encoder
}
hasPrev = true
prevTo = to
case scene.OpFillCubic:
from, _, _, to := scene.DecodeCubic(cmd)
if hasPrev && from != prevTo {
enc.scene[len(enc.scene)-1][0] |= (flagEndPath << 16)
}
hasPrev = true
prevTo = to
default:
panic("unsupported path scene command")
}
+88
View File
@@ -1404,6 +1404,11 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
q = q.Transform(tr)
qs.splitAndEncode(q)
case scene.OpFillCubic:
for _, q := range splitCubic(scene.DecodeCubic(cmd)) {
q = q.Transform(tr)
qs.splitAndEncode(q)
}
default:
panic("unsupported scene command")
}
@@ -1436,6 +1441,14 @@ func decodeToStrokeQuads(pathData []byte) strokeQuads {
quad: q,
}
quads = append(quads, quad)
case scene.OpFillCubic:
for _, q := range splitCubic(scene.DecodeCubic(cmd)) {
quad := strokeQuad{
contour: contour,
quad: q,
}
quads = append(quads, quad)
}
default:
panic("unsupported scene command")
}
@@ -1536,3 +1549,78 @@ func decodeQuad(d []byte) (q quadSegment) {
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
return
}
func splitCubic(from, ctrl0, ctrl1, to f32.Point) []quadSegment {
quads := make([]quadSegment, 0, 10)
// Set the maximum distance proportionally to the longest side
// of the bounding rectangle.
hull := f32.Rectangle{
Min: from,
Max: ctrl0,
}.Canon().Add(ctrl1).Add(to)
l := hull.Dx()
if h := hull.Dy(); h > l {
l = h
}
approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to)
return quads
}
// approxCube approximates a cubic Bézier by a series of quadratic
// curves.
func approxCubeTo(quads *[]quadSegment, splits int, maxDist float32, from, ctrl0, ctrl1, to f32.Point) int {
// The idea is from
// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
// where a quadratic approximates a cubic by eliminating its t³ term
// from its polynomial expression anchored at the starting point:
//
// P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
//
// The control point for the new quadratic Q1 that shares starting point, pen, with P is
//
// C1 = (3ctrl0 - pen)/2
//
// The reverse cubic anchored at the end point has the polynomial
//
// P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
//
// The corresponding quadratic Q2 that shares the end point, to, with P has control
// point
//
// C2 = (3ctrl1 - to)/2
//
// The combined quadratic Bézier, Q, shares both start and end points with its cubic
// and use the midpoint between the two curves Q1 and Q2 as control point:
//
// C = (3ctrl0 - pen + 3ctrl1 - to)/4
c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
const maxSplits = 32
if splits >= maxSplits {
*quads = append(*quads, quadSegment{From: from, Ctrl: c, To: to})
return splits
}
// The maximum distance between the cubic P and its approximation Q given t
// can be shown to be
//
// d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
//
// To save a square root, compare d² with the squared tolerance.
v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from)
d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
if d2 <= maxDist*maxDist {
*quads = append(*quads, quadSegment{From: from, Ctrl: c, To: to})
return splits
}
// De Casteljau split the curve and approximate the halves.
t := float32(0.5)
c0 := from.Add(ctrl0.Sub(from).Mul(t))
c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
c01 := c0.Add(c1.Sub(c0).Mul(t))
c12 := c1.Add(c2.Sub(c1).Mul(t))
c0112 := c01.Add(c12.Sub(c01).Mul(t))
splits++
splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112)
splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to)
return splits
}
+29
View File
@@ -56,6 +56,24 @@ func Line(start, end f32.Point, stroke bool, flags uint32) Command {
}
}
func Cubic(start, ctrl0, ctrl1, end f32.Point, stroke bool) Command {
tag := uint32(OpFillCubic)
if stroke {
tag = uint32(OpStrokeCubic)
}
return Command{
0: tag,
1: math.Float32bits(start.X),
2: math.Float32bits(start.Y),
3: math.Float32bits(ctrl0.X),
4: math.Float32bits(ctrl0.Y),
5: math.Float32bits(ctrl1.X),
6: math.Float32bits(ctrl1.Y),
7: math.Float32bits(end.X),
8: math.Float32bits(end.Y),
}
}
func Quad(start, ctrl, end f32.Point, stroke bool) Command {
tag := uint32(OpFillQuad)
if stroke {
@@ -151,3 +169,14 @@ func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) {
to = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6]))
return
}
func DecodeCubic(cmd Command) (from, ctrl0, ctrl1, to f32.Point) {
if cmd[0] != uint32(OpFillCubic) {
panic("invalid command")
}
from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
ctrl0 = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
ctrl1 = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6]))
to = f32.Pt(math.Float32frombits(cmd[7]), math.Float32frombits(cmd[8]))
return
}
+6 -70
View File
@@ -300,76 +300,12 @@ func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
ctrl0 = ctrl0.Add(p.pen)
ctrl1 = ctrl1.Add(p.pen)
to = to.Add(p.pen)
// Set the maximum distance proportionally to the longest side
// of the bounding rectangle.
hull := f32.Rectangle{
Min: p.pen,
Max: ctrl0,
}.Canon().Add(ctrl1).Add(to)
l := hull.Dx()
if h := hull.Dy(); h > l {
l = h
}
p.approxCubeTo(0, l*0.001, ctrl0, ctrl1, to)
}
// approxCube approximates a cubic Bézier by a series of quadratic
// curves.
func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Point) int {
// The idea is from
// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
// where a quadratic approximates a cubic by eliminating its t³ term
// from its polynomial expression anchored at the starting point:
//
// P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
//
// The control point for the new quadratic Q1 that shares starting point, pen, with P is
//
// C1 = (3ctrl0 - pen)/2
//
// The reverse cubic anchored at the end point has the polynomial
//
// P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
//
// The corresponding quadratic Q2 that shares the end point, to, with P has control
// point
//
// C2 = (3ctrl1 - to)/2
//
// The combined quadratic Bézier, Q, shares both start and end points with its cubic
// and use the midpoint between the two curves Q1 and Q2 as control point:
//
// C = (3ctrl0 - pen + 3ctrl1 - to)/4
c := ctrl0.Mul(3).Sub(p.pen).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
const maxSplits = 32
if splits >= maxSplits {
p.QuadTo(c, to)
return splits
}
// The maximum distance between the cubic P and its approximation Q given t
// can be shown to be
//
// d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
//
// To save a square root, compare d² with the squared tolerance.
v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(p.pen)
d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
if d2 <= maxDist*maxDist {
p.QuadTo(c, to)
return splits
}
// De Casteljau split the curve and approximate the halves.
t := float32(0.5)
c0 := p.pen.Add(ctrl0.Sub(p.pen).Mul(t))
c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
c01 := c0.Add(c1.Sub(c0).Mul(t))
c12 := c1.Add(c2.Sub(c1).Mul(t))
c0112 := c01.Add(c12.Sub(c01).Mul(t))
splits++
splits = p.approxCubeTo(splits, maxDist, c0, c01, c0112)
splits = p.approxCubeTo(splits, maxDist, c12, c2, to)
return splits
data := p.ops.Write(scene.CommandSize + 4)
bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour))
ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to, false))
p.pen = to
p.hasSegments = true
}
// Close closes the path contour.