scene/path.go

Functions Structs

Functions

func Arc

Arc adds an arc path (portion of an ellipse).

The arc is drawn from startAngle to endAngle (in radians).

If sweepClockwise is true, the arc is drawn clockwise.

func (p *Path) Arc(cx, cy, rx, ry, startAngle, endAngle float32, sweepClockwise bool) *Path {
	// Normalize angles
	if sweepClockwise && endAngle < startAngle {
		endAngle += 2 * math.Pi
	} else if !sweepClockwise && startAngle < endAngle {
		startAngle += 2 * math.Pi
	}

	// Calculate start point
	startX := cx + rx*float32(math.Cos(float64(startAngle)))
	startY := cy + ry*float32(math.Sin(float64(startAngle)))
	p.MoveTo(startX, startY)

	// Calculate sweep angle
	sweep := endAngle - startAngle
	if !sweepClockwise {
		sweep = -sweep
	}

	// Split into quarter arcs (max 90 degrees each) for better approximation
	numArcs := int(math.Ceil(math.Abs(float64(sweep)) / (math.Pi / 2)))
	if numArcs < 1 {
		numArcs = 1
	}

	arcAngle := sweep / float32(numArcs)
	currentAngle := startAngle

	for i := 0; i < numArcs; i++ {
		nextAngle := currentAngle + arcAngle
		p.arcSegment(cx, cy, rx, ry, currentAngle, nextAngle)
		currentAngle = nextAngle
	}

	return p
}

func Bounds

Bounds returns the bounding rectangle of the path.

Note: This is a conservative approximation that includes control points.

func (p *Path) Bounds() Rect {
	return p.bounds
}

func Circle

Circle adds a circle path.

func (p *Path) Circle(cx, cy, r float32) *Path {
	return p.Ellipse(cx, cy, r, r)
}

func Clone

Clone creates a deep copy of the path.

func (p *Path) Clone() *Path {
	result := NewPath()
	result.verbs = make([]PathVerb, len(p.verbs))
	copy(result.verbs, p.verbs)
	result.points = make([]float32, len(p.points))
	copy(result.points, p.points)
	result.bounds = p.bounds
	result.start = p.start
	result.cursor = p.cursor
	return result
}

func Close

Close closes the current subpath by drawing a line back to its start.

func (p *Path) Close() *Path {
	p.verbs = append(p.verbs, Close)
	p.cursor = p.start
	return p
}

func Contains

Contains returns true if the point (px, py) is inside the path.

This uses the non-zero winding rule to determine containment.

The test is performed by casting a ray from the point to infinity

and counting the number of times the path crosses the ray.

func (p *Path) Contains(px, py float32) bool {
	// Quick bounds check
	if !p.bounds.IsEmpty() {
		if px < p.bounds.MinX || px > p.bounds.MaxX ||
			py < p.bounds.MinY || py > p.bounds.MaxY {
			return false
		}
	}

	// Count winding number
	winding := 0

	// Track current position and subpath start
	var curX, curY float32
	var startX, startY float32

	pointIdx := 0

	for _, verb := range p.verbs {
		switch verb {
		case MoveTo:
			// Close previous subpath if any
			if curX != startX || curY != startY {
				winding += windingSegment(curX, curY, startX, startY, px, py)
			}
			startX = p.points[pointIdx]
			startY = p.points[pointIdx+1]
			curX, curY = startX, startY
			pointIdx += 2

		case LineTo:
			nextX := p.points[pointIdx]
			nextY := p.points[pointIdx+1]
			winding += windingSegment(curX, curY, nextX, nextY, px, py)
			curX, curY = nextX, nextY
			pointIdx += 2

		case QuadTo:
			// Approximate quad with lines for containment test
			cx := p.points[pointIdx]
			cy := p.points[pointIdx+1]
			x := p.points[pointIdx+2]
			y := p.points[pointIdx+3]

			// Subdivide quadratic into lines
			winding += windingQuad(curX, curY, cx, cy, x, y, px, py)
			curX, curY = x, y
			pointIdx += 4

		case CubicTo:
			// Approximate cubic with lines for containment test
			c1x := p.points[pointIdx]
			c1y := p.points[pointIdx+1]
			c2x := p.points[pointIdx+2]
			c2y := p.points[pointIdx+3]
			x := p.points[pointIdx+4]
			y := p.points[pointIdx+5]

			winding += windingCubic(curX, curY, c1x, c1y, c2x, c2y, x, y, px, py)
			curX, curY = x, y
			pointIdx += 6

		case Close:
			// Close the subpath
			if curX != startX || curY != startY {
				winding += windingSegment(curX, curY, startX, startY, px, py)
			}
			curX, curY = startX, startY
		}
	}

	// Close final subpath if not explicitly closed
	if curX != startX || curY != startY {
		winding += windingSegment(curX, curY, startX, startY, px, py)
	}

	return winding != 0
}

