mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 16:35:36 +00:00
gpu,op/clip: implement dashed stroked paths
Signed-off-by: Sebastien Binet <s@sbinet.org>
This commit is contained in:
committed by
Elias Naur
parent
ac14320bec
commit
e71bf13c9a
+390
@@ -0,0 +1,390 @@
|
||||
// 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"
|
||||
"gioui.org/internal/ops"
|
||||
)
|
||||
|
||||
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: ops.Quad{
|
||||
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: ops.Quad{
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user