123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- // SPDX-License-Identifier: Unlicense OR MIT
- package clip
- import (
- "encoding/binary"
- "hash/maphash"
- "image"
- "math"
- "gioui.org/f32"
- f32internal "gioui.org/internal/f32"
- "gioui.org/internal/ops"
- "gioui.org/internal/scene"
- "gioui.org/internal/stroke"
- "gioui.org/op"
- )
- // Op represents a clip area. Op intersects the current clip area with
- // itself.
- type Op struct {
- path PathSpec
- outline bool
- width float32
- }
- // Stack represents an Op pushed on the clip stack.
- type Stack struct {
- ops *ops.Ops
- id ops.StackID
- macroID uint32
- }
- var pathSeed maphash.Seed
- func init() {
- pathSeed = maphash.MakeSeed()
- }
- // Push saves the current clip state on the stack and updates the current
- // state to the intersection of the current p.
- func (p Op) Push(o *op.Ops) Stack {
- id, macroID := ops.PushOp(&o.Internal, ops.ClipStack)
- p.add(o)
- return Stack{ops: &o.Internal, id: id, macroID: macroID}
- }
- func (p Op) add(o *op.Ops) {
- path := p.path
- if !path.hasSegments && p.width > 0 {
- switch p.path.shape {
- case ops.Rect:
- b := f32internal.FRect(path.bounds)
- var rect Path
- rect.Begin(o)
- rect.MoveTo(b.Min)
- rect.LineTo(f32.Pt(b.Max.X, b.Min.Y))
- rect.LineTo(b.Max)
- rect.LineTo(f32.Pt(b.Min.X, b.Max.Y))
- rect.Close()
- path = rect.End()
- case ops.Path:
- // Nothing to do.
- default:
- panic("invalid empty path for shape")
- }
- }
- bo := binary.LittleEndian
- if path.hasSegments {
- data := ops.Write(&o.Internal, ops.TypePathLen)
- data[0] = byte(ops.TypePath)
- bo.PutUint64(data[1:], path.hash)
- path.spec.Add(o)
- }
- bounds := path.bounds
- if p.width > 0 {
- // Expand bounds to cover stroke.
- half := int(p.width*.5 + .5)
- bounds.Min.X -= half
- bounds.Min.Y -= half
- bounds.Max.X += half
- bounds.Max.Y += half
- data := ops.Write(&o.Internal, ops.TypeStrokeLen)
- data[0] = byte(ops.TypeStroke)
- bo := binary.LittleEndian
- bo.PutUint32(data[1:], math.Float32bits(p.width))
- }
- data := ops.Write(&o.Internal, ops.TypeClipLen)
- data[0] = byte(ops.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 Stack) Pop() {
- ops.PopOp(s.ops, ops.ClipStack, s.id, s.macroID)
- data := ops.Write(s.ops, ops.TypePopClipLen)
- data[0] = byte(ops.TypePopClip)
- }
- type PathSpec struct {
- spec op.CallOp
- // hasSegments tracks whether there are any segments in the path.
- hasSegments bool
- bounds image.Rectangle
- shape ops.Shape
- hash uint64
- }
- // Path constructs a Op clip path described by lines and
- // Bézier curves, where drawing outside the Path is discarded.
- // The inside-ness of a pixel is determines by the non-zero winding rule,
- // similar to the SVG rule of the same name.
- //
- // Path generates no garbage and can be used for dynamic paths; path
- // data is stored directly in the Ops list supplied to Begin.
- type Path struct {
- ops *ops.Ops
- contour int
- pen f32.Point
- macro op.MacroOp
- start f32.Point
- hasSegments bool
- bounds f32internal.Rectangle
- hash maphash.Hash
- }
- // Pos returns the current pen position.
- func (p *Path) Pos() f32.Point { return p.pen }
- // Begin the path, storing the path data and final Op into ops.
- //
- // Caller must also call End to finish the drawing.
- // Forgetting to call it will result in a "panic: cannot mix multi ops with single ones".
- func (p *Path) Begin(o *op.Ops) {
- *p = Path{
- ops: &o.Internal,
- macro: op.Record(o),
- contour: 1,
- }
- p.hash.SetSeed(pathSeed)
- ops.BeginMulti(p.ops)
- data := ops.WriteMulti(p.ops, ops.TypeAuxLen)
- data[0] = byte(ops.TypeAux)
- }
- // End returns a PathSpec ready to use in clipping operations.
- func (p *Path) End() PathSpec {
- p.gap()
- c := p.macro.Stop()
- ops.EndMulti(p.ops)
- return PathSpec{
- spec: c,
- hasSegments: p.hasSegments,
- bounds: p.bounds.Round(),
- hash: p.hash.Sum64(),
- }
- }
- // Move moves the pen by the amount specified by delta.
- func (p *Path) Move(delta f32.Point) {
- to := delta.Add(p.pen)
- p.MoveTo(to)
- }
- // MoveTo moves the pen to the specified absolute coordinate.
- func (p *Path) MoveTo(to f32.Point) {
- if p.pen == to {
- return
- }
- p.gap()
- p.end()
- p.pen = to
- p.start = to
- }
- func (p *Path) gap() {
- if p.pen != p.start {
- // A closed contour starts and ends in the same point.
- // This move creates a gap in the contour, register it.
- data := ops.WriteMulti(p.ops, scene.CommandSize+4)
- bo := binary.LittleEndian
- bo.PutUint32(data[0:], uint32(p.contour))
- p.cmd(data[4:], scene.Gap(p.pen, p.start))
- }
- }
- // end completes the current contour.
- func (p *Path) end() {
- p.contour++
- }
- // Line moves the pen by the amount specified by delta, recording a line.
- func (p *Path) Line(delta f32.Point) {
- to := delta.Add(p.pen)
- p.LineTo(to)
- }
- // LineTo moves the pen to the absolute point specified, recording a line.
- func (p *Path) LineTo(to f32.Point) {
- if to == p.pen {
- return
- }
- data := ops.WriteMulti(p.ops, scene.CommandSize+4)
- bo := binary.LittleEndian
- bo.PutUint32(data[0:], uint32(p.contour))
- p.cmd(data[4:], scene.Line(p.pen, to))
- p.pen = to
- p.expand(to)
- }
- func (p *Path) cmd(data []byte, c scene.Command) {
- ops.EncodeCommand(data, c)
- p.hash.Write(data)
- }
- func (p *Path) expand(pt f32.Point) {
- if !p.hasSegments {
- p.hasSegments = true
- p.bounds = f32internal.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 Bézier from the pen to end
- // with the control point ctrl.
- func (p *Path) Quad(ctrl, to f32.Point) {
- ctrl = ctrl.Add(p.pen)
- to = to.Add(p.pen)
- p.QuadTo(ctrl, to)
- }
- // QuadTo records a quadratic Bézier from the pen to end
- // with the control point ctrl, with absolute coordinates.
- func (p *Path) QuadTo(ctrl, to f32.Point) {
- if ctrl == p.pen && to == p.pen {
- return
- }
- data := ops.WriteMulti(p.ops, scene.CommandSize+4)
- bo := binary.LittleEndian
- bo.PutUint32(data[0:], uint32(p.contour))
- p.cmd(data[4:], scene.Quad(p.pen, ctrl, to))
- p.pen = to
- p.expand(ctrl)
- p.expand(to)
- }
- // ArcTo adds an elliptical arc to the path. The implied ellipse is defined
- // by its focus points f1 and f2.
- // The arc starts in the current point and ends angle radians along the ellipse boundary.
- // The sign of angle determines the direction; positive being counter-clockwise,
- // negative clockwise.
- func (p *Path) ArcTo(f1, f2 f32.Point, angle float32) {
- m, segments := stroke.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 *Path) Arc(f1, f2 f32.Point, angle float32) {
- f1 = f1.Add(p.pen)
- f2 = f2.Add(p.pen)
- p.ArcTo(f1, f2, angle)
- }
- // Cube records a cubic Bézier from the pen through
- // two control points ending in to.
- func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
- p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to))
- }
- // CubeTo records a cubic Bézier from the pen through
- // two control points ending in to, with absolute coordinates.
- func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) {
- if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen {
- return
- }
- data := ops.WriteMulti(p.ops, scene.CommandSize+4)
- bo := binary.LittleEndian
- bo.PutUint32(data[0:], uint32(p.contour))
- p.cmd(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to))
- p.pen = to
- p.expand(ctrl0)
- p.expand(ctrl1)
- p.expand(to)
- }
- // Close closes the path contour.
- func (p *Path) Close() {
- if p.pen != p.start {
- p.LineTo(p.start)
- }
- p.end()
- }
- // Stroke represents a stroked path.
- type Stroke struct {
- Path PathSpec
- // Width of the stroked path.
- Width float32
- }
- // Op returns a clip operation representing the stroke.
- func (s Stroke) Op() Op {
- return Op{
- path: s.Path,
- width: s.Width,
- }
- }
- // Outline represents the area inside of a path, according to the
- // non-zero winding rule.
- type Outline struct {
- Path PathSpec
- }
- // Op returns a clip operation representing the outline.
- func (o Outline) Op() Op {
- return Op{
- path: o.Path,
- outline: true,
- }
- }
|