forked from joejulian/gio
op/clip: remove complex stroke support
In a discussion with Raph Levien, the author of our compute renderer implementation, it became clear to me that it's not at all certain that complex strokes will ever be efficiently supported by a GPU renderer. At the same time, the machinery for converting a complex stroke to a GPU-friendly outline has a significant maintenance cost. Further, it is surprising to users that complex strokes are significantly slower and allocate memory. This change removes support for complex strokes, leaving only round-capped, round-joined strokes supported by the compute renderer. The default renderer still converts all strokes to outline, but it also caches the result. This is an API change. The complex stroke conversion code has been moved to the external gioui.org/x/stroke package, with a similar API. Updats gio#282 (Inkeliz brought up the allocation issue) Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -1,394 +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 stroke
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"gioui.org/f32"
|
||||
)
|
||||
|
||||
type DashOp struct {
|
||||
Phase float32
|
||||
Dashes []float32
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
+5
-146
@@ -40,26 +40,8 @@ import (
|
||||
// op/clip, eliminating the duplicate types.
|
||||
type StrokeStyle struct {
|
||||
Width float32
|
||||
Miter float32
|
||||
Cap StrokeCap
|
||||
Join StrokeJoin
|
||||
}
|
||||
|
||||
type StrokeCap uint8
|
||||
|
||||
const (
|
||||
RoundCap StrokeCap = iota
|
||||
FlatCap
|
||||
SquareCap
|
||||
)
|
||||
|
||||
type StrokeJoin uint8
|
||||
|
||||
const (
|
||||
RoundJoin StrokeJoin = iota
|
||||
BevelJoin
|
||||
)
|
||||
|
||||
// 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.
|
||||
@@ -97,12 +79,6 @@ 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{
|
||||
@@ -155,11 +131,7 @@ func (qs StrokeQuads) split() []StrokeQuads {
|
||||
return o
|
||||
}
|
||||
|
||||
func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads {
|
||||
if !IsSolidLine(dashes) {
|
||||
qs = qs.dash(dashes)
|
||||
}
|
||||
|
||||
func (qs StrokeQuads) stroke(stroke StrokeStyle) StrokeQuads {
|
||||
var (
|
||||
o StrokeQuads
|
||||
hw = 0.5 * stroke.Width
|
||||
@@ -441,29 +413,6 @@ func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
|
||||
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.
|
||||
@@ -549,27 +498,7 @@ func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32
|
||||
// strokePathJoin joins the two paths rhs and lhs, according to the provided
|
||||
// stroke operation.
|
||||
func strokePathJoin(stroke 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 BevelJoin:
|
||||
strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
|
||||
case 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)
|
||||
strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
|
||||
}
|
||||
|
||||
func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
|
||||
@@ -594,79 +523,9 @@ func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Po
|
||||
}
|
||||
}
|
||||
|
||||
func strokePathMiterJoin(stroke 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 StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
|
||||
switch stroke.Cap {
|
||||
case FlatCap:
|
||||
strokePathFlatCap(qs, hw, pivot, n0)
|
||||
case SquareCap:
|
||||
strokePathSquareCap(qs, hw, pivot, n0)
|
||||
case 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(qs, hw, pivot, n0)
|
||||
}
|
||||
|
||||
// strokePathRoundCap caps the start or end of a path with a round cap.
|
||||
@@ -763,9 +622,9 @@ func dist(p1, p2 f32.Point) float64 {
|
||||
return math.Hypot(dx, dy)
|
||||
}
|
||||
|
||||
func StrokePathCommands(style StrokeStyle, dashes DashOp, scene []byte) StrokeQuads {
|
||||
func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads {
|
||||
quads := decodeToStrokeQuads(scene)
|
||||
return quads.stroke(style, dashes)
|
||||
return quads.stroke(style)
|
||||
}
|
||||
|
||||
// decodeToStrokeQuads decodes scene commands to quads ready to stroke.
|
||||
|
||||
Reference in New Issue
Block a user