curve.go
Functions
func BoundingBox
func (l Line) BoundingBox() Rect {
return NewRect(l.P0, l.P1)
}
func BoundingBox
BoundingBox returns the tight axis-aligned bounding box of the curve.
func (c CubicBez) BoundingBox() Rect {
// Start with endpoints
bbox := NewRect(c.P0, c.P3)
// Include extrema points
for _, t := range c.Extrema() {
p := c.Eval(t)
bbox = bbox.Union(NewRect(p, p))
}
return bbox
}
func BoundingBox
BoundingBox returns the tight axis-aligned bounding box of the curve.
func (q QuadBez) BoundingBox() Rect {
// Start with endpoints
bbox := NewRect(q.P0, q.P2)
// Include extrema points
for _, t := range q.Extrema() {
p := q.Eval(t)
bbox = bbox.Union(NewRect(p, p))
}
return bbox
}
func Contains
Contains returns true if the point is inside the rectangle.
func (r Rect) Contains(p Point) bool {
return p.X >= r.Min.X && p.X <= r.Max.X && p.Y >= r.Min.Y && p.Y <= r.Max.Y
}
func Deriv
Deriv returns the derivative curve (a quadratic Bezier).
The derivative gives the tangent direction at any point.
func (c CubicBez) Deriv() QuadBez {
return QuadBez{
P0: Point{X: 3 * (c.P1.X - c.P0.X), Y: 3 * (c.P1.Y - c.P0.Y)},
P1: Point{X: 3 * (c.P2.X - c.P1.X), Y: 3 * (c.P2.Y - c.P1.Y)},
P2: Point{X: 3 * (c.P3.X - c.P2.X), Y: 3 * (c.P3.Y - c.P2.Y)},
}
}
func End
End returns the ending point of the curve.
func (c CubicBez) End() Point {
return c.P3
}
func End
End returns the ending point of the line.
func (l Line) End() Point {
return l.P1
}
func End
End returns the ending point of the curve.
func (q QuadBez) End() Point {
return q.P2
}
func Eval
Eval evaluates the curve at parameter t (0 to 1) using de Casteljau's algorithm.
func (q QuadBez) Eval(t float64) Point {
mt := 1.0 - t
// (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
return Point{
X: mt*mt*q.P0.X + 2*mt*t*q.P1.X + t*t*q.P2.X,
Y: mt*mt*q.P0.Y + 2*mt*t*q.P1.Y + t*t*q.P2.Y,
}
}
func Eval
Eval evaluates the curve at parameter t (0 to 1) using de Casteljau's algorithm.
func (c CubicBez) Eval(t float64) Point {
mt := 1.0 - t
mt2 := mt * mt
mt3 := mt2 * mt
t2 := t * t
t3 := t2 * t
// (1-t)^3 * P0 + 3(1-t)^2*t * P1 + 3(1-t)*t^2 * P2 + t^3 * P3
return Point{
X: mt3*c.P0.X + 3*mt2*t*c.P1.X + 3*mt*t2*c.P2.X + t3*c.P3.X,
Y: mt3*c.P0.Y + 3*mt2*t*c.P1.Y + 3*mt*t2*c.P2.Y + t3*c.P3.Y,
}
}
func Eval
Eval evaluates the line at parameter t (0 to 1).
t=0 returns P0, t=1 returns P1.
func (l Line) Eval(t float64) Point {
return l.P0.Lerp(l.P1, t)
}
func Extrema
Extrema returns parameter values where the derivative is zero (extrema points).
For a cubic Bezier, there can be up to 4 extrema (2 for x, 2 for y).
func (c CubicBez) Extrema() []float64 {
// Pre-allocate for max 4 extrema (2 for x, 2 for y)
result := make([]float64, 0, 4)
// The derivative is a quadratic: B'(t) = a*t^2 + b*t + c
// Where the coefficients come from differentiating the Bernstein form
d0 := c.P1.Sub(c.P0)
d1 := c.P2.Sub(c.P1)
d2 := c.P3.Sub(c.P2)
// X extrema: solve d0.X - 2*d1.X + d2.X = 0
ax := d0.X - 2*d1.X + d2.X
bx := 2 * (d1.X - d0.X)
cx := d0.X
result = append(result, SolveQuadraticInUnitInterval(ax, bx, cx)...)
// Y extrema
ay := d0.Y - 2*d1.Y + d2.Y
by := 2 * (d1.Y - d0.Y)
cy := d0.Y
result = append(result, SolveQuadraticInUnitInterval(ay, by, cy)...)
sort.Float64s(result)
return result
}
func Extrema
Extrema returns parameter values where the derivative is zero (extrema points).
Used for computing tight bounding boxes.
func (q QuadBez) Extrema() []float64 {
var result []float64
// For a quadratic Bezier, the derivative is linear:
// B'(t) = 2[(P1-P0) + t(P2-2P1+P0)]
// Setting to zero: t = (P0-P1) / (P0-2P1+P2)
d0 := q.P1.Sub(q.P0)
d1 := q.P2.Sub(q.P1)
dd := Point{X: d1.X - d0.X, Y: d1.Y - d0.Y}
// X extrema
if dd.X != 0 {
t := -d0.X / dd.X
if t > 0 && t < 1 {
result = append(result, t)
}
}
// Y extrema
if dd.Y != 0 {
t := -d0.Y / dd.Y
if t > 0 && t < 1 {
result = append(result, t)
}
}
sort.Float64s(result)
return result
}
func Height
Height returns the height of the rectangle.
func (r Rect) Height() float64 {
return r.Max.Y - r.Min.Y
}
func Inflections
Inflections returns the parameter values of inflection points.
An inflection point is where the curvature changes sign.
A cubic can have 0, 1, or 2 inflection points.
func (c CubicBez) Inflections() []float64 {
// See https://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html
a := c.P1.Sub(c.P0)
b := c.P2.Sub(c.P1).Sub(a)
cc := c.P3.Sub(c.P0).Sub(c.P2.Sub(c.P1).Mul(3))
// Cross products for the quadratic equation
// Solves: crossBC * t^2 + crossAC * t + crossAB = 0
crossAB := a.Cross(b)
crossAC := a.Cross(cc)
crossBC := b.Cross(cc)
// Note: SolveQuadratic expects a*t^2 + b*t + c = 0
roots := SolveQuadratic(crossBC, crossAC, crossAB)
var result []float64
for _, t := range roots {
if t >= 0 && t <= 1 {
result = append(result, t)
}
}
sort.Float64s(result)
return result
}
func Length
Length returns the length of the line segment.
func (l Line) Length() float64 {
return l.P0.Distance(l.P1)
}
func Midpoint
Midpoint returns the midpoint of the line segment.
func (l Line) Midpoint() Point {
return l.Eval(0.5)
}
func NewCubicBez
NewCubicBez creates a new cubic Bezier curve.
func NewCubicBez(p0, p1, p2, p3 Point) CubicBez {
return CubicBez{P0: p0, P1: p1, P2: p2, P3: p3}
}
func NewLine
NewLine creates a new line segment.
func NewLine(p0, p1 Point) Line {
return Line{P0: p0, P1: p1}
}
func NewQuadBez
NewQuadBez creates a new quadratic Bezier curve.
func NewQuadBez(p0, p1, p2 Point) QuadBez {
return QuadBez{P0: p0, P1: p1, P2: p2}
}
func NewRect
NewRect creates a rectangle from two points.
The points are normalized so Min <= Max.
func NewRect(p1, p2 Point) Rect {
return Rect{
Min: Point{X: math.Min(p1.X, p2.X), Y: math.Min(p1.Y, p2.Y)},
Max: Point{X: math.Max(p1.X, p2.X), Y: math.Max(p1.Y, p2.Y)},
}
}
func Normal
Normal returns the normal vector (perpendicular to tangent) at parameter t.
func (c CubicBez) Normal(t float64) Vec2 {
tan := c.Tangent(t)
return tan.Perp().Normalize()
}
func Raise
Raise elevates the quadratic to a cubic Bezier curve.
Returns an exact cubic representation of this quadratic.
func (q QuadBez) Raise() CubicBez {
// For a quadratic Q with points (P0, P1, P2), the cubic representation is:
// C0 = P0
// C1 = P0 + 2/3 * (P1 - P0) = (P0 + 2*P1) / 3
// C2 = P2 + 2/3 * (P1 - P2) = (2*P1 + P2) / 3
// C3 = P2
return CubicBez{
P0: q.P0,
P1: Point{
X: q.P0.X + (2.0/3.0)*(q.P1.X-q.P0.X),
Y: q.P0.Y + (2.0/3.0)*(q.P1.Y-q.P0.Y),
},
P2: Point{
X: q.P2.X + (2.0/3.0)*(q.P1.X-q.P2.X),
Y: q.P2.Y + (2.0/3.0)*(q.P1.Y-q.P2.Y),
},
P3: q.P2,
}
}
func Reversed
Reversed returns a copy of the line with endpoints swapped.
func (l Line) Reversed() Line {
return Line{P0: l.P1, P1: l.P0}
}
func Start
Start returns the starting point of the line.
func (l Line) Start() Point {
return l.P0
}
func Start
Start returns the starting point of the curve.
func (q QuadBez) Start() Point {
return q.P0
}
func Start
Start returns the starting point of the curve.
func (c CubicBez) Start() Point {
return c.P0
}
func Subdivide
Subdivide splits the curve at t=0.5 into two halves using de Casteljau.
func (q QuadBez) Subdivide() (QuadBez, QuadBez) {
mid := q.Eval(0.5)
return QuadBez{
P0: q.P0,
P1: q.P0.Lerp(q.P1, 0.5),
P2: mid,
}, QuadBez{
P0: mid,
P1: q.P1.Lerp(q.P2, 0.5),
P2: q.P2,
}
}
func Subdivide
Subdivide splits the curve at t=0.5 into two halves using de Casteljau.
func (c CubicBez) Subdivide() (CubicBez, CubicBez) {
// De Casteljau subdivision at t=0.5
p01 := c.P0.Lerp(c.P1, 0.5)
p12 := c.P1.Lerp(c.P2, 0.5)
p23 := c.P2.Lerp(c.P3, 0.5)
p012 := p01.Lerp(p12, 0.5)
p123 := p12.Lerp(p23, 0.5)
mid := p012.Lerp(p123, 0.5)
return CubicBez{P0: c.P0, P1: p01, P2: p012, P3: mid},
CubicBez{P0: mid, P1: p123, P2: p23, P3: c.P3}
}
func Subdivide
Subdivide splits the line at t=0.5 into two halves.
func (l Line) Subdivide() (Line, Line) {
mid := l.Eval(0.5)
return Line{P0: l.P0, P1: mid}, Line{P0: mid, P1: l.P1}
}
func Subsegment
Subsegment returns the portion of the curve from t0 to t1.
func (c CubicBez) Subsegment(t0, t1 float64) CubicBez {
p0 := c.Eval(t0)
p3 := c.Eval(t1)
// Calculate control points using derivative at endpoints
// The derivative at t is: 3[(P1-P0)(1-t)^2 + 2(P2-P1)(1-t)t + (P3-P2)t^2]
d0 := c.P1.Sub(c.P0)
d1 := c.P2.Sub(c.P1)
d2 := c.P3.Sub(c.P2)
scale := (t1 - t0) / 3.0
// Derivative at t0
mt0 := 1.0 - t0
deriv0 := Point{
X: 3 * (d0.X*mt0*mt0 + 2*d1.X*mt0*t0 + d2.X*t0*t0),
Y: 3 * (d0.Y*mt0*mt0 + 2*d1.Y*mt0*t0 + d2.Y*t0*t0),
}
p1 := Point{
X: p0.X + scale*deriv0.X,
Y: p0.Y + scale*deriv0.Y,
}
// Derivative at t1
mt1 := 1.0 - t1
deriv1 := Point{
X: 3 * (d0.X*mt1*mt1 + 2*d1.X*mt1*t1 + d2.X*t1*t1),
Y: 3 * (d0.Y*mt1*mt1 + 2*d1.Y*mt1*t1 + d2.Y*t1*t1),
}
p2 := Point{
X: p3.X - scale*deriv1.X,
Y: p3.Y - scale*deriv1.Y,
}
return CubicBez{P0: p0, P1: p1, P2: p2, P3: p3}
}
func Subsegment
Subsegment returns the portion of the line from t0 to t1.
func (l Line) Subsegment(t0, t1 float64) Line {
return Line{
P0: l.Eval(t0),
P1: l.Eval(t1),
}
}
func Subsegment
Subsegment returns the portion of the curve from t0 to t1.
func (q QuadBez) Subsegment(t0, t1 float64) QuadBez {
p0 := q.Eval(t0)
p2 := q.Eval(t1)
// Calculate the control point for the subsegment
// Using the property that the tangent at any point is the lerp of
// the control polygon edges
d0 := q.P1.Sub(q.P0)
d1 := q.P2.Sub(q.P1)
dt := (t1 - t0)
// Tangent direction at t0, scaled for the new segment
tanDir := Point{
X: d0.X + t0*(d1.X-d0.X),
Y: d0.Y + t0*(d1.Y-d0.Y),
}
p1 := Point{
X: p0.X + dt*tanDir.X,
Y: p0.Y + dt*tanDir.Y,
}
return QuadBez{P0: p0, P1: p1, P2: p2}
}
func Tangent
Tangent returns the tangent vector at parameter t.
func (c CubicBez) Tangent(t float64) Vec2 {
deriv := c.Deriv()
p := deriv.Eval(t)
return Vec2(p)
}
func Union
Union returns the smallest rectangle containing both r and other.
func (r Rect) Union(other Rect) Rect {
return Rect{
Min: Point{X: math.Min(r.Min.X, other.Min.X), Y: math.Min(r.Min.Y, other.Min.Y)},
Max: Point{X: math.Max(r.Max.X, other.Max.X), Y: math.Max(r.Max.Y, other.Max.Y)},
}
}
func Width
Width returns the width of the rectangle.
func (r Rect) Width() float64 {
return r.Max.X - r.Min.X
}
Structs
type Rect struct
Rect represents an axis-aligned rectangle.
Min is the top-left corner (minimum coordinates).
Max is the bottom-right corner (maximum coordinates).
type Rect struct {
Min, Max Point
}
type Line struct
Line represents a line segment from P0 to P1.
type Line struct {
P0, P1 Point
}
type QuadBez struct
QuadBez represents a quadratic Bezier curve with control points P0, P1, P2.
P0 is the start point, P1 is the control point, P2 is the end point.
type QuadBez struct {
P0, P1, P2 Point
}
type CubicBez struct
CubicBez represents a cubic Bezier curve with control points P0, P1, P2, P3.
P0 is the start point, P1 and P2 are control points, P3 is the end point.
type CubicBez struct {
P0, P1, P2, P3 Point
}
BoundingBox returns the axis-aligned bounding box of the line.