@ue-too/curve
This is a library that basically follows the algorithms in Pomax's superb Primer on Bezier Curves by Pomax. Of course, with my own interpretations on the inputs and outputs.
Bezier curve and geometric path library for TypeScript canvas applications.
Overview
@ue-too/curve provides comprehensive tools for working with Bezier curves, lines, and composite paths. Inspired by Pomax's Primer on Bezier Curves, this library includes advanced features like curve intersection detection, offset curves, arc fitting, and arc-length parameterization.
Key Features
- Bezier Curves: Quadratic and cubic Bezier curves with full geometric operations
- Curve Evaluation: Evaluate curves at any parameter
twith caching for performance - Arc-Length Parameterization: Uniform spacing along curves for animations and path following
- Intersection Detection: Self-intersections, curve-to-curve, curve-to-line, and curve-to-circle
- Geometric Queries: Project points, find closest points, calculate bounding boxes
- Advanced Operations: Split curves, offset curves, reduce complexity, fit arcs
- Composite Curves: Multi-segment curves with control points and tangent handles
- Paths: Sequential line segment paths with utilities
Installation
Using Bun:
bun add @ue-too/curveUsing npm:
npm install @ue-too/curveQuick Start
Here's a simple example creating and evaluating a Bezier curve:
import { BCurve } from '@ue-too/curve';
// Create a quadratic Bezier curve
const curve = new BCurve([
{ x: 0, y: 0 }, // Start point
{ x: 50, y: 100 }, // Control point
{ x: 100, y: 0 }, // End point
]);
// Evaluate at midpoint (t = 0.5)
const midpoint = curve.get(0.5);
console.log('Midpoint:', midpoint); // { x: 50, y: 50 }
// Get total arc length
console.log('Length:', curve.fullLength);
// Split curve at t = 0.5
const [left, right] = curve.splitIntoCurves(0.5);Core Concepts
Bezier Curves
Bezier curves are parametric curves defined by control points. This library supports:
- Linear Bezier (2 control points): Straight line
- Quadratic Bezier (3 control points): Simple curve with one control point
- Cubic Bezier (4 control points): Complex curve with two control points
// Linear Bezier (line)
const line = new BCurve([
{ x: 0, y: 0 },
{ x: 100, y: 100 },
]);
// Quadratic Bezier
const quadratic = new BCurve([
{ x: 0, y: 0 },
{ x: 50, y: 100 },
{ x: 100, y: 0 },
]);
// Cubic Bezier
const cubic = new BCurve([
{ x: 0, y: 0 },
{ x: 33, y: 100 },
{ x: 66, y: 100 },
{ x: 100, y: 0 },
]);Parameter t
Bezier curves are evaluated using a parameter t from 0.0 (start) to 1.0 (end):
const start = curve.get(0); // Start point
const mid = curve.get(0.5); // Midpoint
const end = curve.get(1); // End point
const quarter = curve.get(0.25); // 25% along the curveCore APIs
BCurve Class
Main Bezier curve class with extensive geometric operations.
Constructor:
const curve = new BCurve(controlPoints: Point[]);Curve Evaluation:
get(t: number): Point- Get point at parameter tderivative(t: number): Point- Get derivative vector at tnormal(t: number): Point- Get normal vector at ttangent(t: number): Point- Get tangent vector at t
Geometric Properties:
fullLength: number- Total arc length (cached)bbox(): {x: {min: number, max: number}, y: {min: number, max: number}}- Axis-aligned bounding boxextrema(): {x: number[], y: number[]}- Find extrema (min/max) pointscurvature(t: number): number- Calculate curvature at t
Curve Manipulation:
splitIntoCurves(t: number): [BCurve, BCurve]- Split into two curves at tscale(factor: number, origin?: Point): BCurve- Scale curve around originoffset(distance: number): BCurve | BCurve[]- Create offset (parallel) curve
Intersection Detection:
getCurveIntersections(other: BCurve): {selfT: number, otherT: number}[]- Find curve-curve intersectionsgetLineIntersections(line: Line): number[]- Find curve-line intersection pointsgetCircleIntersections(center: Point, radius: number): number[]- Find curve-circle intersectionsgetSelfIntersections(): {t1: number, t2: number}[]- Detect self-intersections
Point Queries:
project(point: Point): {t: number, point: Point, distance: number}- Project point onto curveclosestPoint(point: Point): Point- Find closest point on curve
Arc-Length Functions:
length(t: number): number- Arc length from start to parameter tparameter(length: number): number- Find parameter t for a given arc length
Line Class
Straight line segment utilities.
Constructor:
const line = new Line(start: Point, end: Point);Properties:
start: Point- Starting pointend: Point- Ending pointlength: number- Line length
Methods:
get(t: number): Point- Get point at parameter t (0-1)intersects(other: Line): Point | null- Find intersection point with another lineproject(point: Point): {t: number, point: Point}- Project point onto linedistanceToPoint(point: Point): number- Distance from point to line
CompositeBCurve Class
Composite curve made of multiple Bezier segments with control points and tangent handles.
Constructor:
const composite = new CompositeBCurve(controlPoints: ControlPoint[]);Control Point Structure:
type ControlPoint = {
point: Point; // Anchor point
leftHandle?: Point; // Left tangent handle
rightHandle?: Point; // Right tangent handle
};Path Class
Path composed of sequential line segments.
Constructor:
const path = new Path(points: Point[]);Methods:
get(index: number): Line- Get line segment at indexlength(): number- Total path lengthbbox(): {x: {min, max}, y: {min, max}}- Bounding box
Common Use Cases
Draw a Curve on Canvas
import { BCurve } from '@ue-too/curve';
const curve = new BCurve([
{ x: 50, y: 200 },
{ x: 150, y: 50 },
{ x: 250, y: 200 },
]);
// Draw using canvas API
ctx.beginPath();
ctx.moveTo(curve.points[0].x, curve.points[0].y);
if (curve.points.length === 3) {
// Quadratic curve
ctx.quadraticCurveTo(
curve.points[1].x,
curve.points[1].y,
curve.points[2].x,
curve.points[2].y
);
} else if (curve.points.length === 4) {
// Cubic curve
ctx.bezierCurveTo(
curve.points[1].x,
curve.points[1].y,
curve.points[2].x,
curve.points[2].y,
curve.points[3].x,
curve.points[3].y
);
}
ctx.stroke();Animate Object Along Curve
import { BCurve } from '@ue-too/curve';
const curve = new BCurve([
{ x: 0, y: 100 },
{ x: 100, y: 0 },
{ x: 200, y: 100 },
]);
let t = 0;
function animate() {
t += 0.01;
if (t > 1) t = 0;
const position = curve.get(t);
const tangent = curve.tangent(t);
// Draw object at position with rotation from tangent
const angle = Math.atan2(tangent.y, tangent.x);
drawSprite(position.x, position.y, angle);
requestAnimationFrame(animate);
}Uniform Spacing with Arc-Length Parameterization
import { BCurve } from '@ue-too/curve';
const curve = new BCurve([
{ x: 0, y: 0 },
{ x: 100, y: 100 },
{ x: 200, y: 0 },
]);
const totalLength = curve.fullLength;
const spacing = 10; // Pixels between points
const numPoints = Math.floor(totalLength / spacing);
// Place points uniformly along the curve
for (let i = 0; i <= numPoints; i++) {
const arcLength = i * spacing;
const t = curve.parameter(arcLength); // Convert arc-length to parameter
const point = curve.get(t);
drawPoint(point.x, point.y);
}Detect Curve Intersections
import { BCurve } from '@ue-too/curve';
const curve1 = new BCurve([
{ x: 0, y: 50 },
{ x: 100, y: 150 },
{ x: 200, y: 50 },
]);
const curve2 = new BCurve([
{ x: 0, y: 100 },
{ x: 100, y: 0 },
{ x: 200, y: 100 },
]);
const intersections = curve1.getCurveIntersections(curve2);
intersections.forEach(({ selfT, otherT }) => {
const point1 = curve1.get(selfT);
const point2 = curve2.get(otherT);
console.log('Intersection at:', point1);
// point1 and point2 should be very close (within numerical precision)
});Find Closest Point on Curve
import { BCurve } from '@ue-too/curve';
const curve = new BCurve([
{ x: 0, y: 0 },
{ x: 50, y: 100 },
{ x: 100, y: 0 },
]);
const mousePosition = { x: 60, y: 40 };
const { t, point, distance } = curve.project(mousePosition);
console.log('Closest point:', point);
console.log('At parameter:', t);
console.log('Distance:', distance);
// Snap mouse to curve
if (distance < 10) {
snapToCurve(point);
}Create Offset Curve
import { BCurve } from '@ue-too/curve';
const curve = new BCurve([
{ x: 0, y: 100 },
{ x: 100, y: 0 },
{ x: 200, y: 100 },
]);
// Create curve offset by 20 pixels
const offsetCurves = curve.offset(20);
// Offset may produce multiple curve segments
offsetCurves.forEach(offsetCurve => {
drawCurve(offsetCurve);
});Split Curve at Point
import { BCurve } from '@ue-too/curve';
const curve = new BCurve([
{ x: 0, y: 0 },
{ x: 50, y: 100 },
{ x: 100, y: 0 },
]);
// Split at 30% along the curve
const [leftPart, rightPart] = curve.splitIntoCurves(0.3);
// Draw each part in different colors
drawCurve(leftPart, 'red');
drawCurve(rightPart, 'blue');Bounding Box for Culling
import { BCurve } from '@ue-too/curve';
const curve = new BCurve([
{ x: 10, y: 20 },
{ x: 150, y: 200 },
{ x: 300, y: 50 },
]);
const bbox = curve.bbox();
// Check if curve is visible in viewport
const isVisible =
bbox.x.max >= viewport.left &&
bbox.x.min <= viewport.right &&
bbox.y.max >= viewport.top &&
bbox.y.min <= viewport.bottom;
if (isVisible) {
drawCurve(curve);
}API Reference
For complete API documentation with detailed type information, see the TypeDoc-generated documentation.
TypeScript Support
This package is written in TypeScript with complete type definitions:
import { BCurve, Line, Point } from '@ue-too/curve';
// Points are fully typed
const point: Point = { x: 10, y: 20 };
// Curves are generic over control point count
const quadratic: BCurve = new BCurve([
{ x: 0, y: 0 },
{ x: 50, y: 100 },
{ x: 100, y: 0 },
]);
// Intersection results are typed
const intersections: { selfT: number; otherT: number }[] =
curve1.getCurveIntersections(curve2);Design Philosophy
This library follows these principles:
- Geometric correctness: Implements algorithms from academic literature
- Performance: Caches expensive calculations (arc-length tables)
- Type safety: Leverages TypeScript for compile-time guarantees
- Composability: Build complex curves from simple primitives
- Practical: Focused on real-world canvas and graphics use cases
Performance Considerations
- Arc-length caching: First call to
fullLengthbuilds a lookup table, subsequent calls are O(1) - Intersection detection: Uses recursive subdivision (can be expensive for complex curves)
- Curve splitting: Fast operation using De Casteljau's algorithm
- Offset curves: Computationally expensive, may produce multiple segments
Performance Tips:
- Cache
fullLengthresults if curves don't change - Use bounding box checks before expensive intersection tests
- Reduce curve complexity with
reduce()for long curves - Split curves into segments for faster intersection detection
Related Packages
- @ue-too/math: Vector operations for point manipulation
- @ue-too/animate: Animate objects along curves
- @ue-too/border: Border rendering that can use curves
- @ue-too/board: Canvas board for rendering curves
Further Reading
- A Primer on Bezier Curves by Pomax - Comprehensive guide this library is based on
- bezier-js - Reference implementation in JavaScript
- De Casteljau's algorithm - Curve evaluation and splitting
License
MIT