mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
internal/stroke,gpu: create internal package for stroke to path conversion
Complex strokes are not yet supported in either of the current renderers, so they are converted to filled outlines in package gpu. We're about to move that complexity up to the op/clip package, so we're going to need the converter available from outside package gpu. This change extracts the conversion code and related types to the separate, internal package stroke. No functional changes; a follow-up moves the stroke conversion. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+2
-1
@@ -2,6 +2,7 @@ package gpu
|
||||
|
||||
import (
|
||||
"gioui.org/f32"
|
||||
"gioui.org/internal/stroke"
|
||||
)
|
||||
|
||||
type quadSplitter struct {
|
||||
@@ -46,7 +47,7 @@ func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) {
|
||||
encodeQuadTo(data, qs.contour, from, ctrl, to)
|
||||
}
|
||||
|
||||
func (qs *quadSplitter) splitAndEncode(quad quadSegment) {
|
||||
func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) {
|
||||
cbnd := f32.Rectangle{
|
||||
Min: quad.From,
|
||||
Max: quad.To,
|
||||
|
||||
+4
-3
@@ -18,6 +18,7 @@ import (
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/internal/scene"
|
||||
"gioui.org/internal/stroke"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -695,7 +696,7 @@ func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin b
|
||||
}
|
||||
|
||||
func supportsStroke(p *pathOp) bool {
|
||||
return isSolidLine(p.dashes) && p.stroke.Miter == 0 && p.stroke.Join == clip.RoundJoin && p.stroke.Cap == clip.RoundCap
|
||||
return stroke.IsSolidLine(p.dashes) && p.stroke.Miter == 0 && p.stroke.Join == clip.RoundJoin && p.stroke.Cap == clip.RoundCap
|
||||
}
|
||||
|
||||
func isStroke(p *pathOp) bool {
|
||||
@@ -707,9 +708,9 @@ func encodePath(p *pathOp) encoder {
|
||||
verts := p.pathVerts
|
||||
if p.stroke.Width > 0 && !supportsStroke(p) {
|
||||
quads := decodeToStrokeQuads(verts)
|
||||
quads = quads.stroke(p.stroke, p.dashes)
|
||||
quads = quads.Stroke(p.stroke, p.dashes)
|
||||
for _, quad := range quads {
|
||||
q := quad.quad
|
||||
q := quad.Quad
|
||||
enc.quad(q.From, q.Ctrl, q.To)
|
||||
}
|
||||
return enc
|
||||
|
||||
-389
@@ -1,389 +0,0 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
// The algorithms to compute dashes have been extracted, adapted from
|
||||
// (and used as a reference implementation):
|
||||
// - github.com/tdewolff/canvas (Licensed under MIT)
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"gioui.org/f32"
|
||||
)
|
||||
|
||||
func isSolidLine(sty dashOp) bool {
|
||||
return sty.phase == 0 && len(sty.dashes) == 0
|
||||
}
|
||||
|
||||
func (qs strokeQuads) dash(sty dashOp) strokeQuads {
|
||||
sty = dashCanonical(sty)
|
||||
|
||||
switch {
|
||||
case len(sty.dashes) == 0:
|
||||
return qs
|
||||
case len(sty.dashes) == 1 && sty.dashes[0] == 0.0:
|
||||
return strokeQuads{}
|
||||
}
|
||||
|
||||
if len(sty.dashes)%2 == 1 {
|
||||
// If the dash pattern is of uneven length, dash and space lengths
|
||||
// alternate. The following duplicates the pattern so that uneven
|
||||
// indices are always spaces.
|
||||
sty.dashes = append(sty.dashes, sty.dashes...)
|
||||
}
|
||||
|
||||
var (
|
||||
i0, pos0 = dashStart(sty)
|
||||
out strokeQuads
|
||||
|
||||
contour uint32 = 1
|
||||
)
|
||||
|
||||
for _, ps := range qs.split() {
|
||||
var (
|
||||
i = i0
|
||||
pos = pos0
|
||||
t []float64
|
||||
length = ps.len()
|
||||
)
|
||||
for pos+sty.dashes[i] < length {
|
||||
pos += sty.dashes[i]
|
||||
if 0.0 < pos {
|
||||
t = append(t, float64(pos))
|
||||
}
|
||||
i++
|
||||
if i == len(sty.dashes) {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
|
||||
j0 := 0
|
||||
endsInDash := i%2 == 0
|
||||
if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash {
|
||||
j0 = 1
|
||||
}
|
||||
|
||||
var (
|
||||
qd strokeQuads
|
||||
pd = ps.splitAt(&contour, t...)
|
||||
)
|
||||
for j := j0; j < len(pd)-1; j += 2 {
|
||||
qd = qd.append(pd[j])
|
||||
}
|
||||
if endsInDash {
|
||||
if ps.closed() {
|
||||
qd = pd[len(pd)-1].append(qd)
|
||||
} else {
|
||||
qd = qd.append(pd[len(pd)-1])
|
||||
}
|
||||
}
|
||||
out = out.append(qd)
|
||||
contour++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dashCanonical(sty dashOp) dashOp {
|
||||
var (
|
||||
o = sty
|
||||
ds = o.dashes
|
||||
)
|
||||
|
||||
if len(sty.dashes) == 0 {
|
||||
return sty
|
||||
}
|
||||
|
||||
// Remove zeros except first and last.
|
||||
for i := 1; i < len(ds)-1; i++ {
|
||||
if f32Eq(ds[i], 0.0) {
|
||||
ds[i-1] += ds[i+1]
|
||||
ds = append(ds[:i], ds[i+2:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
// Remove first zero, collapse with second and last.
|
||||
if f32Eq(ds[0], 0.0) {
|
||||
if len(ds) < 3 {
|
||||
return dashOp{
|
||||
phase: 0.0,
|
||||
dashes: []float32{0.0},
|
||||
}
|
||||
}
|
||||
o.phase -= ds[1]
|
||||
ds[len(ds)-1] += ds[1]
|
||||
ds = ds[2:]
|
||||
}
|
||||
|
||||
// Remove last zero, collapse with fist and second to last.
|
||||
if f32Eq(ds[len(ds)-1], 0.0) {
|
||||
if len(ds) < 3 {
|
||||
return dashOp{}
|
||||
}
|
||||
o.phase += ds[len(ds)-2]
|
||||
ds[0] += ds[len(ds)-2]
|
||||
ds = ds[:len(ds)-2]
|
||||
}
|
||||
|
||||
// If there are zeros or negatives, don't draw dashes.
|
||||
for i := 0; i < len(ds); i++ {
|
||||
if ds[i] < 0.0 || f32Eq(ds[i], 0.0) {
|
||||
return dashOp{
|
||||
phase: 0.0,
|
||||
dashes: []float32{0.0},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove repeated patterns.
|
||||
loop:
|
||||
for len(ds)%2 == 0 {
|
||||
mid := len(ds) / 2
|
||||
for i := 0; i < mid; i++ {
|
||||
if !f32Eq(ds[i], ds[mid+i]) {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
ds = ds[:mid]
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func dashStart(sty dashOp) (int, float32) {
|
||||
i0 := 0 // i0 is the index into dashes.
|
||||
for sty.dashes[i0] <= sty.phase {
|
||||
sty.phase -= sty.dashes[i0]
|
||||
i0++
|
||||
if i0 == len(sty.dashes) {
|
||||
i0 = 0
|
||||
}
|
||||
}
|
||||
// pos0 may be negative if the offset lands halfway into dash.
|
||||
pos0 := -sty.phase
|
||||
if sty.phase < 0.0 {
|
||||
var sum float32
|
||||
for _, d := range sty.dashes {
|
||||
sum += d
|
||||
}
|
||||
pos0 = -(sum + sty.phase) // handle negative offsets
|
||||
}
|
||||
return i0, pos0
|
||||
}
|
||||
|
||||
func (qs strokeQuads) len() float32 {
|
||||
var sum float32
|
||||
for i := range qs {
|
||||
q := qs[i].quad
|
||||
sum += quadBezierLen(q.From, q.Ctrl, q.To)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// splitAt splits the path into separate paths at the specified intervals
|
||||
// along the path.
|
||||
// splitAt updates the provided contour counter as it splits the segments.
|
||||
func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads {
|
||||
if len(ts) == 0 {
|
||||
qs.setContour(*contour)
|
||||
return []strokeQuads{qs}
|
||||
}
|
||||
|
||||
sort.Float64s(ts)
|
||||
if ts[0] == 0 {
|
||||
ts = ts[1:]
|
||||
}
|
||||
|
||||
var (
|
||||
j int // index into ts
|
||||
t float64 // current position along curve
|
||||
)
|
||||
|
||||
var oo []strokeQuads
|
||||
var oi strokeQuads
|
||||
push := func() {
|
||||
oo = append(oo, oi)
|
||||
oi = nil
|
||||
}
|
||||
|
||||
for _, ps := range qs.split() {
|
||||
for _, q := range ps {
|
||||
if j == len(ts) {
|
||||
oi = append(oi, q)
|
||||
continue
|
||||
}
|
||||
speed := func(t float64) float64 {
|
||||
return float64(lenPt(quadBezierD1(q.quad.From, q.quad.Ctrl, q.quad.To, float32(t))))
|
||||
}
|
||||
invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0, 1)
|
||||
|
||||
var (
|
||||
t0 float64
|
||||
r0 = q.quad.From
|
||||
r1 = q.quad.Ctrl
|
||||
r2 = q.quad.To
|
||||
|
||||
// from keeps track of the start of the 'running' segment.
|
||||
from = r0
|
||||
)
|
||||
for j < len(ts) && t < ts[j] && ts[j] <= t+dt {
|
||||
tj := invL(ts[j] - t)
|
||||
tsub := (tj - t0) / (1.0 - t0)
|
||||
t0 = tj
|
||||
|
||||
var q1 f32.Point
|
||||
_, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub))
|
||||
|
||||
oi = append(oi, strokeQuad{
|
||||
contour: *contour,
|
||||
quad: quadSegment{
|
||||
From: from,
|
||||
Ctrl: q1,
|
||||
To: r0,
|
||||
},
|
||||
})
|
||||
push()
|
||||
(*contour)++
|
||||
|
||||
from = r0
|
||||
j++
|
||||
}
|
||||
if !f64Eq(t0, 1) {
|
||||
if len(oi) > 0 {
|
||||
r0 = oi.pen()
|
||||
}
|
||||
oi = append(oi, strokeQuad{
|
||||
contour: *contour,
|
||||
quad: quadSegment{
|
||||
From: r0,
|
||||
Ctrl: r1,
|
||||
To: r2,
|
||||
},
|
||||
})
|
||||
}
|
||||
t += dt
|
||||
}
|
||||
}
|
||||
if len(oi) > 0 {
|
||||
push()
|
||||
(*contour)++
|
||||
}
|
||||
|
||||
return oo
|
||||
}
|
||||
|
||||
func f32Eq(a, b float32) bool {
|
||||
const epsilon = 1e-10
|
||||
return math.Abs(float64(a-b)) < epsilon
|
||||
}
|
||||
|
||||
func f64Eq(a, b float64) bool {
|
||||
const epsilon = 1e-10
|
||||
return math.Abs(a-b) < epsilon
|
||||
}
|
||||
|
||||
func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, fp func(float64) float64, tmin, tmax float64) (func(float64) float64, float64) {
|
||||
// The TODOs below are copied verbatim from tdewolff/canvas:
|
||||
//
|
||||
// TODO: find better way to determine N. For Arc 10 seems fine, for some
|
||||
// Quads 10 is too low, for Cube depending on inflection points is
|
||||
// maybe not the best indicator
|
||||
//
|
||||
// TODO: track efficiency, how many times is fp called?
|
||||
// Does a look-up table make more sense?
|
||||
fLength := func(t float64) float64 {
|
||||
return math.Abs(gaussLegendre(fp, tmin, t))
|
||||
}
|
||||
totalLength := fLength(tmax)
|
||||
t := func(L float64) float64 {
|
||||
return bisectionMethod(fLength, L, tmin, tmax)
|
||||
}
|
||||
return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, tmax), totalLength
|
||||
}
|
||||
|
||||
func polynomialChebyshevApprox(N int, f func(float64) float64, xmin, xmax, ymin, ymax float64) func(float64) float64 {
|
||||
var (
|
||||
invN = 1.0 / float64(N)
|
||||
fs = make([]float64, N)
|
||||
)
|
||||
for k := 0; k < N; k++ {
|
||||
u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN)
|
||||
fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1))
|
||||
}
|
||||
|
||||
c := make([]float64, N)
|
||||
for j := 0; j < N; j++ {
|
||||
var a float64
|
||||
for k := 0; k < N; k++ {
|
||||
a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N))
|
||||
}
|
||||
c[j] = 2 * invN * a
|
||||
}
|
||||
|
||||
if ymax < ymin {
|
||||
ymin, ymax = ymax, ymin
|
||||
}
|
||||
return func(x float64) float64 {
|
||||
x = math.Min(xmax, math.Max(xmin, x))
|
||||
u := (x-xmin)/(xmax-xmin)*2 - 1
|
||||
var a float64
|
||||
for j := 0; j < N; j++ {
|
||||
a += c[j] * math.Cos(float64(j)*math.Acos(u))
|
||||
}
|
||||
y := -0.5*c[0] + a
|
||||
if !math.IsNaN(ymin) && !math.IsNaN(ymax) {
|
||||
y = math.Min(ymax, math.Max(ymin, y))
|
||||
}
|
||||
return y
|
||||
}
|
||||
}
|
||||
|
||||
// bisectionMethod finds the value x for which f(x) = y in the interval x
|
||||
// in [xmin, xmax] using the bisection method.
|
||||
func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 {
|
||||
const (
|
||||
maxIter = 100
|
||||
tolerance = 0.001 // 0.1%
|
||||
)
|
||||
|
||||
var (
|
||||
n = 0
|
||||
x float64
|
||||
tolX = math.Abs(xmax-xmin) * tolerance
|
||||
tolY = math.Abs(f(xmax)-f(xmin)) * tolerance
|
||||
)
|
||||
for {
|
||||
x = 0.5 * (xmin + xmax)
|
||||
if n >= maxIter {
|
||||
return x
|
||||
}
|
||||
|
||||
dy := f(x) - y
|
||||
switch {
|
||||
case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX:
|
||||
return x
|
||||
case dy > 0:
|
||||
xmax = x
|
||||
default:
|
||||
xmin = x
|
||||
}
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
type gaussLegendreFunc func(func(float64) float64, float64, float64) float64
|
||||
|
||||
// Gauss-Legendre quadrature integration from a to b with n=7
|
||||
func gaussLegendre7(f func(float64) float64, a, b float64) float64 {
|
||||
c := 0.5 * (b - a)
|
||||
d := 0.5 * (a + b)
|
||||
Qd1 := f(-0.949108*c + d)
|
||||
Qd2 := f(-0.741531*c + d)
|
||||
Qd3 := f(-0.405845*c + d)
|
||||
Qd4 := f(d)
|
||||
Qd5 := f(0.405845*c + d)
|
||||
Qd6 := f(0.741531*c + d)
|
||||
Qd7 := f(0.949108*c + d)
|
||||
return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4)
|
||||
}
|
||||
+43
-64
@@ -26,6 +26,7 @@ import (
|
||||
"gioui.org/internal/opconst"
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/internal/scene"
|
||||
"gioui.org/internal/stroke"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -130,7 +131,7 @@ type pathOp struct {
|
||||
// For compute
|
||||
trans f32.Affine2D
|
||||
stroke clip.StrokeStyle
|
||||
dashes dashOp
|
||||
dashes stroke.DashOp
|
||||
}
|
||||
|
||||
type imageOp struct {
|
||||
@@ -142,24 +143,15 @@ type imageOp struct {
|
||||
place placement
|
||||
}
|
||||
|
||||
type dashOp struct {
|
||||
phase float32
|
||||
dashes []float32
|
||||
}
|
||||
|
||||
type quadSegment struct {
|
||||
From, Ctrl, To f32.Point
|
||||
}
|
||||
|
||||
func decodeDashOp(data []byte) dashOp {
|
||||
func decodeDashOp(data []byte) stroke.DashOp {
|
||||
_ = data[5]
|
||||
if opconst.OpType(data[0]) != opconst.TypeDash {
|
||||
panic("invalid op")
|
||||
}
|
||||
bo := binary.LittleEndian
|
||||
return dashOp{
|
||||
phase: math.Float32frombits(bo.Uint32(data[1:])),
|
||||
dashes: make([]float32, data[5]),
|
||||
return stroke.DashOp{
|
||||
Phase: math.Float32frombits(bo.Uint32(data[1:])),
|
||||
Dashes: make([]float32, data[5]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,7 +844,7 @@ func (d *drawOps) newPathOp() *pathOp {
|
||||
return &d.pathOpCache[len(d.pathOpCache)-1]
|
||||
}
|
||||
|
||||
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle, dashes dashOp) {
|
||||
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle, dashes stroke.DashOp) {
|
||||
npath := d.newPathOp()
|
||||
*npath = pathOp{
|
||||
parent: state.cpath,
|
||||
@@ -891,8 +883,8 @@ func (d *drawOps) save(id int, state drawState) {
|
||||
func (d *drawOps) collectOps(r *ops.Reader, state drawState) {
|
||||
var (
|
||||
quads quadsOp
|
||||
stroke clip.StrokeStyle
|
||||
dashes dashOp
|
||||
str clip.StrokeStyle
|
||||
dashes stroke.DashOp
|
||||
z int
|
||||
)
|
||||
d.save(opconst.InitialStateID, state)
|
||||
@@ -907,22 +899,22 @@ loop:
|
||||
|
||||
case opconst.TypeDash:
|
||||
dashes = decodeDashOp(encOp.Data)
|
||||
if len(dashes.dashes) > 0 {
|
||||
if len(dashes.Dashes) > 0 {
|
||||
encOp, ok = r.Decode()
|
||||
if !ok {
|
||||
panic("gpu: could not decode dashes pattern")
|
||||
}
|
||||
data := encOp.Data[1:]
|
||||
bo := binary.LittleEndian
|
||||
for i := range dashes.dashes {
|
||||
dashes.dashes[i] = math.Float32frombits(bo.Uint32(
|
||||
for i := range dashes.Dashes {
|
||||
dashes.Dashes[i] = math.Float32frombits(bo.Uint32(
|
||||
data[i*4:],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
case opconst.TypeStroke:
|
||||
stroke = decodeStrokeOp(encOp.Data)
|
||||
str = decodeStrokeOp(encOp.Data)
|
||||
|
||||
case opconst.TypePath:
|
||||
encOp, ok = r.Decode()
|
||||
@@ -948,7 +940,7 @@ loop:
|
||||
op.bounds = v.bounds
|
||||
} else {
|
||||
pathData, bounds := d.buildVerts(
|
||||
quads.aux, trans, op.outline, stroke, dashes,
|
||||
quads.aux, trans, op.outline, str, dashes,
|
||||
)
|
||||
op.bounds = bounds
|
||||
if !d.compute {
|
||||
@@ -964,10 +956,10 @@ loop:
|
||||
quads.key.SetTransform(trans)
|
||||
}
|
||||
state.clip = state.clip.Intersect(op.bounds.Add(off))
|
||||
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, stroke, dashes)
|
||||
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, str, dashes)
|
||||
quads = quadsOp{}
|
||||
stroke = clip.StrokeStyle{}
|
||||
dashes = dashOp{}
|
||||
str = clip.StrokeStyle{}
|
||||
dashes = stroke.DashOp{}
|
||||
|
||||
case opconst.TypeColor:
|
||||
state.matType = materialColor
|
||||
@@ -1005,7 +997,7 @@ loop:
|
||||
// The paint operation is sheared or rotated, add a clip path representing
|
||||
// this transformed rectangle.
|
||||
encOp.Key.SetTransform(trans)
|
||||
d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}, dashOp{})
|
||||
d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}, stroke.DashOp{})
|
||||
}
|
||||
|
||||
bounds := boundRectF(cl)
|
||||
@@ -1357,7 +1349,7 @@ func (d *drawOps) writeVertCache(n int) []byte {
|
||||
}
|
||||
|
||||
// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
|
||||
func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle, dashes dashOp) (verts []byte, bounds f32.Rectangle) {
|
||||
func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle, dashes stroke.DashOp) (verts []byte, bounds f32.Rectangle) {
|
||||
inf := float32(math.Inf(+1))
|
||||
d.qs.bounds = f32.Rectangle{
|
||||
Min: f32.Point{X: inf, Y: inf},
|
||||
@@ -1370,12 +1362,12 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str
|
||||
case stroke.Width > 0:
|
||||
// Stroke path.
|
||||
quads := decodeToStrokeQuads(pathData)
|
||||
quads = quads.stroke(stroke, dashes)
|
||||
quads = quads.Stroke(stroke, dashes)
|
||||
for _, quad := range quads {
|
||||
d.qs.contour = quad.contour
|
||||
quad.quad = quad.quad.Transform(tr)
|
||||
d.qs.contour = quad.Contour
|
||||
quad.Quad = quad.Quad.Transform(tr)
|
||||
|
||||
d.qs.splitAndEncode(quad.quad)
|
||||
d.qs.splitAndEncode(quad.Quad)
|
||||
}
|
||||
|
||||
case outline:
|
||||
@@ -1394,13 +1386,13 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
|
||||
cmd := ops.DecodeCommand(pathData[4:])
|
||||
switch cmd.Op() {
|
||||
case scene.OpLine:
|
||||
var q quadSegment
|
||||
var q stroke.QuadSegment
|
||||
q.From, q.To = scene.DecodeLine(cmd)
|
||||
q.Ctrl = q.From.Add(q.To).Mul(.5)
|
||||
q = q.Transform(tr)
|
||||
qs.splitAndEncode(q)
|
||||
case scene.OpQuad:
|
||||
var q quadSegment
|
||||
var q stroke.QuadSegment
|
||||
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
|
||||
q = q.Transform(tr)
|
||||
qs.splitAndEncode(q)
|
||||
@@ -1418,34 +1410,34 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
|
||||
|
||||
// decodeToStrokeQuads is like decodeOutlineQuads, except it returns a list of stroke
|
||||
// quads ready to stroke.
|
||||
func decodeToStrokeQuads(pathData []byte) strokeQuads {
|
||||
quads := make(strokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
|
||||
func decodeToStrokeQuads(pathData []byte) stroke.StrokeQuads {
|
||||
quads := make(stroke.StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
|
||||
for len(pathData) >= scene.CommandSize+4 {
|
||||
contour := bo.Uint32(pathData)
|
||||
cmd := ops.DecodeCommand(pathData[4:])
|
||||
switch cmd.Op() {
|
||||
case scene.OpLine:
|
||||
var q quadSegment
|
||||
var q stroke.QuadSegment
|
||||
q.From, q.To = scene.DecodeLine(cmd)
|
||||
q.Ctrl = q.From.Add(q.To).Mul(.5)
|
||||
quad := strokeQuad{
|
||||
contour: contour,
|
||||
quad: q,
|
||||
quad := stroke.StrokeQuad{
|
||||
Contour: contour,
|
||||
Quad: q,
|
||||
}
|
||||
quads = append(quads, quad)
|
||||
case scene.OpQuad:
|
||||
var q quadSegment
|
||||
var q stroke.QuadSegment
|
||||
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
|
||||
quad := strokeQuad{
|
||||
contour: contour,
|
||||
quad: q,
|
||||
quad := stroke.StrokeQuad{
|
||||
Contour: contour,
|
||||
Quad: q,
|
||||
}
|
||||
quads = append(quads, quad)
|
||||
case scene.OpCubic:
|
||||
for _, q := range splitCubic(scene.DecodeCubic(cmd)) {
|
||||
quad := strokeQuad{
|
||||
contour: contour,
|
||||
quad: q,
|
||||
quad := stroke.StrokeQuad{
|
||||
Contour: contour,
|
||||
Quad: q,
|
||||
}
|
||||
quads = append(quads, quad)
|
||||
}
|
||||
@@ -1537,21 +1529,8 @@ func isPureOffset(t f32.Affine2D) bool {
|
||||
return a == 1 && b == 0 && d == 0 && e == 1
|
||||
}
|
||||
|
||||
func (q quadSegment) Transform(t f32.Affine2D) quadSegment {
|
||||
q.From = t.Transform(q.From)
|
||||
q.Ctrl = t.Transform(q.Ctrl)
|
||||
q.To = t.Transform(q.To)
|
||||
return q
|
||||
}
|
||||
|
||||
func decodeQuad(d []byte) (q quadSegment) {
|
||||
cmd := ops.DecodeCommand(d)
|
||||
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
func splitCubic(from, ctrl0, ctrl1, to f32.Point) []quadSegment {
|
||||
quads := make([]quadSegment, 0, 10)
|
||||
func splitCubic(from, ctrl0, ctrl1, to f32.Point) []stroke.QuadSegment {
|
||||
quads := make([]stroke.QuadSegment, 0, 10)
|
||||
// Set the maximum distance proportionally to the longest side
|
||||
// of the bounding rectangle.
|
||||
hull := f32.Rectangle{
|
||||
@@ -1568,7 +1547,7 @@ func splitCubic(from, ctrl0, ctrl1, to f32.Point) []quadSegment {
|
||||
|
||||
// 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 {
|
||||
func approxCubeTo(quads *[]stroke.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
|
||||
@@ -1596,7 +1575,7 @@ func approxCubeTo(quads *[]quadSegment, splits int, maxDist float32, from, ctrl0
|
||||
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})
|
||||
*quads = append(*quads, stroke.QuadSegment{From: from, Ctrl: c, To: to})
|
||||
return splits
|
||||
}
|
||||
// The maximum distance between the cubic P and its approximation Q given t
|
||||
@@ -1608,7 +1587,7 @@ func approxCubeTo(quads *[]quadSegment, splits int, maxDist float32, from, ctrl0
|
||||
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})
|
||||
*quads = append(*quads, stroke.QuadSegment{From: from, Ctrl: c, To: to})
|
||||
return splits
|
||||
}
|
||||
// De Casteljau split the curve and approximate the halves.
|
||||
|
||||
-641
@@ -1,641 +0,0 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
// Most of the algorithms to compute strokes and their offsets have been
|
||||
// extracted, adapted from (and used as a reference implementation):
|
||||
// - github.com/tdewolff/canvas (Licensed under MIT)
|
||||
//
|
||||
// These algorithms have been implemented from:
|
||||
// Fast, precise flattening of cubic Bézier path and offset curves
|
||||
// Thomas F. Hain, et al.
|
||||
//
|
||||
// An electronic version is available at:
|
||||
// https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf
|
||||
//
|
||||
// Possible improvements (in term of speed and/or accuracy) on these
|
||||
// algorithms are:
|
||||
//
|
||||
// - Polar Stroking: New Theory and Methods for Stroking Paths,
|
||||
// M. Kilgard
|
||||
// https://arxiv.org/pdf/2007.00308.pdf
|
||||
//
|
||||
// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
|
||||
// R. Levien
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/internal/scene"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
)
|
||||
|
||||
// strokeTolerance is used to reconcile rounding errors arising
|
||||
// when splitting quads into smaller and smaller segments to approximate
|
||||
// them into straight lines, and when joining back segments.
|
||||
//
|
||||
// The magic value of 0.01 was found by striking a compromise between
|
||||
// aesthetic looking (curves did look like curves, even after linearization)
|
||||
// and speed.
|
||||
const strokeTolerance = 0.01
|
||||
|
||||
type strokeQuad struct {
|
||||
contour uint32
|
||||
quad quadSegment
|
||||
}
|
||||
|
||||
type strokeState struct {
|
||||
p0, p1 f32.Point // p0 is the start point, p1 the end point.
|
||||
n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point.
|
||||
r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point.
|
||||
ctl f32.Point // ctl is the control point of the quadratic Bézier segment.
|
||||
}
|
||||
|
||||
type strokeQuads []strokeQuad
|
||||
|
||||
func (qs *strokeQuads) setContour(n uint32) {
|
||||
for i := range *qs {
|
||||
(*qs)[i].contour = n
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *strokeQuads) pen() f32.Point {
|
||||
return (*qs)[len(*qs)-1].quad.To
|
||||
}
|
||||
|
||||
func (qs *strokeQuads) closed() bool {
|
||||
beg := (*qs)[0].quad.From
|
||||
end := (*qs)[len(*qs)-1].quad.To
|
||||
return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y)
|
||||
}
|
||||
|
||||
func (qs *strokeQuads) lineTo(pt f32.Point) {
|
||||
end := qs.pen()
|
||||
*qs = append(*qs, strokeQuad{
|
||||
quad: quadSegment{
|
||||
From: end,
|
||||
Ctrl: end.Add(pt).Mul(0.5),
|
||||
To: pt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (qs *strokeQuads) arc(f1, f2 f32.Point, angle float32) {
|
||||
var (
|
||||
p clip.Path
|
||||
o = new(op.Ops)
|
||||
)
|
||||
p.Begin(o)
|
||||
p.Move(qs.pen())
|
||||
beg := len(o.Data())
|
||||
p.Arc(f1, f2, angle)
|
||||
end := len(o.Data())
|
||||
raw := o.Data()[beg:end]
|
||||
|
||||
for qi := 0; len(raw) >= (scene.CommandSize + 4); qi++ {
|
||||
quad := decodeQuad(raw[4:])
|
||||
raw = raw[scene.CommandSize+4:]
|
||||
*qs = append(*qs, strokeQuad{
|
||||
quad: quad,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// split splits a slice of quads into slices of quads grouped
|
||||
// by contours (ie: splitted at move-to boundaries).
|
||||
func (qs strokeQuads) split() []strokeQuads {
|
||||
if len(qs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
c uint32
|
||||
o []strokeQuads
|
||||
i = len(o)
|
||||
)
|
||||
for _, q := range qs {
|
||||
if q.contour != c {
|
||||
c = q.contour
|
||||
i = len(o)
|
||||
o = append(o, strokeQuads{})
|
||||
}
|
||||
o[i] = append(o[i], q)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (qs strokeQuads) stroke(stroke clip.StrokeStyle, dashes dashOp) strokeQuads {
|
||||
if !isSolidLine(dashes) {
|
||||
qs = qs.dash(dashes)
|
||||
}
|
||||
|
||||
var (
|
||||
o strokeQuads
|
||||
hw = 0.5 * stroke.Width
|
||||
)
|
||||
|
||||
for _, ps := range qs.split() {
|
||||
rhs, lhs := ps.offset(hw, stroke)
|
||||
switch lhs {
|
||||
case nil:
|
||||
o = o.append(rhs)
|
||||
default:
|
||||
// Closed path.
|
||||
// Inner path should go opposite direction to cancel outer path.
|
||||
switch {
|
||||
case ps.ccw():
|
||||
lhs = lhs.reverse()
|
||||
o = o.append(rhs)
|
||||
o = o.append(lhs)
|
||||
default:
|
||||
rhs = rhs.reverse()
|
||||
o = o.append(lhs)
|
||||
o = o.append(rhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// offset returns the right-hand and left-hand sides of the path, offset by
|
||||
// the half-width hw.
|
||||
// The stroke handles how segments are joined and ends are capped.
|
||||
func (qs strokeQuads) offset(hw float32, stroke clip.StrokeStyle) (rhs, lhs strokeQuads) {
|
||||
var (
|
||||
states []strokeState
|
||||
beg = qs[0].quad.From
|
||||
end = qs[len(qs)-1].quad.To
|
||||
closed = beg == end
|
||||
)
|
||||
for i := range qs {
|
||||
q := qs[i].quad
|
||||
|
||||
var (
|
||||
n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw)
|
||||
n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw)
|
||||
r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0)
|
||||
r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1)
|
||||
)
|
||||
states = append(states, strokeState{
|
||||
p0: q.From,
|
||||
p1: q.To,
|
||||
n0: n0,
|
||||
n1: n1,
|
||||
r0: r0,
|
||||
r1: r1,
|
||||
ctl: q.Ctrl,
|
||||
})
|
||||
}
|
||||
|
||||
for i, state := range states {
|
||||
rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance))
|
||||
lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance))
|
||||
|
||||
// join the current and next segments
|
||||
if hasNext := i+1 < len(states); hasNext || closed {
|
||||
var next strokeState
|
||||
switch {
|
||||
case hasNext:
|
||||
next = states[i+1]
|
||||
case closed:
|
||||
next = states[0]
|
||||
}
|
||||
if state.n1 != next.n0 {
|
||||
strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if closed {
|
||||
rhs.close()
|
||||
lhs.close()
|
||||
return rhs, lhs
|
||||
}
|
||||
|
||||
qbeg := &states[0]
|
||||
qend := &states[len(states)-1]
|
||||
|
||||
// Default to counter-clockwise direction.
|
||||
lhs = lhs.reverse()
|
||||
strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1)
|
||||
|
||||
rhs = rhs.append(lhs)
|
||||
strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
|
||||
|
||||
rhs.close()
|
||||
|
||||
return rhs, nil
|
||||
}
|
||||
|
||||
func (qs *strokeQuads) close() {
|
||||
p0 := (*qs)[len(*qs)-1].quad.To
|
||||
p1 := (*qs)[0].quad.From
|
||||
|
||||
if p1 == p0 {
|
||||
return
|
||||
}
|
||||
|
||||
*qs = append(*qs, strokeQuad{
|
||||
quad: quadSegment{
|
||||
From: p0,
|
||||
Ctrl: p0.Add(p1).Mul(0.5),
|
||||
To: p1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ccw returns whether the path is counter-clockwise.
|
||||
func (qs strokeQuads) ccw() bool {
|
||||
// Use the Shoelace formula:
|
||||
// https://en.wikipedia.org/wiki/Shoelace_formula
|
||||
var area float32
|
||||
for _, ps := range qs.split() {
|
||||
for i := 1; i < len(ps); i++ {
|
||||
pi := ps[i].quad.To
|
||||
pj := ps[i-1].quad.To
|
||||
area += (pi.X - pj.X) * (pi.Y + pj.Y)
|
||||
}
|
||||
}
|
||||
return area <= 0.0
|
||||
}
|
||||
|
||||
func (qs strokeQuads) reverse() strokeQuads {
|
||||
if len(qs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ps := make(strokeQuads, 0, len(qs))
|
||||
for i := range qs {
|
||||
q := qs[len(qs)-1-i]
|
||||
q.quad.To, q.quad.From = q.quad.From, q.quad.To
|
||||
ps = append(ps, q)
|
||||
}
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
func (qs strokeQuads) append(ps strokeQuads) strokeQuads {
|
||||
switch {
|
||||
case len(ps) == 0:
|
||||
return qs
|
||||
case len(qs) == 0:
|
||||
return ps
|
||||
}
|
||||
|
||||
// Consolidate quads and smooth out rounding errors.
|
||||
// We need to also check for the strokeTolerance to correctly handle
|
||||
// join/cap points or on-purpose disjoint quads.
|
||||
p0 := qs[len(qs)-1].quad.To
|
||||
p1 := ps[0].quad.From
|
||||
if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance {
|
||||
qs = append(qs, strokeQuad{
|
||||
quad: quadSegment{
|
||||
From: p0,
|
||||
Ctrl: p0.Add(p1).Mul(0.5),
|
||||
To: p1,
|
||||
},
|
||||
})
|
||||
}
|
||||
return append(qs, ps...)
|
||||
}
|
||||
|
||||
// strokePathNorm returns the normal vector at t.
|
||||
func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
|
||||
switch t {
|
||||
case 0:
|
||||
n := p1.Sub(p0)
|
||||
if n.X == 0 && n.Y == 0 {
|
||||
return f32.Point{}
|
||||
}
|
||||
n = rot90CW(n)
|
||||
return normPt(n, d)
|
||||
case 1:
|
||||
n := p2.Sub(p1)
|
||||
if n.X == 0 && n.Y == 0 {
|
||||
return f32.Point{}
|
||||
}
|
||||
n = rot90CW(n)
|
||||
return normPt(n, d)
|
||||
}
|
||||
panic("impossible")
|
||||
}
|
||||
|
||||
func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
|
||||
func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) }
|
||||
|
||||
// cosPt returns the cosine of the opening angle between p and q.
|
||||
func cosPt(p, q f32.Point) float32 {
|
||||
np := math.Hypot(float64(p.X), float64(p.Y))
|
||||
nq := math.Hypot(float64(q.X), float64(q.Y))
|
||||
return dotPt(p, q) / float32(np*nq)
|
||||
}
|
||||
|
||||
func normPt(p f32.Point, l float32) f32.Point {
|
||||
d := math.Hypot(float64(p.X), float64(p.Y))
|
||||
l64 := float64(l)
|
||||
if math.Abs(d-l64) < 1e-10 {
|
||||
return f32.Point{}
|
||||
}
|
||||
n := float32(l64 / d)
|
||||
return f32.Point{X: p.X * n, Y: p.Y * n}
|
||||
}
|
||||
|
||||
func lenPt(p f32.Point) float32 {
|
||||
return float32(math.Hypot(float64(p.X), float64(p.Y)))
|
||||
}
|
||||
|
||||
func dotPt(p, q f32.Point) float32 {
|
||||
return p.X*q.X + p.Y*q.Y
|
||||
}
|
||||
|
||||
func perpDot(p, q f32.Point) float32 {
|
||||
return p.X*q.Y - p.Y*q.X
|
||||
}
|
||||
|
||||
// strokePathCurv returns the curvature at t, along the quadratic Bézier
|
||||
// curve defined by the triplet (beg, ctl, end).
|
||||
func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 {
|
||||
var (
|
||||
d1p = quadBezierD1(beg, ctl, end, t)
|
||||
d2p = quadBezierD2(beg, ctl, end, t)
|
||||
|
||||
// Negative when bending right, ie: the curve is CW at this point.
|
||||
a = float64(perpDot(d1p, d2p))
|
||||
)
|
||||
|
||||
// We check early that the segment isn't too line-like and
|
||||
// save a costly call to math.Pow that will be discarded by dividing
|
||||
// with a too small 'a'.
|
||||
if math.Abs(a) < 1e-10 {
|
||||
return float32(math.NaN())
|
||||
}
|
||||
return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a)
|
||||
}
|
||||
|
||||
// quadBezierSample returns the point on the Bézier curve at t.
|
||||
// B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2
|
||||
func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point {
|
||||
t1 := 1 - t
|
||||
c0 := t1 * t1
|
||||
c1 := 2 * t1 * t
|
||||
c2 := t * t
|
||||
|
||||
o := p0.Mul(c0)
|
||||
o = o.Add(p1.Mul(c1))
|
||||
o = o.Add(p2.Mul(c2))
|
||||
return o
|
||||
}
|
||||
|
||||
// quadBezierD1 returns the first derivative of the Bézier curve with respect to t.
|
||||
// B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1)
|
||||
func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point {
|
||||
p10 := p1.Sub(p0).Mul(2 * (1 - t))
|
||||
p21 := p2.Sub(p1).Mul(2 * t)
|
||||
|
||||
return p10.Add(p21)
|
||||
}
|
||||
|
||||
// quadBezierD2 returns the second derivative of the Bézier curve with respect to t:
|
||||
// B''(t) = 2(P2 - 2P1 + P0)
|
||||
func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
|
||||
p := p2.Sub(p1.Mul(2)).Add(p0)
|
||||
return p.Mul(2)
|
||||
}
|
||||
|
||||
// quadBezierLen returns the length of the Bézier curve.
|
||||
// See:
|
||||
// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/
|
||||
func quadBezierLen(p0, p1, p2 f32.Point) float32 {
|
||||
a := p0.Sub(p1.Mul(2)).Add(p2)
|
||||
b := p1.Mul(2).Sub(p0.Mul(2))
|
||||
A := float64(4 * dotPt(a, a))
|
||||
B := float64(4 * dotPt(a, b))
|
||||
C := float64(dotPt(b, b))
|
||||
if f64Eq(A, 0.0) {
|
||||
// p1 is in the middle between p0 and p2,
|
||||
// so it is a straight line from p0 to p2.
|
||||
return lenPt(p2.Sub(p0))
|
||||
}
|
||||
|
||||
Sabc := 2 * math.Sqrt(A+B+C)
|
||||
A2 := math.Sqrt(A)
|
||||
A32 := 2 * A * A2
|
||||
C2 := 2 * math.Sqrt(C)
|
||||
BA := B / A2
|
||||
return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32))
|
||||
}
|
||||
|
||||
func strokeQuadBezier(state strokeState, d, flatness float32) strokeQuads {
|
||||
// Gio strokes are only quadratic Bézier curves, w/o any inflection point.
|
||||
// So we just have to flatten them.
|
||||
var qs strokeQuads
|
||||
return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness)
|
||||
}
|
||||
|
||||
// flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments,
|
||||
// themselves also encoded as Bézier (degenerate, flat) quadratic curves.
|
||||
func flattenQuadBezier(qs strokeQuads, p0, p1, p2 f32.Point, d, flatness float32) strokeQuads {
|
||||
var (
|
||||
t float32
|
||||
flat64 = float64(flatness)
|
||||
)
|
||||
for t < 1 {
|
||||
s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X))
|
||||
den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y))
|
||||
if s2*den == 0.0 {
|
||||
break
|
||||
}
|
||||
|
||||
s2 /= den
|
||||
t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2)))
|
||||
if t >= 1.0 {
|
||||
break
|
||||
}
|
||||
var q0, q1, q2 f32.Point
|
||||
q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t)
|
||||
qs.addLine(q0, q1, q2, 0, d)
|
||||
}
|
||||
qs.addLine(p0, p1, p2, 1, d)
|
||||
return qs
|
||||
}
|
||||
|
||||
func (qs *strokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) {
|
||||
|
||||
switch i := len(*qs); i {
|
||||
case 0:
|
||||
p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d))
|
||||
default:
|
||||
// Address possible rounding errors and use previous point.
|
||||
p0 = (*qs)[i-1].quad.To
|
||||
}
|
||||
|
||||
p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d))
|
||||
|
||||
*qs = append(*qs,
|
||||
strokeQuad{
|
||||
quad: quadSegment{
|
||||
From: p0,
|
||||
Ctrl: p0.Add(p1).Mul(0.5),
|
||||
To: p1,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// quadInterp returns the interpolated point at t.
|
||||
func quadInterp(p, q f32.Point, t float32) f32.Point {
|
||||
return f32.Pt(
|
||||
(1-t)*p.X+t*q.X,
|
||||
(1-t)*p.Y+t*q.Y,
|
||||
)
|
||||
}
|
||||
|
||||
// quadBezierSplit returns the pair of triplets (from,ctrl,to) Bézier curve,
|
||||
// split before (resp. after) the provided parametric t value.
|
||||
func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32.Point, f32.Point, f32.Point, f32.Point) {
|
||||
|
||||
var (
|
||||
b0 = p0
|
||||
b1 = quadInterp(p0, p1, t)
|
||||
b2 = quadBezierSample(p0, p1, p2, t)
|
||||
|
||||
a0 = b2
|
||||
a1 = quadInterp(p1, p2, t)
|
||||
a2 = p2
|
||||
)
|
||||
|
||||
return b0, b1, b2, a0, a1, a2
|
||||
}
|
||||
|
||||
// strokePathJoin joins the two paths rhs and lhs, according to the provided
|
||||
// stroke operation.
|
||||
func strokePathJoin(stroke clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
|
||||
if stroke.Miter > 0 {
|
||||
strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
|
||||
return
|
||||
}
|
||||
switch stroke.Join {
|
||||
case clip.BevelJoin:
|
||||
strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
|
||||
case clip.RoundJoin:
|
||||
strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
|
||||
default:
|
||||
panic("impossible")
|
||||
}
|
||||
}
|
||||
|
||||
func strokePathBevelJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
|
||||
|
||||
rp := pivot.Add(n1)
|
||||
lp := pivot.Sub(n1)
|
||||
|
||||
rhs.lineTo(rp)
|
||||
lhs.lineTo(lp)
|
||||
}
|
||||
|
||||
func strokePathRoundJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
|
||||
rp := pivot.Add(n1)
|
||||
lp := pivot.Sub(n1)
|
||||
cw := dotPt(rot90CW(n0), n1) >= 0.0
|
||||
switch {
|
||||
case cw:
|
||||
// Path bends to the right, ie. CW (or 180 degree turn).
|
||||
c := pivot.Sub(lhs.pen())
|
||||
angle := -math.Acos(float64(cosPt(n0, n1)))
|
||||
lhs.arc(c, c, float32(angle))
|
||||
lhs.lineTo(lp) // Add a line to accommodate for rounding errors.
|
||||
rhs.lineTo(rp)
|
||||
default:
|
||||
// Path bends to the left, ie. CCW.
|
||||
angle := math.Acos(float64(cosPt(n0, n1)))
|
||||
c := pivot.Sub(rhs.pen())
|
||||
rhs.arc(c, c, float32(angle))
|
||||
rhs.lineTo(rp) // Add a line to accommodate for rounding errors.
|
||||
lhs.lineTo(lp)
|
||||
}
|
||||
}
|
||||
|
||||
func strokePathMiterJoin(stroke clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
|
||||
if n0 == n1.Mul(-1) {
|
||||
strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
|
||||
return
|
||||
}
|
||||
|
||||
// This is to handle nearly linear joints that would be clipped otherwise.
|
||||
limit := math.Max(float64(stroke.Miter), 1.001)
|
||||
|
||||
cw := dotPt(rot90CW(n0), n1) >= 0.0
|
||||
if cw {
|
||||
// hw is used to calculate |R|.
|
||||
// When running CW, n0 and n1 point the other way,
|
||||
// so the sign of r0 and r1 is negated.
|
||||
hw = -hw
|
||||
}
|
||||
hw64 := float64(hw)
|
||||
|
||||
cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1))))
|
||||
d := hw64 / cos
|
||||
if math.Abs(limit*hw64) < math.Abs(d) {
|
||||
stroke.Miter = 0 // Set miter to zero to disable the miter joint.
|
||||
strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
|
||||
return
|
||||
}
|
||||
mid := pivot.Add(normPt(n0.Add(n1), float32(d)))
|
||||
|
||||
rp := pivot.Add(n1)
|
||||
lp := pivot.Sub(n1)
|
||||
switch {
|
||||
case cw:
|
||||
// Path bends to the right, ie. CW.
|
||||
lhs.lineTo(mid)
|
||||
default:
|
||||
// Path bends to the left, ie. CCW.
|
||||
rhs.lineTo(mid)
|
||||
}
|
||||
rhs.lineTo(rp)
|
||||
lhs.lineTo(lp)
|
||||
}
|
||||
|
||||
// strokePathCap caps the provided path qs, according to the provided stroke operation.
|
||||
func strokePathCap(stroke clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
|
||||
switch stroke.Cap {
|
||||
case clip.FlatCap:
|
||||
strokePathFlatCap(qs, hw, pivot, n0)
|
||||
case clip.SquareCap:
|
||||
strokePathSquareCap(qs, hw, pivot, n0)
|
||||
case clip.RoundCap:
|
||||
strokePathRoundCap(qs, hw, pivot, n0)
|
||||
default:
|
||||
panic("impossible")
|
||||
}
|
||||
}
|
||||
|
||||
// strokePathFlatCap caps the start or end of a path with a flat cap.
|
||||
func strokePathFlatCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
|
||||
end := pivot.Sub(n0)
|
||||
qs.lineTo(end)
|
||||
}
|
||||
|
||||
// strokePathSquareCap caps the start or end of a path with a square cap.
|
||||
func strokePathSquareCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
|
||||
var (
|
||||
e = pivot.Add(rot90CCW(n0))
|
||||
corner1 = e.Add(n0)
|
||||
corner2 = e.Sub(n0)
|
||||
end = pivot.Sub(n0)
|
||||
)
|
||||
|
||||
qs.lineTo(corner1)
|
||||
qs.lineTo(corner2)
|
||||
qs.lineTo(end)
|
||||
}
|
||||
|
||||
// strokePathRoundCap caps the start or end of a path with a round cap.
|
||||
func strokePathRoundCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
|
||||
c := pivot.Sub(qs.pen())
|
||||
qs.arc(c, c, math.Pi)
|
||||
}
|
||||
Reference in New Issue
Block a user