// SPDX-License-Identifier: Unlicense OR MIT // Public op API for Gio. Merges op.go, clip/*.go, and paint/*.go into // package gio alongside the internal ops (ops.mx). // // Name mapping from the original multi-package layout: // op.Ops -> OpList (wraps internal Ops via .Internal field) // op.MacroOp -> MacroOp // op.CallOp -> CallOp // op.TransformOp -> TransformOp // op.TransformStack-> TransformStack // op.InvalidateCmd -> InvalidateCmd // clip.Op -> ClipOp // clip.Stack -> ClipStack // clip.PathSpec -> PathSpec // clip.Path -> ClipPath // clip.Stroke -> ClipStroke // clip.Outline -> ClipOutline // clip.Rect -> ClipRect // clip.RRect -> ClipRRect // clip.Ellipse -> ClipEllipse // paint.ImageFilter -> ImageFilter // paint.ImageOp -> PaintImageOp // paint.ColorOp -> PaintColorOp // paint.LinearGradientOp -> PaintLinearGradientOp // paint.PaintOp -> PaintPaintOp // paint.OpacityStack -> PaintOpacityStack package gio import ( "encoding/binary" "hash" "hash/fnv" "image" "image/color" "image/draw" "math" "time" ) // --------------------------------------------------------------------------- // op.go - OpList and friends // --------------------------------------------------------------------------- // OpList is the public ops list (was op.Ops). It wraps the internal Ops. type OpList struct { Internal Ops } // MacroOp records a list of operations for later use. type MacroOp struct { ops *Ops id StackID pc PC } // CallOp invokes the operations recorded by Record. type CallOp struct { ops *Ops start PC end PC } // InvalidateCmd requests a redraw at the given time. Use // the zero value to request an immediate redraw. type InvalidateCmd struct { At time.Time } func (InvalidateCmd) ImplementsCommand() {} // TransformOp represents a transformation that can be pushed on the // transformation stack. type TransformOp struct { t Affine2D } // TransformStack represents a TransformOp pushed on the transformation stack. type TransformStack struct { id StackID macroID uint32 ops *Ops } // Defer executes c after all other operations have completed, including // previously deferred operations. func Defer(o *OpList, c CallOp) { if c.ops == nil { return } state := SaveOps(&o.Internal) m := Record(o) state.Load() c.Add(o) c = m.Stop() data := WriteOps(&o.Internal, TypeDeferLen) data[0] = byte(TypeDefer) c.Add(o) } // Reset the OpList, preparing it for re-use. func (o *OpList) Reset() { ResetOps(&o.Internal) } // Record a macro of operations. func Record(o *OpList) MacroOp { m := MacroOp{ ops: &o.Internal, id: PushMacro(&o.Internal), pc: PCFor(&o.Internal), } data := WriteOps(m.ops, TypeMacroLen) data[0] = byte(TypeMacro) return m } // Stop ends a previously started recording and returns a CallOp for replaying it. func (m MacroOp) Stop() CallOp { PopMacro(m.ops, m.id) FillMacro(m.ops, m.pc) return CallOp{ ops: m.ops, start: m.pc.Add(TypeMacro), end: PCFor(m.ops), } } // Add the recorded list of operations. func (c CallOp) Add(o *OpList) { if c.ops == nil { return } AddCall(&o.Internal, c.ops, c.start, c.end) } // Offset converts an offset to a TransformOp. func Offset(off image.Point) TransformOp { offf := Pt(float32(off.X), float32(off.Y)) return AffineTransform(AffineId().Offset(offf)) } // AffineTransform creates a TransformOp representing the transformation a. func AffineTransform(a Affine2D) TransformOp { return TransformOp{t: a} } // Push the current transformation to the stack and then multiply the // current transformation with t. func (t TransformOp) Push(o *OpList) TransformStack { id, macroID := PushOp(&o.Internal, TransStack) t.add(o, true) return TransformStack{ops: &o.Internal, id: id, macroID: macroID} } // Add is like Push except it doesn't push the current transformation to the // stack. func (t TransformOp) Add(o *OpList) { t.add(o, false) } func (t TransformOp) add(o *OpList, push bool) { data := WriteOps(&o.Internal, TypeTransformLen) data[0] = byte(TypeTransform) if push { data[1] = 1 } bo := binary.LittleEndian() a, b, c, d, e, f := t.t.Elems() bo.PutUint32(data[2:], math.Float32bits(a)) bo.PutUint32(data[2+4*1:], math.Float32bits(b)) bo.PutUint32(data[2+4*2:], math.Float32bits(c)) bo.PutUint32(data[2+4*3:], math.Float32bits(d)) bo.PutUint32(data[2+4*4:], math.Float32bits(e)) bo.PutUint32(data[2+4*5:], math.Float32bits(f)) } func (t TransformStack) Pop() { PopOp(t.ops, TransStack, t.id, t.macroID) data := WriteOps(t.ops, TypePopTransformLen) data[0] = byte(TypePopTransform) } // --------------------------------------------------------------------------- // clip/clip.go - ClipOp, ClipStack, PathSpec, ClipPath // --------------------------------------------------------------------------- // ClipOp represents a clip area. ClipOp intersects the current clip area // with itself. (was clip.Op) type ClipOp struct { path PathSpec outline bool width float32 } // ClipStack represents a ClipOp pushed on the clip stack. (was clip.Stack) type ClipStack struct { ops *Ops id StackID macroID uint32 } // PathSpec describes a completed path for use in clip operations. type PathSpec struct { spec CallOp hasSegments bool bounds image.Rectangle shape Shape hash uint64 } // ClipPath constructs a clip path described by lines and Bezier curves. // (was clip.Path) type ClipPath struct { ops *Ops contour int32 pen Point macro MacroOp start Point hasSegments bool bounds Rectangle hash hash.Hash64 } // Push saves the current clip state on the stack and updates the current // state to the intersection of the current p. func (p ClipOp) Push(o *OpList) ClipStack { id, macroID := PushOp(&o.Internal, ClipStackKind) p.add(o) return ClipStack{ops: &o.Internal, id: id, macroID: macroID} } func (p ClipOp) add(o *OpList) { path := p.path if !path.hasSegments && p.width > 0 { switch p.path.shape { case ShapeRect: b := FRect(path.bounds) var rect ClipPath rect.Begin(o) rect.MoveTo(b.Min) rect.LineTo(Pt(b.Max.X, b.Min.Y)) rect.LineTo(b.Max) rect.LineTo(Pt(b.Min.X, b.Max.Y)) rect.Close() path = rect.End() case ShapePath: // Nothing to do. default: panic("invalid empty path for shape") } } bo := binary.LittleEndian() if path.hasSegments { data := WriteOps(&o.Internal, TypePathLen) data[0] = byte(TypePath) bo.PutUint64(data[1:], path.hash) path.spec.Add(o) } bounds := path.bounds if p.width > 0 { half := int(p.width*.5 + .5) bounds.Min.X -= half bounds.Min.Y -= half bounds.Max.X += half bounds.Max.Y += half data := WriteOps(&o.Internal, TypeStrokeLen) data[0] = byte(TypeStroke) bo.PutUint32(data[1:], math.Float32bits(p.width)) } data := WriteOps(&o.Internal, TypeClipLen) data[0] = byte(TypeClip) bo.PutUint32(data[1:], uint32(bounds.Min.X)) bo.PutUint32(data[5:], uint32(bounds.Min.Y)) bo.PutUint32(data[9:], uint32(bounds.Max.X)) bo.PutUint32(data[13:], uint32(bounds.Max.Y)) if p.outline { data[17] = byte(1) } data[18] = byte(path.shape) } func (s ClipStack) Pop() { PopOp(s.ops, ClipStackKind, s.id, s.macroID) data := WriteOps(s.ops, TypePopClipLen) data[0] = byte(TypePopClip) } // Pos returns the current pen position. func (p *ClipPath) Pos() Point { return p.pen } // Begin the path, storing the path data and final op into o. func (p *ClipPath) Begin(o *OpList) { h := fnv.New64a() *p = ClipPath{ ops: &o.Internal, macro: Record(o), contour: 1, hash: h, } BeginMulti(p.ops) data := WriteMulti(p.ops, TypeAuxLen) data[0] = byte(TypeAux) } // End returns a PathSpec ready to use in clipping operations. func (p *ClipPath) End() PathSpec { p.gap() c := p.macro.Stop() EndMulti(p.ops) return PathSpec{ spec: c, hasSegments: p.hasSegments, bounds: p.bounds.Round(), hash: p.hash.Sum64(), } } // Move moves the pen by delta. func (p *ClipPath) Move(delta Point) { to := delta.Add(p.pen) p.MoveTo(to) } // MoveTo moves the pen to the specified absolute coordinate. func (p *ClipPath) MoveTo(to Point) { if p.pen == to { return } p.gap() p.end() p.pen = to p.start = to } func (p *ClipPath) gap() { if p.pen != p.start { data := WriteMulti(p.ops, CommandSize+4) bo := binary.LittleEndian() bo.PutUint32(data[0:], uint32(p.contour)) p.cmd(data[4:], Gap(p.pen, p.start)) } } func (p *ClipPath) end() { p.contour++ } // Line moves the pen by delta, recording a line. func (p *ClipPath) Line(delta Point) { to := delta.Add(p.pen) p.LineTo(to) } // LineTo moves the pen to 'to', recording a line. func (p *ClipPath) LineTo(to Point) { if to == p.pen { return } data := WriteMulti(p.ops, CommandSize+4) bo := binary.LittleEndian() bo.PutUint32(data[0:], uint32(p.contour)) p.cmd(data[4:], Line(p.pen, to)) p.expand(p.pen) p.expand(to) p.pen = to } func (p *ClipPath) cmd(data []byte, c Command) { EncodeCommand(data, c) p.hash.Write(data) } func (p *ClipPath) expand(pt Point) { if !p.hasSegments { p.hasSegments = true p.bounds = Rectangle{Min: pt, Max: pt} } else { b := p.bounds if pt.X < b.Min.X { b.Min.X = pt.X } if pt.Y < b.Min.Y { b.Min.Y = pt.Y } if pt.X > b.Max.X { b.Max.X = pt.X } if pt.Y > b.Max.Y { b.Max.Y = pt.Y } p.bounds = b } } // Quad records a quadratic Bezier from the pen to 'to' // with the control point ctrl (relative). func (p *ClipPath) Quad(ctrl, to Point) { ctrl = ctrl.Add(p.pen) to = to.Add(p.pen) p.QuadTo(ctrl, to) } // QuadTo records a quadratic Bezier from the pen to 'to' // with the control point ctrl, with absolute coordinates. func (p *ClipPath) QuadTo(ctrl, to Point) { if ctrl == p.pen && to == p.pen { return } data := WriteMulti(p.ops, CommandSize+4) bo := binary.LittleEndian() bo.PutUint32(data[0:], uint32(p.contour)) p.cmd(data[4:], Quad(p.pen, ctrl, to)) p.expand(p.pen) p.expand(ctrl) p.expand(to) p.pen = to } // ArcTo adds an elliptical arc to the path. func (p *ClipPath) ArcTo(f1, f2 Point, angle float32) { m, segments := ArcTransform(p.pen, f1, f2, angle) for i := 0; i < segments; i++ { p0 := p.pen p1 := m.Transform(p0) p2 := m.Transform(p1) ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) p.QuadTo(ctl, p2) } } // Arc is like ArcTo where f1 and f2 are relative to the current position. func (p *ClipPath) Arc(f1, f2 Point, angle float32) { f1 = f1.Add(p.pen) f2 = f2.Add(p.pen) p.ArcTo(f1, f2, angle) } // Cube records a cubic Bezier from the pen through // two control points ending in 'to' (relative). func (p *ClipPath) Cube(ctrl0, ctrl1, to Point) { p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to)) } // CubeTo records a cubic Bezier from the pen through // two control points ending in 'to', with absolute coordinates. func (p *ClipPath) CubeTo(ctrl0, ctrl1, to Point) { if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen { return } data := WriteMulti(p.ops, CommandSize+4) bo := binary.LittleEndian() bo.PutUint32(data[0:], uint32(p.contour)) p.cmd(data[4:], Cubic(p.pen, ctrl0, ctrl1, to)) p.expand(p.pen) p.expand(ctrl0) p.expand(ctrl1) p.expand(to) p.pen = to } // Close closes the path contour. func (p *ClipPath) Close() { if p.pen != p.start { p.LineTo(p.start) } p.end() } // --------------------------------------------------------------------------- // clip/clip.go - ClipStroke, ClipOutline // --------------------------------------------------------------------------- // ClipStroke represents a stroked path. (was clip.Stroke) type ClipStroke struct { Path PathSpec Width float32 } // Op returns a ClipOp representing the stroke. func (s ClipStroke) Op() ClipOp { return ClipOp{ path: s.Path, width: s.Width, } } // ClipOutline represents the area inside of a path, according to the // non-zero winding rule. (was clip.Outline) type ClipOutline struct { Path PathSpec } // Op returns a ClipOp representing the outline. func (ol ClipOutline) Op() ClipOp { return ClipOp{ path: ol.Path, outline: true, } } // --------------------------------------------------------------------------- // clip/shapes.go - ClipRect, ClipRRect, ClipEllipse // --------------------------------------------------------------------------- // ClipRect represents the clip area of a pixel-aligned rectangle. type ClipRect image.Rectangle // Op returns the ClipOp for the rectangle. func (r ClipRect) Op() ClipOp { return ClipOp{ outline: true, path: r.Path(), } } // Push the clip operation on the clip stack. func (r ClipRect) Push(o *OpList) ClipStack { return r.Op().Push(o) } // Path returns the PathSpec for the rectangle. func (r ClipRect) Path() PathSpec { return PathSpec{ shape: ShapeRect, bounds: image.Rectangle(r), } } // UniformClipRRect returns a ClipRRect with all corner radii set to radius. func UniformClipRRect(rect image.Rectangle, radius int) ClipRRect { return ClipRRect{ Rect: rect, SE: radius, SW: radius, NE: radius, NW: radius, } } // ClipRRect represents the clip area of a rectangle with rounded corners. type ClipRRect struct { Rect image.Rectangle SE, SW, NW, NE int } // Op returns the ClipOp for the rounded rectangle. func (rr ClipRRect) Op(o *OpList) ClipOp { if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 { return ClipRect(rr.Rect).Op() } return ClipOutline{Path: rr.Path(o)}.Op() } // Push the rectangle clip on the clip stack. func (rr ClipRRect) Push(o *OpList) ClipStack { return rr.Op(o).Push(o) } // Path returns the PathSpec for the rounded rectangle. func (rr ClipRRect) Path(o *OpList) PathSpec { var p ClipPath p.Begin(o) const q = 4 * (math.Sqrt2 - 1) / 3 const iq = 1 - q se, sw, nw, ne := float32(rr.SE), float32(rr.SW), float32(rr.NW), float32(rr.NE) rrf := FRect(rr.Rect) w, n, e, s := rrf.Min.X, rrf.Min.Y, rrf.Max.X, rrf.Max.Y p.MoveTo(Point{X: w + nw, Y: n}) p.LineTo(Point{X: e - ne, Y: n}) p.CubeTo( Point{X: e - ne*iq, Y: n}, Point{X: e, Y: n + ne*iq}, Point{X: e, Y: n + ne}) p.LineTo(Point{X: e, Y: s - se}) p.CubeTo( Point{X: e, Y: s - se*iq}, Point{X: e - se*iq, Y: s}, Point{X: e - se, Y: s}) p.LineTo(Point{X: w + sw, Y: s}) p.CubeTo( Point{X: w + sw*iq, Y: s}, Point{X: w, Y: s - sw*iq}, Point{X: w, Y: s - sw}) p.LineTo(Point{X: w, Y: n + nw}) p.CubeTo( Point{X: w, Y: n + nw*iq}, Point{X: w + nw*iq, Y: n}, Point{X: w + nw, Y: n}) return p.End() } // ClipEllipse represents the largest axis-aligned ellipse contained in its bounds. type ClipEllipse image.Rectangle // Op returns the ClipOp for the filled ellipse. func (el ClipEllipse) Op(o *OpList) ClipOp { return ClipOutline{Path: el.Path(o)}.Op() } // Push the filled ellipse clip op on the clip stack. func (el ClipEllipse) Push(o *OpList) ClipStack { return el.Op(o).Push(o) } // Path constructs a path for the ellipse. func (el ClipEllipse) Path(o *OpList) PathSpec { bounds := image.Rectangle(el) if bounds.Dx() == 0 || bounds.Dy() == 0 { return PathSpec{shape: ShapeRect} } var p ClipPath p.Begin(o) bf := FRect(bounds) center := bf.Max.Add(bf.Min).Mul(.5) diam := bf.Dx() r := diam * .5 scale := bf.Dy() / diam const q = 4 * (math.Sqrt2 - 1) / 3 curve := r * q top := Point{X: center.X, Y: center.Y - r*scale} p.MoveTo(top) p.CubeTo( Point{X: center.X + curve, Y: center.Y - r*scale}, Point{X: center.X + r, Y: center.Y - curve*scale}, Point{X: center.X + r, Y: center.Y}, ) p.CubeTo( Point{X: center.X + r, Y: center.Y + curve*scale}, Point{X: center.X + curve, Y: center.Y + r*scale}, Point{X: center.X, Y: center.Y + r*scale}, ) p.CubeTo( Point{X: center.X - curve, Y: center.Y + r*scale}, Point{X: center.X - r, Y: center.Y + curve*scale}, Point{X: center.X - r, Y: center.Y}, ) p.CubeTo( Point{X: center.X - r, Y: center.Y - curve*scale}, Point{X: center.X - curve, Y: center.Y - r*scale}, top, ) ellipse := p.End() ellipse.shape = ShapeEllipse return ellipse } // --------------------------------------------------------------------------- // paint/paint.go // --------------------------------------------------------------------------- // ImageFilter is the scaling filter for images. type ImageFilter byte const ( ImageFilterLinear ImageFilter = iota ImageFilterNearest ) // PaintImageOp sets the brush to an image. (was paint.ImageOp) type PaintImageOp struct { Filter ImageFilter uniform bool color color.NRGBA src *image.RGBA handle any } // PaintColorOp sets the brush to a constant color. type PaintColorOp struct { Color color.NRGBA } // PaintLinearGradientOp sets the brush to a gradient. type PaintLinearGradientOp struct { Stop1 Point Color1 color.NRGBA Stop2 Point Color2 color.NRGBA } // PaintPaintOp fills the current clip area with the current brush. type PaintPaintOp struct{} // PaintOpacityStack represents an opacity applied to all painting operations // until Pop is called. type PaintOpacityStack struct { id StackID macroID uint32 ops *Ops } // NewPaintImageOp creates a PaintImageOp backed by src. func NewPaintImageOp(src image.Image) PaintImageOp { switch src := src.(type) { case *image.Uniform: col := color.NRGBAModel().Convert(src.C).(color.NRGBA) return PaintImageOp{ uniform: true, color: col, } case *image.RGBA: // Use src pointer as handle so GPU texture is cached across frames // (same *image.RGBA = same cache key = texture reuse, not re-upload). return PaintImageOp{ src: src, handle: src, } } sz := src.Bounds().Size() dst := image.NewRGBA(image.Rectangle{ Max: sz, }) draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src) return PaintImageOp{ src: dst, handle: dst, // use pointer as stable cache key } } func (i PaintImageOp) Size() image.Point { if i.src == nil { return image.Point{} } return i.src.Bounds().Size() } func (i PaintImageOp) Add(o *OpList) { if i.uniform { PaintColorOp{ Color: i.color, }.Add(o) return } else if i.src == nil || i.src.Bounds().Empty() { return } data := WriteOps2(&o.Internal, TypeImageLen, i.src, i.handle) data[0] = byte(TypeImage) data[1] = byte(i.Filter) } func (c PaintColorOp) Add(o *OpList) { data := WriteOps(&o.Internal, TypeColorLen) data[0] = byte(TypeColor) data[1] = c.Color.R data[2] = c.Color.G data[3] = c.Color.B data[4] = c.Color.A } func (c PaintLinearGradientOp) Add(o *OpList) { data := WriteOps(&o.Internal, TypeLinearGradientLen) data[0] = byte(TypeLinearGradient) bo := binary.LittleEndian() bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X)) bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y)) bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X)) bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y)) data[17+0] = c.Color1.R data[17+1] = c.Color1.G data[17+2] = c.Color1.B data[17+3] = c.Color1.A data[21+0] = c.Color2.R data[21+1] = c.Color2.G data[21+2] = c.Color2.B data[21+3] = c.Color2.A } func (d PaintPaintOp) Add(o *OpList) { data := WriteOps(&o.Internal, TypePaintLen) data[0] = byte(TypePaint) } // FillShape fills the clip shape with a color. func FillShape(o *OpList, c color.NRGBA, shape ClipOp) { defer shape.Push(o).Pop() Fill(o, c) } // Fill paints an infinitely large plane with the provided color. func Fill(o *OpList, c color.NRGBA) { PaintColorOp{Color: c}.Add(o) PaintPaintOp{}.Add(o) } // PushOpacity creates a drawing layer with an opacity in the range [0;1]. func PushOpacity(o *OpList, opacity float32) PaintOpacityStack { if opacity > 1 { opacity = 1 } if opacity < 0 { opacity = 0 } id, macroID := PushOp(&o.Internal, OpacityStackKind) data := WriteOps(&o.Internal, TypePushOpacityLen) bo := binary.LittleEndian() data[0] = byte(TypePushOpacity) bo.PutUint32(data[1:], math.Float32bits(opacity)) return PaintOpacityStack{ops: &o.Internal, id: id, macroID: macroID} } func (t PaintOpacityStack) Pop() { PopOp(t.ops, OpacityStackKind, t.id, t.macroID) data := WriteOps(t.ops, TypePopOpacityLen) data[0] = byte(TypePopOpacity) }