Aura
An agent drawn as an iridescent sphere in a rainbow halo, with a state for each part of a conversation: idle, listening, thinking, speaking, done and asleep.
Idle: breathing slowly, its colours drifting.
// Which way round the centre a pixel is, as a unit vector rather than an angle:
// an angle wraps somewhere, and the rainbow would show the seam. The palette at
// that angle, turned by an offset, is cos(θ + φ) = cos θ cos φ − sin θ sin φ.
const toward = q.div(max(r, 0.0001));
const around = (offset) => {
const phase = u.d.add(offset).mul(TWO_PI);
return u.a.add(u.b.mul(toward.x.mul(cos(phase)).sub(toward.y.mul(sin(phase)))));
};
// Inside, colour as a field: noise warped by noise, in 3D so it wraps the ball.
// (Offset into positive space: the noise has a seam where a coordinate crosses zero.)
const point = vec3(onDisc, z).mul(scale).add(vec3(OFFSET, u.flow.mul(0.07).add(OFFSET), u.flow.mul(0.19)));
const warp = mx_fractal_noise_float(point, 2, 2, 0.5);
const field = mx_fractal_noise_float(point.add(vec3(warp.mul(u.swirl.mul(0.9).add(0.2)))), 2, 2, 0.5);
// Outside, the halo is the rim's own colour, fading with distance; rings travel
// out through it while it listens.
const reach = exp(beyond.mul(-2.2).div(u.halo));
const rings = sin(beyond.mul(20).sub(u.rings)).mul(0.5).add(0.5).mul(u.ripple).mul(reach);
// Each state is a set of targets; every value springs toward its state's.
for (const key of KEYS) look[key] = springStep(look[key], LOOKS[state][key], dt);One shader, no sphere
There's no geometry. One square fills the canvas, and a TSL fragment shader works out each pixel's colour from how far it is from the centre in units of the radius, and, inside, which point of a ball it would be. The colour there is a field of noise warped by more noise, in three dimensions so it wraps the ball, flowing with time; at the rim the whole rainbow runs round the edge. Milky in the middle and deepening outwards, a shade darker just inside the edge and lit from the upper left with a soft gloss, it reads as thick glass lit from inside. Outside, each pixel takes the colour of the nearest point of the rim and fades with distance, so the halo is the sphere's own light, without a seam.
The colours come from cosine palettes, three triples making a smooth loop round the wheel, so a hue that keeps turning never jumps. Round the rim the shader never computes an angle, since an angle has to wrap somewhere and the rainbow would show where: it turns the palette with the direction itself, using cos(θ + φ) = cos θ cos φ − sin θ sin φ. The same source compiles to WebGPU or WebGL 2, whichever the browser has, and the shader smooths its own edge instead of multisampling.
States as targets
Each state is a handful of numbers: energy, how fast the colour flows and how much it churns, how far the halo reaches, rings in it, a light running round the rim, saturation, and how much breath, speech and a heard voice swell the sphere. Changing state changes the targets, and every value springs toward its own, damped just under critical, so one state melts into the next and the bloom of Done overshoots a little before it settles. Speech is syllables at four and a half a second, each its own loudness, in phrases with pauses; listening swells with a voice that's never quite still; idle breathes once every four and a half seconds.
It only draws while it's on screen and allowed to move. Under reduced motion it holds a still frame of each state, changes only when you pick one, and the conversation doesn't play.