func CubicTo

CubicTo draws a cubic Bezier curve.

The curve goes from the current point to (x, y) using (c1x, c1y) and (c2x, c2y) as control points.

func (p *Path) CubicTo(c1x, c1y, c2x, c2y, x, y float32) *Path {
	p.verbs = append(p.verbs, CubicTo)
	p.points = append(p.points, c1x, c1y, c2x, c2y, x, y)
	p.bounds = p.bounds.UnionPoint(c1x, c1y)
	p.bounds = p.bounds.UnionPoint(c2x, c2y)
	p.bounds = p.bounds.UnionPoint(x, y)
	// For accurate bounds, we should compute curve extrema,
	// but union with control points is a conservative approximation
	p.cursor = [2]float32{x, y}
	return p
}

func Elements

Elements returns an iterator over all path elements.

This uses Go 1.25+ iter.Seq for efficient, zero-allocation iteration

when used with a for-range loop.

 

Example:

 

for elem := range path.Elements() {

switch elem.Verb {

case MoveTo:

fmt.Printf("Move to %v\n", elem.Points[0])

case LineTo:

fmt.Printf("Line to %v\n", elem.Points[0])

case QuadTo:

fmt.Printf("Quad to %v via %v\n", elem.Points[1], elem.Points[0])

case CubicTo:

fmt.Printf("Cubic to %v\n", elem.Points[2])

case Close:

fmt.Println("Close")

}

}

func (p *Path) Elements() iter.Seq[PathElement] {
	return func(yield func(PathElement) bool) {
		pointIdx := 0

		for _, verb := range p.verbs {
			var elem PathElement
			elem.Verb = verb

			switch verb {
			case MoveTo, LineTo:
				elem.Points = []Point{
					{p.points[pointIdx], p.points[pointIdx+1]},
				}
				pointIdx += 2

			case QuadTo:
				elem.Points = []Point{
					{p.points[pointIdx], p.points[pointIdx+1]},
					{p.points[pointIdx+2], p.points[pointIdx+3]},
				}
				pointIdx += 4

			case CubicTo:
				elem.Points = []Point{
					{p.points[pointIdx], p.points[pointIdx+1]},
					{p.points[pointIdx+2], p.points[pointIdx+3]},
					{p.points[pointIdx+4], p.points[pointIdx+5]},
				}
				pointIdx += 6

			case Close:
				elem.Points = nil
			}

			if !yield(elem) {
				return
			}
		}
	}
}

func ElementsWithCursor

ElementsWithCursor returns an iterator that includes the current cursor position.

This is useful when you need to know the starting point of each segment.

func (p *Path) ElementsWithCursor() iter.Seq2[Point, PathElement] {
	return func(yield func(Point, PathElement) bool) {
		pointIdx := 0
		cursor := Point{0, 0}

		for _, verb := range p.verbs {
			var elem PathElement
			elem.Verb = verb
			prevCursor := cursor

			switch verb {
			case MoveTo:
				elem.Points = []Point{
					{p.points[pointIdx], p.points[pointIdx+1]},
				}
				cursor = elem.Points[0]
				pointIdx += 2

			case LineTo:
				elem.Points = []Point{
					{p.points[pointIdx], p.points[pointIdx+1]},
				}
				cursor = elem.Points[0]
				pointIdx += 2

			case QuadTo:
				elem.Points = []Point{
					{p.points[pointIdx], p.points[pointIdx+1]},
					{p.points[pointIdx+2], p.points[pointIdx+3]},
				}
				cursor = elem.Points[1]
				pointIdx += 4

			case CubicTo:
				elem.Points = []Point{
					{p.points[pointIdx], p.points[pointIdx+1]},
					{p.points[pointIdx+2], p.points[pointIdx+3]},
					{p.points[pointIdx+4], p.points[pointIdx+5]},
				}
				cursor = elem.Points[2]
				pointIdx += 6

			case Close:
				elem.Points = nil
				// cursor returns to subpath start (handled by caller if needed)
			}

			if !yield(prevCursor, elem) {
				return
			}
		}
	}
}

func Ellipse

Ellipse adds an ellipse path.

func (p *Path) Ellipse(cx, cy, rx, ry float32) *Path {
	// Magic number for approximating circular arcs with cubic beziers
	k := float32(0.5522847498)
	kx := k * rx
	ky := k * ry

	// Start at the right edge
	p.MoveTo(cx+rx, cy)

	// Four quarter-circle arcs
	p.CubicTo(cx+rx, cy+ky, cx+kx, cy+ry, cx, cy+ry)	// to bottom
	p.CubicTo(cx-kx, cy+ry, cx-rx, cy+ky, cx-rx, cy)	// to left
	p.CubicTo(cx-rx, cy-ky, cx-kx, cy-ry, cx, cy-ry)	// to top
	p.CubicTo(cx+kx, cy-ry, cx+rx, cy-ky, cx+rx, cy)	// to right (start)

	return p.Close()
}

