Uzay

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 tt to a 3D point:

r(t)=(x(t),y(t),z(t)),t[tstart,tend]\vec{r}(t) = (x(t), y(t), z(t)), \quad t \in [t_{\text{start}}, t_{\text{end}}]

Instead of defining yy as a function of xx, 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:

  • x=tx = t (moves forward)
  • y=sin(t)y = \sin(t) (oscillates vertically)
  • z=cos(t)z = \cos(t) (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

PropertyTypeDefaultDescription
f(t: number) => Vec3(t) => vec3(t, t, t)The parametric function
tStartnumber0Parameter start value
tEndnumber1Parameter end value
samplesnumber64Number of sample points
colorstring"white"CSS color string
thicknessnumber1Line width
visiblebooleantrueWhether the curve is shown
pointerEvents"auto" | "none""auto"Whether the curve receives pointer events
tagsstring[][]Custom tags

Returned Item

FieldTypeDescription
idstringUnique identifier
fBoundAtom<(t: number) => Vec3>Function atom
tStartBoundAtom<number>Start parameter atom
tEndBoundAtom<number>End parameter atom
samplesBoundAtom<number>Sample count atom
colorBoundAtom<string>Color atom
thicknessBoundAtom<number>Thickness atom
visibleBoundAtom<boolean>Visibility atom
pointerEventsBoundAtom<string>Pointer events atom
tagsBoundAtom<string[]>Tags atom

On this page