path.go

Functions Structs

Functions

func Append

Append adds all elements from other to this path.

The current point and subpath start are updated to match other's state.

func (p *Path) Append(other *Path) {
	if other == nil || len(other.verbs) == 0 {
		return
	}
	p.verbs = append(p.verbs, other.verbs...)
	p.coords = append(p.coords, other.coords...)
	p.current = other.current
	p.start = other.start
}

func Arc

Arc adds a circular arc to the path.

The arc is drawn from angle1 to angle2 (in radians) around center (cx, cy).

func (p *Path) Arc(cx, cy, r, angle1, angle2 float64) {
	// Normalize angles
	const twoPi = 2 * math.Pi
	for angle2 < angle1 {
		angle2 += twoPi
	}

	// Split into multiple cubic Bezier curves
	// Maximum 90 degrees per segment
	const maxAngle = math.Pi / 2
	numSegments := int(math.Ceil((angle2 - angle1) / maxAngle))
	angleStep := (angle2 - angle1) / float64(numSegments)

	for i := 0; i < numSegments; i++ {
		a1 := angle1 + float64(i)*angleStep
		a2 := a1 + angleStep
		p.arcSegment(cx, cy, r, a1, a2)
	}
}

func Bounds

Bounds returns the axis-aligned bounding box of the path as image.Rectangle.

Computed incrementally during path construction (O(1) per operation).

Returns empty rectangle for empty paths.

func (p *Path) Bounds() image.Rectangle {
	if !p.boundsValid {
		return image.Rectangle{}
	}
	return image.Rect(
		int(math.Floor(p.boundsMinX)),
		int(math.Floor(p.boundsMinY)),
		int(math.Ceil(p.boundsMaxX)),
		int(math.Ceil(p.boundsMaxY)),
	)
}

func Circle

Circle adds a circle to the path using cubic Bezier curves.

func (p *Path) Circle(cx, cy, r float64) {
	// Magic constant for circle approximation with cubic Beziers
	const k = 0.5522847498307936	// 4/3 * (sqrt(2) - 1)
	offset := r * k

	p.MoveTo(cx+r, cy)
	p.CubicTo(cx+r, cy+offset, cx+offset, cy+r, cx, cy+r)
	p.CubicTo(cx-offset, cy+r, cx-r, cy+offset, cx-r, cy)
	p.CubicTo(cx-r, cy-offset, cx-offset, cy-r, cx, cy-r)
	p.CubicTo(cx+offset, cy-r, cx+r, cy-offset, cx+r, cy)
	p.Close()
}

func Clear

Clear removes all elements from the path, releasing the underlying storage.

func (p *Path) Clear() {
	p.verbs = p.verbs[:0]
	p.coords = p.coords[:0]
	p.start = Point{}
	p.current = Point{}
	p.boundsValid = false
}

func Clone

Clone creates a deep copy of the path.

func (p *Path) Clone() *Path {
	result := &Path{
		verbs:		make([]PathVerb, len(p.verbs)),
		coords:		make([]float64, len(p.coords)),
		start:		p.start,
		current:	p.current,
	}
	copy(result.verbs, p.verbs)
	copy(result.coords, p.coords)
	return result
}

func Close

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

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

func Coords

Coords returns the coordinate stream. The returned slice must not be modified.

func (p *Path) Coords() []float64 {
	return p.coords
}

func CubicTo

CubicTo draws a cubic Bezier curve.

func (p *Path) CubicTo(c1x, c1y, c2x, c2y, x, y float64) {
	p.verbs = append(p.verbs, CubicTo)
	p.coords = append(p.coords, c1x, c1y, c2x, c2y, x, y)
	p.expandBounds(c1x, c1y)
	p.expandBounds(c2x, c2y)
	p.expandBounds(x, y)
	p.current = Pt(x, y)
}

func CurrentPoint

CurrentPoint returns the current point.

func (p *Path) CurrentPoint() Point {
	return p.current
}

func Ellipse

Ellipse adds an ellipse to the path.

func (p *Path) Ellipse(cx, cy, rx, ry float64) {
	const k = 0.5522847498307936
	ox := rx * k
	oy := ry * k

	p.MoveTo(cx+rx, cy)
	p.CubicTo(cx+rx, cy+oy, cx+ox, cy+ry, cx, cy+ry)
	p.CubicTo(cx-ox, cy+ry, cx-rx, cy+oy, cx-rx, cy)
	p.CubicTo(cx-rx, cy-oy, cx-ox, cy-ry, cx, cy-ry)
	p.CubicTo(cx+ox, cy-ry, cx+rx, cy-oy, cx+rx, cy)
	p.Close()
}

func HasCurrentPoint

HasCurrentPoint returns true if the path has a current point.

A path has a current point after MoveTo, LineTo, or any curve operation.

func (p *Path) HasCurrentPoint() bool {
	return len(p.verbs) > 0
}

func HasCurves

HasCurves reports whether the path contains any quadratic or cubic curves.

func (p *Path) HasCurves() bool {
	for _, v := range p.verbs {
		if v == QuadTo || v == CubicTo {
			return true
		}
	}
	return false
}

func Iterate

Iterate calls fn for each verb in the path with the corresponding coordinate slice.

This is the primary zero-allocation iteration API.

 

The coords slice passed to fn is a sub-slice of the path's coordinate buffer:

- MoveTo: coords has 2 elements (x, y)

- LineTo: coords has 2 elements (x, y)