func Get

Get retrieves a path from the pool or creates a new one.

func (pp *PathPool) Get() *Path {
	if len(pp.paths) > 0 {
		p := pp.paths[len(pp.paths)-1]
		pp.paths = pp.paths[:len(pp.paths)-1]
		p.Reset()
		return p
	}
	return NewPath()
}

func IsEmpty

IsEmpty returns true if the path has no commands.

func (p *Path) IsEmpty() bool {
	return len(p.verbs) == 0
}

func LineTo

LineTo draws a line from the current point to (x, y).

func (p *Path) LineTo(x, y float32) *Path {
	p.verbs = append(p.verbs, LineTo)
	p.points = append(p.points, x, y)
	p.bounds = p.bounds.UnionPoint(x, y)
	p.cursor = [2]float32{x, y}
	return p
}

func MoveTo

MoveTo begins a new subpath at the specified point.

func (p *Path) MoveTo(x, y float32) *Path {
	p.verbs = append(p.verbs, MoveTo)
	p.points = append(p.points, x, y)
	p.bounds = p.bounds.UnionPoint(x, y)
	p.start = [2]float32{x, y}
	p.cursor = [2]float32{x, y}
	return p
}

func NewPath

NewPath creates a new empty path.

func NewPath() *Path {
	return &Path{
		verbs:	make([]PathVerb, 0, 16),
		points:	make([]float32, 0, 64),
		bounds:	EmptyRect(),
	}
}

func NewPathPool

NewPathPool creates a new path pool.

func NewPathPool() *PathPool {
	return &PathPool{
		paths: make([]*Path, 0, 8),
	}
}

func PointCount

PointCount returns the number of float32 values in the point stream.

func (p *Path) PointCount() int {
	return len(p.points)
}

func PointCount

PointCount returns the number of points this verb consumes.

func (v PathVerb) PointCount() int {
	switch v {
	case MoveTo, LineTo:
		return 2	// x, y
	case QuadTo:
		return 4	// cx, cy, x, y
	case CubicTo:
		return 6	// c1x, c1y, c2x, c2y, x, y
	case Close:
		return 0
	default:
		return 0
	}
}

func Points

Points returns the point data stream.

func (p *Path) Points() []float32 {
	return p.points
}

func Put

Put returns a path to the pool for reuse.

func (pp *PathPool) Put(p *Path) {
	if p == nil {
		return
	}
	pp.paths = append(pp.paths, p)
}

func QuadTo

QuadTo draws a quadratic Bezier curve.

The curve goes from the current point to (x, y) using (cx, cy) as control point.

func (p *Path) QuadTo(cx, cy, x, y float32) *Path {
	p.verbs = append(p.verbs, QuadTo)
	p.points = append(p.points, cx, cy, x, y)
	p.bounds = p.bounds.UnionPoint(cx, cy)
	p.bounds = p.bounds.UnionPoint(x, y)
	// For accurate bounds, we should compute curve extrema,
	// but union with control points is a conservative approximation
	p.cursor = [2]float32{x, y}
	return p
}

func Rectangle

Rectangle adds a rectangle path.

func (p *Path) Rectangle(x, y, w, h float32) *Path {
	return p.MoveTo(x, y).
		LineTo(x+w, y).
		LineTo(x+w, y+h).
		LineTo(x, y+h).
		Close()
}

func Reset

Reset clears the path for reuse without deallocating memory.

func (p *Path) Reset() {
	p.verbs = p.verbs[:0]
	p.points = p.points[:0]
	p.bounds = EmptyRect()
	p.start = [2]float32{0, 0}
	p.cursor = [2]float32{0, 0}
}

func Reverse

Reverse returns a new path with the direction reversed.

This is useful for creating cut-out shapes.

