Parametric Curves
3D curves defined by parametric equations
What is a Parametric Curve?
A parametric curve is defined by a function that maps a parameter to a 3D point:
Instead of defining as a function of , you define each coordinate independently as a function of the parameter. This allows for curves that loop back on themselves or form complex shapes.
Basic Usage
const helix = scene.create("parametricfunction3d", {
f: (t) => vec3(t, Math.sin(t), Math.cos(t)),
tStart: 0,
tEnd: 10,
color: "dodgerblue",
thickness: 1,
samples: 160,
});This draws a helix where:
- (moves forward)
- (oscillates vertically)
- (oscillates in depth)
How Sampling Works
The curve is approximated by connecting discrete sample points. The samples property controls how many points are computed:
// Low samples: jaggy curve
scene.create("parametricfunction3d", {
f: (t) => vec3(Math.cos(t), Math.sin(t), 0),
tStart: 0,
tEnd: Math.PI * 2,
samples: 8, // Octagon-ish
});
// High samples: smooth curve
scene.create("parametricfunction3d", {
f: (t) => vec3(Math.cos(t), Math.sin(t), 0),
tStart: 0,
tEnd: Math.PI * 2,
samples: 64, // Smooth circle
});Higher sample counts look better but has performance impacts.
Common Curves
Circle
scene.create("parametricfunction3d", {
f: (t) => vec3(Math.cos(t), Math.sin(t), 0),
tStart: 0,
tEnd: Math.PI * 2,
samples: 64,
});Helix
scene.create("parametricfunction3d", {
f: (t) => vec3(Math.cos(t), t * 0.2, Math.sin(t)),
tStart: 0,
tEnd: Math.PI * 6,
samples: 128,
});Lissajous Curve
scene.create("parametricfunction3d", {
f: (t) => vec3(Math.sin(3 * t), Math.sin(4 * t), Math.sin(5 * t)),
tStart: 0,
tEnd: Math.PI * 2,
samples: 256,
});Examples
Reactive Parameter Range
Animate a curve by making tEnd reactive:
const maxT = scene.atom(0);
const helix = scene.create("parametricfunction3d", {
f: (t) => vec3(t, Math.sin(t), Math.cos(t)),
tStart: 0,
tEnd: maxT,
color: "dodgerblue",
samples: scene.atom((get) => Math.max(16, get(maxT) * 16)),
});
// Animate: curve grows over time
function animate() {
maxT.set((prev) => prev + 0.05);
requestAnimationFrame(animate);
}
animate();Notice how samples is also reactive, it increases as the curve gets longer to maintain smoothness.
Reactive Function
The function itself can be an atom. This is powerful for curves that fundamentally change shape:
const offset = scene.atom(0);
const curve = scene.create("parametricfunction3d", {
f: scene.atom((get) => {
// Important: Get the value of the dependency outside the function
const x = get(offset);
// Return a new function that depends on offset
return (t: number) => vec3(x + t, Math.sin(t), Math.cos(t));
}),
tStart: 0,
tEnd: 10,
samples: 160,
});Moving Circle
A circle that slides along the x-axis:
const x = scene.atom(0);
const circleFunc = scene.atom((get) => {
const xPos = get(x);
return (t: number) => vec3(xPos, Math.cos(t), Math.sin(t));
});
const circle = scene.create("parametricfunction3d", {
f: circleFunc,
tStart: 0,
tEnd: Math.PI * 2,
color: "crimson",
samples: 64,
});Combining with Points
Highlight a point on a curve:
const t = scene.atom(0);
// The curve
const curve = scene.create("parametricfunction3d", {
f: (u) => vec3(u, Math.sin(u), Math.cos(u)),
tStart: 0,
tEnd: 10,
color: "dodgerblue",
samples: 160,
});
// A point that traces the curve
const point = scene.create("point3d", {
coords: scene.atom((get) => {
const u = get(t);
return vec3(u, Math.sin(u), Math.cos(u));
}),
color: "gold",
radius: 2,
});Draggable Point on a Curve
You can constrain a point so it's only draggable along a curve. The idea: use a "custom" drag mode with a handler that projects the camera ray onto the curve to find the nearest parameter t, then update a t atom that drives the point's position.
const f = (t: number) => vec3(
3 * Math.cos(t),
t * 0.3,
3 * Math.sin(t),
);
const curve = scene.create("parametricfunction3d", {
f,
tStart: -10,
tEnd: 10,
samples: 200,
color: "dodgerblue",
});
// t is the source of truth for the point's position
const tAtom = scene.atom(0);
const point = scene.create("point3d", {
coords: scene.atom((get) => f(get(tAtom))),
color: "gold",
radius: 3,
draggable: "custom",
});
point.on("drag", (event) => {
if (event.phase === "start") return;
// Find the t where the curve is closest to the camera ray.
// Starting from the current t and iterating with Newton's method
// converges quickly since we have a good initial guess each frame.
const nearest = findNearestTOnCurve(
f, event.ray, tAtom.get(), -10, 10
);
tAtom.set(nearest);
});The projection function finds the parameter t that minimizes the distance between the curve and the camera ray. Using the ray (instead of a projected 3D point) makes dragging feel responsive from any camera angle.
function findNearestTOnCurve(
f: (t: number) => Vec3,
ray: { origin: Vec3; direction: Vec3 },
currentT: number,
tStart: number,
tEnd: number,
): number {
const d = Vec3.normalized(ray.direction);
let t = currentT;
for (let i = 0; i < 8; i++) {
const v = Vec3.subtract(f(t), ray.origin);
// Component of v perpendicular to the ray
const proj = Vec3.dot(v, d);
const perp = Vec3.subtract(v, Vec3.scaled(d, proj));
// Numerical derivative f'(t)
const EPS = 1e-5;
const deriv = Vec3.scaled(
Vec3.subtract(f(t + EPS), f(t - EPS)),
1 / (2 * EPS),
);
// Newton step: move t to reduce the perpendicular distance
const g = Vec3.dot(deriv, perp);
const derivDotD = Vec3.dot(deriv, d);
const gPrime = Vec3.dot(deriv, deriv) - derivDotD * derivDotD;
if (Math.abs(gPrime) < 1e-12) break;
t = Math.max(tStart, Math.min(tEnd, t - g / gPrime));
}
return t;
}API Reference
Options
| Property | Type | Default | Description |
|---|---|---|---|
f | (t: number) => Vec3 | (t) => vec3(t, t, t) | The parametric function |
tStart | number | 0 | Parameter start value |
tEnd | number | 1 | Parameter end value |
samples | number | 64 | Number of sample points |
color | string | "white" | CSS color string |
thickness | number | 1 | Line width |
visible | boolean | true | Whether the curve is shown |
pointerEvents | "auto" | "none" | "auto" | Whether the curve receives pointer events |
tags | string[] | [] | Custom tags |
Returned Item
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
f | BoundAtom<(t: number) => Vec3> | Function atom |
tStart | BoundAtom<number> | Start parameter atom |
tEnd | BoundAtom<number> | End parameter atom |
samples | BoundAtom<number> | Sample count atom |
color | BoundAtom<string> | Color atom |
thickness | BoundAtom<number> | Thickness atom |
visible | BoundAtom<boolean> | Visibility atom |
pointerEvents | BoundAtom<string> | Pointer events atom |
tags | BoundAtom<string[]> | Tags atom |