Ring of light
Turning the bright side of the ring of light once around
The bright side of a ring of light drawn with gold dots travels along the rim of the dark circle. The dots never move; when the direction changes, only the brightness shifts. The speed rises a little partway through, then settles. At the loop's seam the speed before and after lines up, so the motion connects smoothly. The only thing that moves is the direction of the bright side, nothing else.
- Published
- June 15, 2026
- Topics
- Loop · Corona · Eclipse · Canvas
Set up the dark circle, the gold dots, and the bright side
- The dark circle, placed as a round empty space with nothing drawn, and the gold dots scattered only outside it
- The ring of gold dots, with fewer dots the farther out you go and the densest part toward the inside
- The bright side, where dots closer to the way
directionpoints are brighter and the far side is darker - The motion of
direction, which turns byTURNoverperiodFramesand is slowest at thebaseAngleheading
There are two functions to write. One returns the direction of the bright side from the frame number, and one decides the brightness of a single dot from that direction. The dots are scattered once at the start and never moved after that. The only thing that changes is the brightness.
// You animate ONE thing: direction, the lit side. It makes one full turn per loop, back to the start.
// The lowercase names (baseAngle, lobeStrength) are values you pick for your own screen —
// what matters is the structure, not the numbers.
const TURN = 360;
// The direction is one full turn plus a change in speed along the way. That change is a sine wave, so the speed matches at both ends of the loop.
function directionAt(local, periodFrames) {
const phase = local / periodFrames; // how far through the loop
const turn = TURN * phase; // one turn at a steady rate
const breath = rateDepth * Math.sin(2 * Math.PI * phase); // the change in speed along the way
return baseAngle + turn + breath; // the slowest heading is baseAngle
}
// The dots don't move. Each frame, only the brightness is recomputed from the gap to the direction.
function brightnessAt(dotAngle, direction, radial) {
const gap = ((dotAngle - direction) * Math.PI) / 180; // how far this dot is from the direction
const lit = 1 + lobeStrength * Math.cos(gap); // the closer to the direction, the brighter
return Math.max(0, Math.min(1, radial * lit)); // radial is the base brightness from distance to center
}breath is a sin wave, so it returns to the same value at the start and end of the loop, and the way the speed changes lines up at both ends too. That way the direction not only comes back to the same place after one turn, but the speed across the seam matches as well, so the motion connects smoothly. Raising rateDepth makes the speed difference larger, but raising it too far makes a sharp burst of acceleration partway through stand out and look unnatural.
The key point is that not a single dot disappears or appears. A dot's position is decided independently of the direction, and even on the darkest side the brightness only drops to the floor you set. So even as the bright side moves, the cluster of dots stays the same throughout the loop. The whole picture doesn't rotate; only the brightness moves in place. That is why it looks like the rim of living plasma.
Prepare plenty of dots and scatter them once at the start. Avoid the dark circle at the center and make the inside denser. After that you just scale the coordinates you fixed at the start to the screen size. There's no need to run the placement math every frame.
The animation runs with requestAnimationFrame. Multiply the elapsed time by the frame rate and take the remainder after dividing by periodFrames, which gives local, the current position within the loop. The finishing grain and chromatic aberration are layered over the picture after the brightness is recomputed.
All the direction decides is which side is bright. So even if you change how the dots are placed, fireworks or snow, the same directionAt and brightnessAt can move the bright side.