mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 01:45: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)
|
||||||
|
}
|
||||||
+40
-3
@@ -109,6 +109,23 @@ type imageOp struct {
|
|||||||
place placement
|
place placement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dashOp struct {
|
||||||
|
phase float32
|
||||||
|
dashes []float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDashOp(data []byte) 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]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func decodeStrokeOp(data []byte) clip.StrokeStyle {
|
func decodeStrokeOp(data []byte) clip.StrokeStyle {
|
||||||
_ = data[10]
|
_ = data[10]
|
||||||
if opconst.OpType(data[0]) != opconst.TypeStroke {
|
if opconst.OpType(data[0]) != opconst.TypeStroke {
|
||||||
@@ -812,6 +829,7 @@ func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
|
|||||||
var (
|
var (
|
||||||
quads quadsOp
|
quads quadsOp
|
||||||
stroke clip.StrokeStyle
|
stroke clip.StrokeStyle
|
||||||
|
dashes dashOp
|
||||||
)
|
)
|
||||||
loop:
|
loop:
|
||||||
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
||||||
@@ -822,6 +840,22 @@ loop:
|
|||||||
dop := ops.DecodeTransform(encOp.Data)
|
dop := ops.DecodeTransform(encOp.Data)
|
||||||
state.t = state.t.Mul(dop)
|
state.t = state.t.Mul(dop)
|
||||||
|
|
||||||
|
case opconst.TypeDash:
|
||||||
|
dashes = decodeDashOp(encOp.Data)
|
||||||
|
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(
|
||||||
|
data[i*4:],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case opconst.TypeStroke:
|
case opconst.TypeStroke:
|
||||||
stroke = decodeStrokeOp(encOp.Data)
|
stroke = decodeStrokeOp(encOp.Data)
|
||||||
|
|
||||||
@@ -851,7 +885,9 @@ loop:
|
|||||||
// Why is this not used for the offset shapes?
|
// Why is this not used for the offset shapes?
|
||||||
op.bounds = v.bounds
|
op.bounds = v.bounds
|
||||||
} else {
|
} else {
|
||||||
quads.aux, op.bounds = d.buildVerts(quads.aux, trans, op.outline, stroke)
|
quads.aux, op.bounds = d.buildVerts(
|
||||||
|
quads.aux, trans, op.outline, stroke, dashes,
|
||||||
|
)
|
||||||
// add it to the cache, without GPU data, so the transform can be
|
// add it to the cache, without GPU data, so the transform can be
|
||||||
// reused.
|
// reused.
|
||||||
d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds})
|
d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds})
|
||||||
@@ -865,6 +901,7 @@ loop:
|
|||||||
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off)
|
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off)
|
||||||
quads = quadsOp{}
|
quads = quadsOp{}
|
||||||
stroke = clip.StrokeStyle{}
|
stroke = clip.StrokeStyle{}
|
||||||
|
dashes = dashOp{}
|
||||||
|
|
||||||
case opconst.TypeColor:
|
case opconst.TypeColor:
|
||||||
state.matType = materialColor
|
state.matType = materialColor
|
||||||
@@ -1251,7 +1288,7 @@ func (d *drawOps) writeVertCache(n int) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
|
// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
|
||||||
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) {
|
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle, dashes dashOp) (verts []byte, bounds f32.Rectangle) {
|
||||||
inf := float32(math.Inf(+1))
|
inf := float32(math.Inf(+1))
|
||||||
d.qs.bounds = f32.Rectangle{
|
d.qs.bounds = f32.Rectangle{
|
||||||
Min: f32.Point{X: inf, Y: inf},
|
Min: f32.Point{X: inf, Y: inf},
|
||||||
@@ -1273,7 +1310,7 @@ func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, outline bool, stroke c
|
|||||||
quads = append(quads, quad)
|
quads = append(quads, quad)
|
||||||
aux = aux[ops.QuadSize+4:]
|
aux = aux[ops.QuadSize+4:]
|
||||||
}
|
}
|
||||||
quads = quads.stroke(stroke)
|
quads = quads.stroke(stroke, dashes)
|
||||||
for _, quad := range quads {
|
for _, quad := range quads {
|
||||||
d.qs.contour = quad.contour
|
d.qs.contour = quad.contour
|
||||||
quad.quad = quad.quad.Transform(tr)
|
quad.quad = quad.quad.Transform(tr)
|
||||||
|
|||||||
+44
-1
@@ -46,10 +46,22 @@ type strokeState struct {
|
|||||||
|
|
||||||
type strokeQuads []strokeQuad
|
type strokeQuads []strokeQuad
|
||||||
|
|
||||||
|
func (qs *strokeQuads) setContour(n uint32) {
|
||||||
|
for i := range *qs {
|
||||||
|
(*qs)[i].contour = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qs *strokeQuads) pen() f32.Point {
|
func (qs *strokeQuads) pen() f32.Point {
|
||||||
return (*qs)[len(*qs)-1].quad.To
|
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) {
|
func (qs *strokeQuads) lineTo(pt f32.Point) {
|
||||||
end := qs.pen()
|
end := qs.pen()
|
||||||
*qs = append(*qs, strokeQuad{
|
*qs = append(*qs, strokeQuad{
|
||||||
@@ -106,7 +118,11 @@ func (qs strokeQuads) split() []strokeQuads {
|
|||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qs strokeQuads) stroke(stroke clip.StrokeStyle) strokeQuads {
|
func (qs strokeQuads) stroke(stroke clip.StrokeStyle, dashes dashOp) strokeQuads {
|
||||||
|
if !isSolidLine(dashes) {
|
||||||
|
qs = qs.dash(dashes)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
o strokeQuads
|
o strokeQuads
|
||||||
hw = 0.5 * stroke.Width
|
hw = 0.5 * stroke.Width
|
||||||
@@ -291,6 +307,10 @@ func normPt(p f32.Point, l float32) f32.Point {
|
|||||||
return f32.Point{X: p.X * n, Y: p.Y * n}
|
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 {
|
func dotPt(p, q f32.Point) float32 {
|
||||||
return p.X*q.X + p.Y*q.Y
|
return p.X*q.X + p.Y*q.Y
|
||||||
}
|
}
|
||||||
@@ -349,6 +369,29 @@ func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
|
|||||||
return p.Mul(2)
|
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 {
|
func strokeQuadBezier(state strokeState, d, flatness float32) strokeQuads {
|
||||||
// Gio strokes are only quadratic Bézier curves, w/o any inflection point.
|
// Gio strokes are only quadratic Bézier curves, w/o any inflection point.
|
||||||
// So we just have to flatten them.
|
// So we just have to flatten them.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const (
|
|||||||
TypeCursor
|
TypeCursor
|
||||||
TypePath
|
TypePath
|
||||||
TypeStroke
|
TypeStroke
|
||||||
|
TypeDash
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -61,6 +62,7 @@ const (
|
|||||||
TypeCursorLen = 1 + 1
|
TypeCursorLen = 1 + 1
|
||||||
TypePathLen = 1 + 4
|
TypePathLen = 1 + 4
|
||||||
TypeStrokeLen = 1 + 4 + 4 + 1 + 1
|
TypeStrokeLen = 1 + 4 + 4 + 1 + 1
|
||||||
|
TypeDashLen = 1 + 4 + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t OpType) Size() int {
|
func (t OpType) Size() int {
|
||||||
@@ -90,6 +92,7 @@ func (t OpType) Size() int {
|
|||||||
TypeCursorLen,
|
TypeCursorLen,
|
||||||
TypePathLen,
|
TypePathLen,
|
||||||
TypeStrokeLen,
|
TypeStrokeLen,
|
||||||
|
TypeDashLen,
|
||||||
}[t-firstOpIndex]
|
}[t-firstOpIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,28 @@ func TestPaintClippedRect(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPaintClippedBorder(t *testing.T) {
|
||||||
|
run(t, func(o *op.Ops) {
|
||||||
|
var dashes clip.Dash
|
||||||
|
dashes.Begin(o)
|
||||||
|
dashes.Phase(1)
|
||||||
|
dashes.Dash(2)
|
||||||
|
dashes.Dash(1)
|
||||||
|
|
||||||
|
clip.Border{
|
||||||
|
Rect: f32.Rect(25, 25, 60, 60),
|
||||||
|
Width: 4,
|
||||||
|
Dashes: dashes.End(),
|
||||||
|
}.Add(o)
|
||||||
|
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
|
||||||
|
}, func(r result) {
|
||||||
|
r.expect(0, 0, colornames.White)
|
||||||
|
r.expect(25, 25, colornames.Red)
|
||||||
|
r.expect(50, 0, colornames.White)
|
||||||
|
r.expect(10, 50, colornames.White)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPaintClippedCirle(t *testing.T) {
|
func TestPaintClippedCirle(t *testing.T) {
|
||||||
run(t, func(o *op.Ops) {
|
run(t, func(o *op.Ops) {
|
||||||
r := float32(10)
|
r := float32(10)
|
||||||
@@ -318,6 +340,177 @@ func TestStrokedPathZeroWidth(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDashedPathFlatCapEllipse(t *testing.T) {
|
||||||
|
run(t, func(o *op.Ops) {
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
p := newEllipsePath(o)
|
||||||
|
|
||||||
|
var dash clip.Dash
|
||||||
|
dash.Begin(o)
|
||||||
|
dash.Dash(5)
|
||||||
|
dash.Dash(3)
|
||||||
|
|
||||||
|
clip.Stroke{
|
||||||
|
Path: p,
|
||||||
|
Style: clip.StrokeStyle{
|
||||||
|
Width: 10,
|
||||||
|
Cap: clip.FlatCap,
|
||||||
|
Join: clip.BevelJoin,
|
||||||
|
Miter: float32(math.Inf(+1)),
|
||||||
|
},
|
||||||
|
Dashes: dash.End(),
|
||||||
|
}.Op().Add(o)
|
||||||
|
|
||||||
|
paint.Fill(
|
||||||
|
o,
|
||||||
|
red,
|
||||||
|
)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
p := newEllipsePath(o)
|
||||||
|
clip.Stroke{
|
||||||
|
Path: p,
|
||||||
|
Style: clip.StrokeStyle{
|
||||||
|
Width: 2,
|
||||||
|
},
|
||||||
|
}.Op().Add(o)
|
||||||
|
|
||||||
|
paint.Fill(
|
||||||
|
o,
|
||||||
|
black,
|
||||||
|
)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
}, func(r result) {
|
||||||
|
r.expect(0, 0, colornames.White)
|
||||||
|
r.expect(0, 62, colornames.Red)
|
||||||
|
r.expect(0, 65, colornames.Black)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashedPathFlatCapZ(t *testing.T) {
|
||||||
|
run(t, func(o *op.Ops) {
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
p := newZigZagPath(o)
|
||||||
|
var dash clip.Dash
|
||||||
|
dash.Begin(o)
|
||||||
|
dash.Dash(5)
|
||||||
|
dash.Dash(3)
|
||||||
|
|
||||||
|
clip.Stroke{
|
||||||
|
Path: p,
|
||||||
|
Style: clip.StrokeStyle{
|
||||||
|
Width: 10,
|
||||||
|
Cap: clip.FlatCap,
|
||||||
|
Join: clip.BevelJoin,
|
||||||
|
Miter: float32(math.Inf(+1)),
|
||||||
|
},
|
||||||
|
Dashes: dash.End(),
|
||||||
|
}.Op().Add(o)
|
||||||
|
paint.Fill(o, red)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
p := newZigZagPath(o)
|
||||||
|
clip.Stroke{
|
||||||
|
Path: p,
|
||||||
|
Style: clip.StrokeStyle{Width: 2},
|
||||||
|
}.Op().Add(o)
|
||||||
|
paint.Fill(o, black)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
}, func(r result) {
|
||||||
|
r.expect(0, 0, colornames.White)
|
||||||
|
r.expect(40, 10, colornames.Black)
|
||||||
|
r.expect(40, 12, colornames.Red)
|
||||||
|
r.expect(46, 12, colornames.White)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashedPathFlatCapZNoDash(t *testing.T) {
|
||||||
|
run(t, func(o *op.Ops) {
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
p := newZigZagPath(o)
|
||||||
|
var dash clip.Dash
|
||||||
|
dash.Begin(o)
|
||||||
|
dash.Phase(1)
|
||||||
|
|
||||||
|
clip.Stroke{
|
||||||
|
Path: p,
|
||||||
|
Style: clip.StrokeStyle{
|
||||||
|
Width: 10,
|
||||||
|
Cap: clip.FlatCap,
|
||||||
|
Join: clip.BevelJoin,
|
||||||
|
Miter: float32(math.Inf(+1)),
|
||||||
|
},
|
||||||
|
Dashes: dash.End(),
|
||||||
|
}.Op().Add(o)
|
||||||
|
paint.Fill(o, red)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
clip.Stroke{
|
||||||
|
Path: newZigZagPath(o),
|
||||||
|
Style: clip.StrokeStyle{Width: 2},
|
||||||
|
}.Op().Add(o)
|
||||||
|
paint.Fill(o, black)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
}, func(r result) {
|
||||||
|
r.expect(0, 0, colornames.White)
|
||||||
|
r.expect(40, 10, colornames.Black)
|
||||||
|
r.expect(40, 12, colornames.Red)
|
||||||
|
r.expect(46, 12, colornames.Red)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashedPathFlatCapZNoPath(t *testing.T) {
|
||||||
|
run(t, func(o *op.Ops) {
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
var dash clip.Dash
|
||||||
|
dash.Begin(o)
|
||||||
|
dash.Dash(0)
|
||||||
|
clip.Stroke{
|
||||||
|
Path: newZigZagPath(o),
|
||||||
|
Style: clip.StrokeStyle{
|
||||||
|
Width: 10,
|
||||||
|
Cap: clip.FlatCap,
|
||||||
|
Join: clip.BevelJoin,
|
||||||
|
Miter: float32(math.Inf(+1)),
|
||||||
|
},
|
||||||
|
Dashes: dash.End(),
|
||||||
|
}.Op().Add(o)
|
||||||
|
paint.Fill(o, red)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
{
|
||||||
|
stk := op.Push(o)
|
||||||
|
p := newZigZagPath(o)
|
||||||
|
clip.Stroke{
|
||||||
|
Path: p,
|
||||||
|
Style: clip.StrokeStyle{Width: 2},
|
||||||
|
}.Op().Add(o)
|
||||||
|
paint.Fill(o, black)
|
||||||
|
stk.Pop()
|
||||||
|
}
|
||||||
|
}, func(r result) {
|
||||||
|
r.expect(0, 0, colornames.White)
|
||||||
|
r.expect(40, 10, colornames.Black)
|
||||||
|
r.expect(40, 12, colornames.White)
|
||||||
|
r.expect(46, 12, colornames.White)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func newStrokedPath(o *op.Ops) clip.PathSpec {
|
func newStrokedPath(o *op.Ops) clip.PathSpec {
|
||||||
p := new(clip.Path)
|
p := new(clip.Path)
|
||||||
p.Begin(o)
|
p.Begin(o)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 582 B |
@@ -1,10 +1,11 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
"gioui.org/io/clipboard"
|
"gioui.org/io/clipboard"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClipboardDuplicateEvent(t *testing.T) {
|
func TestClipboardDuplicateEvent(t *testing.T) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Op struct {
|
|||||||
|
|
||||||
outline bool
|
outline bool
|
||||||
stroke StrokeStyle
|
stroke StrokeStyle
|
||||||
|
dashes DashSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Op) Add(o *op.Ops) {
|
func (p Op) Add(o *op.Ops) {
|
||||||
@@ -42,6 +43,15 @@ func (p Op) Add(o *op.Ops) {
|
|||||||
data[10] = uint8(p.stroke.Join)
|
data[10] = uint8(p.stroke.Join)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.dashes.phase != 0 || p.dashes.size > 0 {
|
||||||
|
data := o.Write(opconst.TypeDashLen)
|
||||||
|
data[0] = byte(opconst.TypeDash)
|
||||||
|
bo := binary.LittleEndian
|
||||||
|
bo.PutUint32(data[1:], math.Float32bits(p.dashes.phase))
|
||||||
|
data[5] = p.dashes.size // FIXME(sbinet) uint16? uint32?
|
||||||
|
p.dashes.spec.Add(o)
|
||||||
|
}
|
||||||
|
|
||||||
data := o.Write(opconst.TypeClipLen)
|
data := o.Write(opconst.TypeClipLen)
|
||||||
data[0] = byte(opconst.TypeClip)
|
data[0] = byte(opconst.TypeClip)
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
|
|||||||
+4
-2
@@ -50,8 +50,9 @@ func (rr RRect) Add(ops *op.Ops) {
|
|||||||
// Border represents the clip area of a rectangular border.
|
// Border represents the clip area of a rectangular border.
|
||||||
type Border struct {
|
type Border struct {
|
||||||
// Rect is the bounds of the border.
|
// Rect is the bounds of the border.
|
||||||
Rect f32.Rectangle
|
Rect f32.Rectangle
|
||||||
Width float32
|
Width float32
|
||||||
|
Dashes DashSpec
|
||||||
// The corner radii.
|
// The corner radii.
|
||||||
SE, SW, NW, NE float32
|
SE, SW, NW, NE float32
|
||||||
}
|
}
|
||||||
@@ -68,6 +69,7 @@ func (b Border) Op(ops *op.Ops) Op {
|
|||||||
Style: StrokeStyle{
|
Style: StrokeStyle{
|
||||||
Width: b.Width,
|
Width: b.Width,
|
||||||
},
|
},
|
||||||
|
Dashes: b.Dashes,
|
||||||
}.Op()
|
}.Op()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
|
|
||||||
package clip
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"gioui.org/internal/opconst"
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
// Stroke represents a stroked path.
|
// Stroke represents a stroked path.
|
||||||
type Stroke struct {
|
type Stroke struct {
|
||||||
Path PathSpec
|
Path PathSpec
|
||||||
Style StrokeStyle
|
Style StrokeStyle
|
||||||
|
|
||||||
|
// Dashes specify the dashes of the stroke.
|
||||||
|
// The empty value denotes no dashes.
|
||||||
|
Dashes DashSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Op returns a clip operation representing the stroke.
|
// Op returns a clip operation representing the stroke.
|
||||||
@@ -13,6 +25,7 @@ func (s Stroke) Op() Op {
|
|||||||
return Op{
|
return Op{
|
||||||
path: s.Path,
|
path: s.Path,
|
||||||
stroke: s.Style,
|
stroke: s.Style,
|
||||||
|
dashes: s.Dashes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,3 +70,49 @@ const (
|
|||||||
// RoundJoin joins path segments with a round segment.
|
// RoundJoin joins path segments with a round segment.
|
||||||
RoundJoin
|
RoundJoin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Dash records dashes' lengths and phase for a stroked path.
|
||||||
|
type Dash struct {
|
||||||
|
ops *op.Ops
|
||||||
|
macro op.MacroOp
|
||||||
|
phase float32
|
||||||
|
size uint8 // size of the pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dash) Begin(ops *op.Ops) {
|
||||||
|
d.ops = ops
|
||||||
|
d.macro = op.Record(ops)
|
||||||
|
// Write the TypeAux opcode
|
||||||
|
data := ops.Write(opconst.TypeAuxLen)
|
||||||
|
data[0] = byte(opconst.TypeAux)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dash) Phase(v float32) {
|
||||||
|
d.phase = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dash) Dash(length float32) {
|
||||||
|
if d.size == math.MaxUint8 {
|
||||||
|
panic("clip: too large dash pattern")
|
||||||
|
}
|
||||||
|
data := d.ops.Write(4)
|
||||||
|
bo := binary.LittleEndian
|
||||||
|
bo.PutUint32(data[0:], math.Float32bits(length))
|
||||||
|
d.size++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dash) End() DashSpec {
|
||||||
|
c := d.macro.Stop()
|
||||||
|
return DashSpec{
|
||||||
|
spec: c,
|
||||||
|
phase: d.phase,
|
||||||
|
size: d.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashSpec describes a dashed pattern.
|
||||||
|
type DashSpec struct {
|
||||||
|
spec op.CallOp
|
||||||
|
phase float32
|
||||||
|
size uint8 // size of the pattern
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user