@celesta/math
Seeded randomness that renders the same way every time.
@celesta/math is a small, dependency-free toolkit for the numbers behind motion: seeded randomness, smooth noise, number shaping, waves, angles, and 2D points. It is bundled with the runtime, so a composition imports it directly:
import { random, randomRange, noise } from '@celesta/math';
Choose File Set Up TypeScript for editor types (see Your first React composition), or use it in the web editor, which accepts @celesta/react, @celesta/math, and react. The package is not published to npm yet.
This chapter covers randomness. Noise & fbm covers smooth drift, and shaping, waves & geometry covers the everyday helpers.
Why not Math.random()?
A composition is a function of the frame. The preview scrubs backward, the exporter renders frames in order, and both must draw the same picture for the same frame. Math.random() gives a different answer every call, so stars would jump around each time you scrub. Every function in @celesta/math is a pure function of its inputs: the same seed returns the same value in preview, in export, and on every machine.
Seeds
A Seed is a number or a string. random(seed) returns a number in [0, 1), and the same seed always returns the same number. Give each thing you randomize its own seed, and vary it by index and by property:
const x = randomRange(`star-${i}-x`, 0, 1920);
const y = randomRange(`star-${i}-y`, 0, 1080);
const size = randomRange(`star-${i}-size`, 2, 6);
- Reusing a seed reuses the value.
random('a')called twice is the same number, soxandyneed different seeds or the stars line up on a diagonal. - A fractional number is its own seed:
0.5does not collide with0or1. A number and a string that spells it, such as0.5and'0.5', are the same seed. - A seed must be a string or a finite number.
NaNandInfinitythrow an error that names the function. - To make a value change over time, put time in the seed:
random(Math.floor(frame / 4))jumps to a new value every four frames, which suits flicker and glitch effects. For smooth change, usenoise.
The random functions
Randomness
| API | Use it for |
|---|---|
random(seed) |
A number in [0, 1). |
randomRange(seed, min, max) |
A number in [min, max). |
randomInt(seed, min, max) |
A whole number from min to max, both included. Throws if no whole number lies between them. |
randomBool(seed, probability?) |
true with the given probability (0.5 by default). |
randomSign(seed) |
-1 or 1, each half the time. Handy for mirroring a direction or spin. |
randomPick(seed, items) |
One element of items. Throws on an empty list. |
shuffle(seed, items) |
A shuffled copy of items; the original is untouched. |
randomGaussian(seed, mean?, stdDev?) |
A bell-curve value: most land within stdDev of mean (defaults 0 and 1), a few land much further out. |
randomInCircle(seed, radius?, center?) |
A point spread evenly over a disc, as { x, y }. Defaults to radius 1 about the origin. |
A seeded starfield
Because the seeds are fixed, the field below is identical on every frame; only the drift, from noise, moves:
import { noise, randomRange } from '@celesta/math';
function Stars() {
const frame = useCurrentFrame();
return <>
{Array.from({ length: 80 }, (_, i) => (
<Rect key={i} width={4} height={4} cornerRadius={2} fill="#ffffff"
x={randomRange(`star-${i}-x`, 0, 1920) + noise(i, frame / 40) * 30}
y={randomRange(`star-${i}-y`, 0, 1080)}
opacity={randomRange(`star-${i}-o`, 0.2, 0.8)} />
))}
</>;
}
Choosing and ordering things
randomPick and shuffle pick from lists you already have, such as a palette or a set of captions. Seed them with something stable, like the item’s index:
const palette = ['#a68bbf', '#7cf29c', '#f2c97c', '#7cc4f2'];
const color = randomPick(`bar-${i}-color`, palette);
const order = shuffle('slides', ['intro', 'demo', 'pricing', 'outro']);
const mirrored = randomBool(`bar-${i}-flip`, 0.3); // flipped about 30% of the time
const spin = 45 * randomSign(`bar-${i}-spin`); // clockwise or counter-clockwise
Natural-looking scatter
Uniform randomness looks artificial: every value is as likely as any other. randomGaussian clusters values around a center, and randomInCircle spreads points evenly by area instead of crowding the middle of a disc, so a burst of particles looks round rather than starry-centered:
import { randomGaussian, randomInCircle } from '@celesta/math';
const particles = Array.from({ length: 120 }, (_, i) => {
const p = randomInCircle(`spark-${i}`, 260, { x: 960, y: 540 });
const size = Math.max(2, randomGaussian(`spark-${i}-size`, 8, 3));
return <Rect key={i} x={p.x} y={p.y} anchorX={0.5} anchorY={0.5}
width={size} height={size} cornerRadius={size / 2} fill="#f2c97c" />;
});