scene/path.go
Functions
func Arc
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
}
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.