- QuadTo: coords has 4 elements (cx, cy, x, y)

- CubicTo: coords has 6 elements (c1x, c1y, c2x, c2y, x, y)

- Close: coords has 0 elements (nil)

func (p *Path) Iterate(fn func(verb PathVerb, coords []float64)) {
	ci := 0
	for _, v := range p.verbs {
		n := verbCoordCount(v)
		if n > 0 {
			fn(v, p.coords[ci:ci+n])
		} else {
			fn(v, nil)
		}
		ci += n
	}
}

func LineTo

LineTo draws a line to a point.

func (p *Path) LineTo(x, y float64) {
	p.verbs = append(p.verbs, LineTo)
	p.coords = append(p.coords, x, y)
	p.expandBounds(x, y)
	p.current = Pt(x, y)
}

func MoveTo

MoveTo moves to a point without drawing.

func (p *Path) MoveTo(x, y float64) {
	p.verbs = append(p.verbs, MoveTo)
	p.coords = append(p.coords, x, y)
	p.expandBounds(x, y)
	p.start = Pt(x, y)
	p.current = p.start
}

func NewPath

NewPath creates a new empty path with stack-backed buffers.

func NewPath() *Path {
	p := &Path{}
	p.verbs = p.verbsBuf[:0]
	p.coords = p.coordsBuf[:0]
	return p
}

func NumVerbs

NumVerbs returns the number of verbs in the path.

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

func QuadraticTo

QuadraticTo draws a quadratic Bezier curve.

func (p *Path) QuadraticTo(cx, cy, x, y float64) {
	p.verbs = append(p.verbs, QuadTo)
	p.coords = append(p.coords, cx, cy, x, y)
	p.expandBounds(cx, cy)
	p.expandBounds(x, y)
	p.current = Pt(x, y)
}

func Rectangle

Rectangle adds a rectangle to the path.

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

func Reset

Reset clears the path for reuse, keeping allocated capacity.

This is identical to Clear in the current implementation.

func (p *Path) Reset() {
	p.verbs = p.verbs[:0]
	p.coords = p.coords[:0]
	p.start = Point{}
	p.current = Point{}
}

func RoundedRectangle

RoundedRectangle adds a rectangle with rounded corners.

func (p *Path) RoundedRectangle(x, y, w, h, r float64) {
	// Clamp radius to half of the smaller dimension
	maxR := math.Min(w, h) / 2
	if r > maxR {
		r = maxR
	}

	p.MoveTo(x+r, y)
	p.LineTo(x+w-r, y)
	p.Arc(x+w-r, y+r, r, -math.Pi/2, 0)
	p.LineTo(x+w, y+h-r)
	p.Arc(x+w-r, y+h-r, r, 0, math.Pi/2)
	p.LineTo(x+r, y+h)
	p.Arc(x+r, y+h-r, r, math.Pi/2, math.Pi)
	p.LineTo(x, y+r)
	p.Arc(x+r, y+r, r, math.Pi, 3*math.Pi/2)
	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 "Unknown"
	}
}

func Transform

Transform applies a transformation matrix to all points in the path.

func (p *Path) Transform(m Matrix) *Path {
	result := NewPath()
	p.Iterate(func(verb PathVerb, coords []float64) {
		switch verb {
		case MoveTo:
			pt := m.TransformPoint(Pt(coords[0], coords[1]))
			result.MoveTo(pt.X, pt.Y)
		case LineTo:
			pt := m.TransformPoint(Pt(coords[0], coords[1]))
			result.LineTo(pt.X, pt.Y)
		case QuadTo:
			ctrl := m.TransformPoint(Pt(coords[0], coords[1]))
			pt := m.TransformPoint(Pt(coords[2], coords[3]))
			result.QuadraticTo(ctrl.X, ctrl.Y, pt.X, pt.Y)
		case CubicTo:
			ctrl1 := m.TransformPoint(Pt(coords[0], coords[1]))
			ctrl2 := m.TransformPoint(Pt(coords[2], coords[3]))
			pt := m.TransformPoint(Pt(coords[4], coords[5]))
			result.CubicTo(ctrl1.X, ctrl1.Y, ctrl2.X, ctrl2.Y, pt.X, pt.Y)
		case Close:
			result.Close()
		}
	})
	return result
}

func Verbs

Verbs returns the verb stream. The returned slice must not be modified.

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

Structs

type Path struct

Path represents a vector path using SOA (Structure of Arrays) layout.

 

Internally, the path stores verbs and coordinates in separate contiguous slices

for cache efficiency and zero per-verb heap allocations. This matches the

enterprise standard used by Skia, Cairo, tiny-skia, Blend2D, and femtovg.

type Path struct {
	verbs	[]PathVerb
	coords	[]float64
	start	Point	// Starting point of current subpath
	current	Point	// Current point

	// Incremental bounding box (Skia pattern: updated on every MoveTo/LineTo/etc).
	// O(1) per path operation, zero extra cost vs computing at Fill() time.
	boundsMinX, boundsMinY	float64
	boundsMaxX, boundsMaxY	float64
	boundsValid		bool

	// Embedded stack buffers for small paths (≤32 verbs, ≤10 cubics).
	// Avoids heap allocation for typical shapes (rect=5, circle=7, icon≤20 verbs).
	// Slices point to these buffers initially; grow to heap only if exceeded.
	verbsBuf	[32]PathVerb
	coordsBuf	[96]float64	// 96 = 16 cubics × 6 coords, or 48 lines × 2 coords
}