clip.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package clip
4
5 import (
6 "encoding/binary"
7 "image"
8 "math"
9
10 "github.com/p9c/p9/pkg/gel/gio/f32"
11 "github.com/p9c/p9/pkg/gel/gio/internal/opconst"
12 "github.com/p9c/p9/pkg/gel/gio/internal/ops"
13 "github.com/p9c/p9/pkg/gel/gio/internal/scene"
14 "github.com/p9c/p9/pkg/gel/gio/internal/stroke"
15 "github.com/p9c/p9/pkg/gel/gio/op"
16 )
17
18 // Op represents a clip area. Op intersects the current clip area with
19 // itself.
20 type Op struct {
21 bounds image.Rectangle
22 path PathSpec
23
24 outline bool
25 stroke StrokeStyle
26 dashes DashSpec
27 }
28
29 func (p Op) Add(o *op.Ops) {
30 str := p.stroke
31 dashes := p.dashes
32 path := p.path
33 outline := p.outline
34 approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap)
35 if approx {
36 // If the stroke is not natively supported by the compute renderer, construct a filled path
37 // that approximates it.
38 path = p.approximateStroke(o)
39 dashes = DashSpec{}
40 str = StrokeStyle{}
41 outline = true
42 }
43
44 if path.hasSegments {
45 data := o.Write(opconst.TypePathLen)
46 data[0] = byte(opconst.TypePath)
47 path.spec.Add(o)
48 }
49
50 if str.Width > 0 {
51 data := o.Write(opconst.TypeStrokeLen)
52 data[0] = byte(opconst.TypeStroke)
53 bo := binary.LittleEndian
54 bo.PutUint32(data[1:], math.Float32bits(str.Width))
55 }
56
57 data := o.Write(opconst.TypeClipLen)
58 data[0] = byte(opconst.TypeClip)
59 bo := binary.LittleEndian
60 bo.PutUint32(data[1:], uint32(p.bounds.Min.X))
61 bo.PutUint32(data[5:], uint32(p.bounds.Min.Y))
62 bo.PutUint32(data[9:], uint32(p.bounds.Max.X))
63 bo.PutUint32(data[13:], uint32(p.bounds.Max.Y))
64 if outline {
65 data[17] = byte(1)
66 }
67 }
68
69 func (p Op) approximateStroke(o *op.Ops) PathSpec {
70 if !p.path.hasSegments {
71 return PathSpec{}
72 }
73
74 var r ops.Reader
75 // Add path op for us to decode. Use a macro to omit it from later decodes.
76 ignore := op.Record(o)
77 r.ResetAt(o, ops.NewPC(o))
78 p.path.spec.Add(o)
79 ignore.Stop()
80 encOp, ok := r.Decode()
81 if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
82 panic("corrupt path data")
83 }
84 pathData := encOp.Data[opconst.TypeAuxLen:]
85
86 // Decode dashes in a similar way.
87 var dashes stroke.DashOp
88 if p.dashes.phase != 0 || p.dashes.size > 0 {
89 ignore := op.Record(o)
90 r.ResetAt(o, ops.NewPC(o))
91 p.dashes.spec.Add(o)
92 ignore.Stop()
93 encOp, ok := r.Decode()
94 if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
95 panic("corrupt dash data")
96 }
97 dashes.Dashes = make([]float32, p.dashes.size)
98 dashData := encOp.Data[opconst.TypeAuxLen:]
99 bo := binary.LittleEndian
100 for i := range dashes.Dashes {
101 dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:]))
102 }
103 dashes.Phase = p.dashes.phase
104 }
105
106 // Approximate and output path data.
107 var outline Path
108 outline.Begin(o)
109 ss := stroke.StrokeStyle{
110 Width: p.stroke.Width,
111 Miter: p.stroke.Miter,
112 Cap: stroke.StrokeCap(p.stroke.Cap),
113 Join: stroke.StrokeJoin(p.stroke.Join),
114 }
115 quads := stroke.StrokePathCommands(ss, dashes, pathData)
116 pen := f32.Pt(0, 0)
117 for _, quad := range quads {
118 q := quad.Quad
119 if q.From != pen {
120 pen = q.From
121 outline.MoveTo(pen)
122 }
123 outline.contour = int(quad.Contour)
124 outline.QuadTo(q.Ctrl, q.To)
125 }
126 return outline.End()
127 }
128
129 type PathSpec struct {
130 spec op.CallOp
131 // open is true if any path contour is not closed. A closed contour starts
132 // and ends in the same point.
133 open bool
134 // hasSegments tracks whether there are any segments in the path.
135 hasSegments bool
136 }
137
138 // Path constructs a Op clip path described by lines and
139 // Bézier curves, where drawing outside the Path is discarded.
140 // The inside-ness of a pixel is determines by the non-zero winding rule,
141 // similar to the SVG rule of the same name.
142 //
143 // Path generates no garbage and can be used for dynamic paths; path
144 // data is stored directly in the Ops list supplied to Begin.
145 type Path struct {
146 ops *op.Ops
147 open bool
148 contour int
149 pen f32.Point
150 macro op.MacroOp
151 start f32.Point
152 hasSegments bool
153 }
154
155 // Pos returns the current pen position.
156 func (p *Path) Pos() f32.Point { return p.pen }
157
158 // Begin the path, storing the path data and final Op into ops.
159 func (p *Path) Begin(ops *op.Ops) {
160 p.ops = ops
161 p.macro = op.Record(ops)
162 // Write the TypeAux opcode
163 data := ops.Write(opconst.TypeAuxLen)
164 data[0] = byte(opconst.TypeAux)
165 }
166
167 // End returns a PathSpec ready to use in clipping operations.
168 func (p *Path) End() PathSpec {
169 c := p.macro.Stop()
170 return PathSpec{
171 spec: c,
172 open: p.open || p.pen != p.start,
173 hasSegments: p.hasSegments,
174 }
175 }
176
177 // Move moves the pen by the amount specified by delta.
178 func (p *Path) Move(delta f32.Point) {
179 to := delta.Add(p.pen)
180 p.MoveTo(to)
181 }
182
183 // MoveTo moves the pen to the specified absolute coordinate.
184 func (p *Path) MoveTo(to f32.Point) {
185 p.open = p.open || p.pen != p.start
186 p.end()
187 p.pen = to
188 p.start = to
189 }
190
191 // end completes the current contour.
192 func (p *Path) end() {
193 p.contour++
194 }
195
196 // Line moves the pen by the amount specified by delta, recording a line.
197 func (p *Path) Line(delta f32.Point) {
198 to := delta.Add(p.pen)
199 p.LineTo(to)
200 }
201
202 // LineTo moves the pen to the absolute point specified, recording a line.
203 func (p *Path) LineTo(to f32.Point) {
204 data := p.ops.Write(scene.CommandSize + 4)
205 bo := binary.LittleEndian
206 bo.PutUint32(data[0:], uint32(p.contour))
207 ops.EncodeCommand(data[4:], scene.Line(p.pen, to))
208 p.pen = to
209 p.hasSegments = true
210 }
211
212 // Quad records a quadratic Bézier from the pen to end
213 // with the control point ctrl.
214 func (p *Path) Quad(ctrl, to f32.Point) {
215 ctrl = ctrl.Add(p.pen)
216 to = to.Add(p.pen)
217 p.QuadTo(ctrl, to)
218 }
219
220 // QuadTo records a quadratic Bézier from the pen to end
221 // with the control point ctrl, with absolute coordinates.
222 func (p *Path) QuadTo(ctrl, to f32.Point) {
223 data := p.ops.Write(scene.CommandSize + 4)
224 bo := binary.LittleEndian
225 bo.PutUint32(data[0:], uint32(p.contour))
226 ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to))
227 p.pen = to
228 p.hasSegments = true
229 }
230
231 // Arc adds an elliptical arc to the path. The implied ellipse is defined
232 // by its focus points f1 and f2.
233 // The arc starts in the current point and ends angle radians along the ellipse boundary.
234 // The sign of angle determines the direction; positive being counter-clockwise,
235 // negative clockwise.
236 func (p *Path) Arc(f1, f2 f32.Point, angle float32) {
237 f1 = f1.Add(p.pen)
238 f2 = f2.Add(p.pen)
239 const segments = 16
240 m := stroke.ArcTransform(p.pen, f1, f2, angle, segments)
241
242 for i := 0; i < segments; i++ {
243 p0 := p.pen
244 p1 := m.Transform(p0)
245 p2 := m.Transform(p1)
246 ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
247 p.QuadTo(ctl, p2)
248 }
249 }
250
251 // Cube records a cubic Bézier from the pen through
252 // two control points ending in to.
253 func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
254 p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to))
255 }
256
257 // CubeTo records a cubic Bézier from the pen through
258 // two control points ending in to, with absolute coordinates.
259 func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) {
260 if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen {
261 return
262 }
263 data := p.ops.Write(scene.CommandSize + 4)
264 bo := binary.LittleEndian
265 bo.PutUint32(data[0:], uint32(p.contour))
266 ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to))
267 p.pen = to
268 p.hasSegments = true
269 }
270
271 // Close closes the path contour.
272 func (p *Path) Close() {
273 if p.pen != p.start {
274 p.LineTo(p.start)
275 }
276 p.end()
277 }
278
279 // Outline represents the area inside of a path, according to the
280 // non-zero winding rule.
281 type Outline struct {
282 Path PathSpec
283 }
284
285 // Op returns a clip operation representing the outline.
286 func (o Outline) Op() Op {
287 if o.Path.open {
288 panic("not all path contours are closed")
289 }
290 return Op{
291 path: o.Path,
292 outline: true,
293 }
294 }
295