func (p *Path) Reverse() *Path {
	if p.IsEmpty() {
		return NewPath()
	}

	result := NewPath()

	// Collect subpaths
	var subpaths []subpathData
	var current subpathData
	pointIdx := 0

	for _, verb := range p.verbs {
		switch verb {
		case MoveTo:
			if len(current.verbs) > 0 {
				subpaths = append(subpaths, current)
			}
			current = subpathData{
				verbs:	[]PathVerb{verb},
				points:	[]float32{p.points[pointIdx], p.points[pointIdx+1]},
				startX:	p.points[pointIdx],
				startY:	p.points[pointIdx+1],
			}
			pointIdx += 2
		case LineTo:
			current.verbs = append(current.verbs, verb)
			current.points = append(current.points, p.points[pointIdx], p.points[pointIdx+1])
			pointIdx += 2
		case QuadTo:
			current.verbs = append(current.verbs, verb)
			current.points = append(current.points, p.points[pointIdx:pointIdx+4]...)
			pointIdx += 4
		case CubicTo:
			current.verbs = append(current.verbs, verb)
			current.points = append(current.points, p.points[pointIdx:pointIdx+6]...)
			pointIdx += 6
		case Close:
			current.verbs = append(current.verbs, verb)
			current.closed = true
		}
	}
	if len(current.verbs) > 0 {
		subpaths = append(subpaths, current)
	}

	// Reverse each subpath
	for _, sp := range subpaths {
		reverseSubpath(result, sp)
	}

	return result
}

func RoundedRectangle

RoundedRectangle adds a rounded rectangle path.

func (p *Path) RoundedRectangle(x, y, w, h, r float32) *Path {
	// Clamp radius to half the minimum dimension
	maxR := min32(w, h) / 2
	if r > maxR {
		r = maxR
	}
	if r <= 0 {
		return p.Rectangle(x, y, w, h)
	}

	// Magic number for approximating circular arcs with cubic beziers
	// k = 4 * (sqrt(2) - 1) / 3 ≈ 0.5523
	k := float32(0.5522847498)
	kr := k * r

	// Start from top-left corner (after the rounded corner)
	p.MoveTo(x+r, y)

	// Top edge and top-right corner
	p.LineTo(x+w-r, y)
	p.CubicTo(x+w-r+kr, y, x+w, y+r-kr, x+w, y+r)

	// Right edge and bottom-right corner
	p.LineTo(x+w, y+h-r)
	p.CubicTo(x+w, y+h-r+kr, x+w-r+kr, y+h, x+w-r, y+h)

	// Bottom edge and bottom-left corner
	p.LineTo(x+r, y+h)
	p.CubicTo(x+r-kr, y+h, x, y+h-r+kr, x, y+h-r)

	// Left edge and top-left corner
	p.LineTo(x, y+r)
	p.CubicTo(x, y+r-kr, x+r-kr, y, x+r, y)

	return p.Close()
}

func String

String returns a human-readable name for the verb.

func (v PathVerb) String() string {
	switch v {
	case MoveTo:
		return "MoveTo"
	case LineTo:
		return "LineTo"
	case QuadTo:
		return "QuadTo"
	case CubicTo:
		return "CubicTo"
	case Close:
		return "Close"
	default:
		return unknownStr
	}
}

func Transform

Transform returns a new path with all points transformed by the affine matrix.

func (p *Path) Transform(t Affine) *Path {
	result := NewPath()
	result.verbs = make([]PathVerb, len(p.verbs))
	copy(result.verbs, p.verbs)
	result.points = make([]float32, len(p.points))

	// Transform all points
	for i := 0; i < len(p.points); i += 2 {
		x, y := t.TransformPoint(p.points[i], p.points[i+1])
		result.points[i] = x
		result.points[i+1] = y
		result.bounds = result.bounds.UnionPoint(x, y)
	}

	// Transform start and cursor
	result.start[0], result.start[1] = t.TransformPoint(p.start[0], p.start[1])
	result.cursor[0], result.cursor[1] = t.TransformPoint(p.cursor[0], p.cursor[1])

	return result
}

func VerbCount

VerbCount returns the number of verbs in the path.

func (p *Path) VerbCount() int {
	return len(p.verbs)
}

func Verbs

Verbs returns the verb stream.

func (p *Path) Verbs() []PathVerb {
	return p.verbs
}

Structs

type Path struct

Path represents a vector path for encoding.

It stores path commands (verbs) and coordinate data separately

for efficient processing and encoding.

type Path struct {
	verbs	[]PathVerb
	points	[]float32
	bounds	Rect
	start	[2]float32	// Start of current subpath for Close
	cursor	[2]float32	// Current position
}

type PathElement struct

PathElement represents a single path command with its associated points.

This type is used by the Elements() iterator for ergonomic path traversal.

type PathElement struct {
	// Verb is the path command type.
	Verb	PathVerb

	// Points contains the coordinates for this element.
	// The number of points depends on the verb:
	//   - MoveTo: 1 point (destination)
	//   - LineTo: 1 point (destination)
	//   - QuadTo: 2 points (control, destination)
	//   - CubicTo: 3 points (control1, control2, destination)
	//   - Close: 0 points
	Points	[]Point
}

type Point struct

Point represents a 2D point with float32 coordinates.

This is used by PathElement for iterator-based path traversal.

type Point struct {
	X, Y float32
}

type PathPool struct

PathPool manages a pool of reusable Path objects.

type PathPool struct {
	paths []*Path
}