Skip to content本文へ移動

math: shaping, waves & geometry

Number shaping, waves, angles, and 2D points.

The remaining helpers in @celesta/math reshape numbers, build repeating motion, and do the angle and point arithmetic that layout and orbits need. They are plain functions, so you can use them anywhere: in a component, in prepare(), or in a data file.

Shaping numbers

Scalars

API Use it for
clamp(value, min, max) Limits value to [min, max]. clamp01(value) limits it to [0, 1].
lerp(a, b, t) The value t of the way from a to b. t is not clamped, so values outside 0–1 extrapolate.
inverseLerp(a, b, value) The opposite: how far value is from a to b, as a t. Returns 0 when a === b.
remap(value, inMin, inMax, outMin, outMax) Moves a value from one range to another, without clamping. remapClamped keeps the result inside the output range.
step(edge, x) 0 below edge, 1 from edge on.
smoothstep(edge0, edge1, x) A smooth 0–1 ramp as x goes from edge0 to edge1. smootherstep has an even gentler start and finish.
fract(x) The fractional part, always in [0, 1), also for negative numbers.
mod(value, divisor) Modulo with the sign of the divisor: mod(-1, 4) is 3, not -1.
wrap(value, min, max) Wraps into [min, max), for looping positions and hues.
pingPong(value, length) Bounces a steadily increasing value back and forth between 0 and length.
snap(value, increment) Rounds to the nearest multiple of increment, for stepped or grid-locked motion.
roundTo(value, decimals?) Rounds to a number of decimals, for tidy on-screen readouts.
approxEqual(a, b, epsilon?) Whether two numbers differ by at most epsilon (1e-6 by default).
import { pingPong, snap, wrap } from '@celesta/math';

const frame = useCurrentFrame();
const ticker = wrap(frame * 4, -200, 1920);   // scrolls right, re-enters at the left
const sweep = pingPong(frame * 8, 1600);      // 0 → 1600 → 0, repeating
const stepped = snap(sweep, 100);             // the same sweep, in 100 px jumps

Waves

The four wave functions repeat with a period of 1 and return values in [-1, 1], so you pass frame / framesPerCycle. They are in phase: each is positive for the first half of a cycle and negative for the second, so you can swap one for another.

Waves

API Use it for
sineWave(t) A smooth sine. Gentle pulses, bobbing, breathing.
triangleWave(t) Straight ramps up and down. Constant-speed back-and-forth.
squareWave(t) 1 for the first half of each cycle, -1 for the second. On/off blinking.
sawtoothWave(t) Ramps from 0 up to 1, drops to -1 halfway, and ramps back to 0. Repeating sweeps.
import { sineWave } from '@celesta/math';

const { fps } = useVideoConfig();
const frame = useCurrentFrame();
const breath = sineWave(frame / (fps * 2)); // one full cycle every two seconds

<Rect x={960} y={540} anchorX={0.5} anchorY={0.5} width={200} height={200}
  cornerRadius={100} scale={1 + 0.08 * breath} fill="#a68bbf" />

Angles

Functions that take or return angles use radians. A layer’s rotation prop is in degrees, so convert with radToDeg when you hand it a computed angle.

Angles

API Use it for
TAU A full turn in radians (2π).
degToRad(degrees), radToDeg(radians) Convert between degrees and radians.
normalizeAngle(angle) Wraps an angle into [-π, π).
angleDifference(from, to) The shortest signed turn from one angle to another.
lerpAngle(a, b, t) Interpolates between angles the short way round, so 350° to 10° passes through 0° instead of sweeping back through 180°.

Points and geometry

Points are plain { x, y } objects, typed as Vec2. Celesta’s y axis points down, so a positive angle turns clockwise on screen, the same as rotation.

2D points

API Use it for
distance(a, b) The straight-line distance between two points.
angleBetween(from, to) The direction from one point to another, in radians; 0 points along +x.
lerpPoint(a, b, t), midpoint(a, b) A point along a segment, and the point halfway.
rotatePoint(point, angle, origin?) Turns a point about an origin (the coordinate origin by default).
polarToCartesian(angle, radius, center?), cartesianToPolar(point, center?) Convert between a direction-and-distance and an { x, y }. The second returns { angle, radius }.
quadraticBezierPoint(p0, p1, p2, t), cubicBezierPoint(p0, p1, p2, p3, t) The point at t (0–1) along a Bézier curve, as drawn by a path’s quadTo and cubicTo. Use them to move an object along a curve you also draw.
import { TAU, polarToCartesian } from '@celesta/math';

const frame = useCurrentFrame();
const dots = Array.from({ length: 12 }, (_, i) => {
  const angle = (i / 12) * TAU + frame / 60; // evenly spaced, slowly turning
  const p = polarToCartesian(angle, 300, { x: 960, y: 540 });
  return <Rect key={i} x={p.x} y={p.y} anchorX={0.5} anchorY={0.5}
    width={24} height={24} cornerRadius={12} fill="#a68bbf" />;
});
import { cubicBezierPoint } from '@celesta/math';

const p0 = { x: 200, y: 800 }, p1 = { x: 500, y: 200 };
const p2 = { x: 1400, y: 200 }, p3 = { x: 1700, y: 800 };
const t = progress(useCurrentFrame(), 0, 90, Easings.easeInOutCubic);
const marker = cubicBezierPoint(p0, p1, p2, p3, t);

<Path commands={[
  { type: 'moveTo', ...p0 },
  { type: 'cubicTo', x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, x: p3.x, y: p3.y },
]} stroke="#57456c" strokeWidth={4} />
<Rect x={marker.x} y={marker.y} anchorX={0.5} anchorY={0.5} width={32} height={32}
  cornerRadius={16} fill="#a68bbf" />

Both quadraticBezierPoint and cubicBezierPoint use the same control points as a Path’s quadTo and cubicTo commands, so the marker stays on the drawn curve. See Text effects, camera & lines for Path.

Try it without installing.

The playground runs Celesta in your browser, export included.

Open the playground