diff --git a/gpu/dash.go b/gpu/dash.go new file mode 100644 index 00000000..06f0cbf8 --- /dev/null +++ b/gpu/dash.go @@ -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) +} diff --git a/gpu/gpu.go b/gpu/gpu.go index 60cf3f50..a5912bec 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -109,6 +109,23 @@ type imageOp struct { 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 { _ = data[10] if opconst.OpType(data[0]) != opconst.TypeStroke { @@ -812,6 +829,7 @@ func (d *drawOps) collectOps(r *ops.Reader, state drawState) int { var ( quads quadsOp stroke clip.StrokeStyle + dashes dashOp ) loop: for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { @@ -822,6 +840,22 @@ loop: dop := ops.DecodeTransform(encOp.Data) 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: stroke = decodeStrokeOp(encOp.Data) @@ -851,7 +885,9 @@ loop: // Why is this not used for the offset shapes? op.bounds = v.bounds } 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 // reused. d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds}) @@ -865,6 +901,7 @@ loop: d.addClipPath(&state, quads.aux, quads.key, op.bounds, off) quads = quadsOp{} stroke = clip.StrokeStyle{} + dashes = dashOp{} case opconst.TypeColor: 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. -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)) d.qs.bounds = f32.Rectangle{ 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) aux = aux[ops.QuadSize+4:] } - quads = quads.stroke(stroke) + quads = quads.stroke(stroke, dashes) for _, quad := range quads { d.qs.contour = quad.contour quad.quad = quad.quad.Transform(tr) diff --git a/gpu/stroke.go b/gpu/stroke.go index edfb923f..785acff0 100644 --- a/gpu/stroke.go +++ b/gpu/stroke.go @@ -46,10 +46,22 @@ type strokeState struct { 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{ @@ -106,7 +118,11 @@ func (qs strokeQuads) split() []strokeQuads { 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 ( o strokeQuads 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} } +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 } @@ -349,6 +369,29 @@ 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. diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index e0db436c..361abda1 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -33,6 +33,7 @@ const ( TypeCursor TypePath TypeStroke + TypeDash ) const ( @@ -61,6 +62,7 @@ const ( TypeCursorLen = 1 + 1 TypePathLen = 1 + 4 TypeStrokeLen = 1 + 4 + 4 + 1 + 1 + TypeDashLen = 1 + 4 + 1 ) func (t OpType) Size() int { @@ -90,6 +92,7 @@ func (t OpType) Size() int { TypeCursorLen, TypePathLen, TypeStrokeLen, + TypeDashLen, }[t-firstOpIndex] } diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go index f8f0f337..a92cda84 100644 --- a/internal/rendertest/clip_test.go +++ b/internal/rendertest/clip_test.go @@ -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) { run(t, func(o *op.Ops) { 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 { p := new(clip.Path) p.Begin(o) diff --git a/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png new file mode 100644 index 00000000..03843489 Binary files /dev/null and b/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png differ diff --git a/internal/rendertest/refs/TestDashedPathFlatCapZ.png b/internal/rendertest/refs/TestDashedPathFlatCapZ.png new file mode 100644 index 00000000..10695941 Binary files /dev/null and b/internal/rendertest/refs/TestDashedPathFlatCapZ.png differ diff --git a/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png new file mode 100644 index 00000000..23a13c3b Binary files /dev/null and b/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png differ diff --git a/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png new file mode 100644 index 00000000..35158fc4 Binary files /dev/null and b/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png differ diff --git a/internal/rendertest/refs/TestPaintClippedBorder.png b/internal/rendertest/refs/TestPaintClippedBorder.png new file mode 100644 index 00000000..eb866d2c Binary files /dev/null and b/internal/rendertest/refs/TestPaintClippedBorder.png differ diff --git a/io/router/clipboard_test.go b/io/router/clipboard_test.go index 6aa30798..30373312 100644 --- a/io/router/clipboard_test.go +++ b/io/router/clipboard_test.go @@ -1,10 +1,11 @@ package router import ( + "testing" + "gioui.org/io/clipboard" "gioui.org/io/event" "gioui.org/op" - "testing" ) func TestClipboardDuplicateEvent(t *testing.T) { diff --git a/op/clip/clip.go b/op/clip/clip.go index 825cba05..b16a27ba 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -21,6 +21,7 @@ type Op struct { outline bool stroke StrokeStyle + dashes DashSpec } func (p Op) Add(o *op.Ops) { @@ -42,6 +43,15 @@ func (p Op) Add(o *op.Ops) { 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[0] = byte(opconst.TypeClip) bo := binary.LittleEndian diff --git a/op/clip/shapes.go b/op/clip/shapes.go index 3227b0fc..6bfe0154 100644 --- a/op/clip/shapes.go +++ b/op/clip/shapes.go @@ -50,8 +50,9 @@ func (rr RRect) Add(ops *op.Ops) { // Border represents the clip area of a rectangular border. type Border struct { // Rect is the bounds of the border. - Rect f32.Rectangle - Width float32 + Rect f32.Rectangle + Width float32 + Dashes DashSpec // The corner radii. SE, SW, NW, NE float32 } @@ -68,6 +69,7 @@ func (b Border) Op(ops *op.Ops) Op { Style: StrokeStyle{ Width: b.Width, }, + Dashes: b.Dashes, }.Op() } diff --git a/op/clip/stroke.go b/op/clip/stroke.go index 8cfb721b..170c076b 100644 --- a/op/clip/stroke.go +++ b/op/clip/stroke.go @@ -2,10 +2,22 @@ package clip +import ( + "encoding/binary" + "math" + + "gioui.org/internal/opconst" + "gioui.org/op" +) + // Stroke represents a stroked path. type Stroke struct { Path PathSpec 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. @@ -13,6 +25,7 @@ func (s Stroke) Op() Op { return Op{ path: s.Path, stroke: s.Style, + dashes: s.Dashes, } } @@ -57,3 +70,49 @@ const ( // RoundJoin joins path segments with a round segment. 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 +}