The lighthouse at the top of our homepage is our logo brought to life: the sun breathes, its rays turn, the lantern sweeps its beam around, gulls circle and the water glints. It is a 12-second video that loops forever without a visible jump. We made it with Remotion, which renders React components frame by frame into video.
Start from the real logo
Instead of redrawing the logo, the scene uses the logo's own SVG coordinates. The sun is the logo's circle, the lighthouse is the logo's path. That keeps the animation unmistakably on brand, and positions like "the lamp is at (699, 410)" are the same numbers the website uses for the interactive beam that follows your cursor.
Everything is a function of the loop's time
The one rule that makes a loop seamless: never animate from the frame number directly. Convert the frame to a time inside the cycle, and make every value periodic over that cycle.
export const FPS = 30;
export const CYCLE = 12; // seconds
export const DURATION = FPS * CYCLE;
export const tauOf = (frame: number) =>
(((Math.round(frame) % DURATION) + DURATION) % DURATION) / FPS;
// k full waves per cycle; an integer k always returns to the start
export const wave = (tau: number, k = 1, phase = 0) =>
(1 - Math.cos(((tau / CYCLE) * k + phase) * 2 * Math.PI)) / 2;
With that in place:
- the lamp turns exactly once per cycle, so its angle at the last frame leads straight into the first;
- the sun breathes twice per cycle (
wave(tau, 2)); - the 18 rays rotate by exactly one ray-step per cycle, so the pattern looks identical at the seam even though it never stops moving;
- surface texture drifts along a closed circle, not a straight line;
- each star twinkles at its own integer frequency.
Draw the beam like a camera would
A lighthouse beam turning toward you gets shorter on screen and wider at the end, and flares when it points straight at you. Each beam is a soft polygon whose length follows sin(angle) and whose brightness follows cos(angle), with a lens flare that peaks when the beam faces the viewer. Two beams, half a turn apart, give two flashes per cycle.
Two themes, two sizes
The site has light and dark themes, so the scene renders twice, with the paper colour of each theme as background and a cream outline for the night version. Each theme is encoded at 1080 pixels for desktop and 720 for phones, in two formats, with a still poster from the first frame. The poster shows instantly; the video fades in once it plays, and because the poster is frame zero, you cannot see the hand-off.
The files are small: 140 to 640 KB per loop.
Check the seam with numbers
Eyes miss small jumps; numbers do not. After rendering, compare the first and the last frame:
ffmpeg -i loop.mp4 -vf "select=eq(n\,0)+eq(n\,359)" -vsync 0 f%d.png
compare -metric MAE f1.png f2.png null:
Our loops measure a mean difference of about one level out of 255, which is invisible.
Respect the visitor
The loop only starts after the page has loaded and only while it is on screen. It never plays for people who prefer reduced motion, or on Save-Data and very slow connections, where the poster simply stays. And on the theme switch, the other theme's loop is lined up to the same moment, so the beam does not jump when the page changes colour.
Video on iPhone has one more trap, which cost us an evening: why Safari would not play our video.