Skip to main content
← Blog

One Revolution Every Three Minutes — Lessons from Rotating a Background Sky

VauDium·

Behind fecit's task list sits a night sky drawn from real stars. I set it rotating around Polaris. Even a decoration nobody will notice turned out to have physics, state, and a budget.

One Revolution Every Three Minutes — Lessons from Rotating a Background Sky

Fecit’s task list has a night sky behind it. I scattered stars in the background so the screen wouldn’t feel empty on light days — but they aren’t random decorative dots. They’re real stars from the Yale Bright Star Catalogue, drawn with an azimuthal equidistant projection centered on the celestial pole.

The exact center of the screen is the pole, and the fecit logo sits there. Follow the two pointer stars of the Big Dipper and they really do lead to the logo — fecit stands where Polaris would be. Users in the southern hemisphere get the southern sky instead, Southern Cross included.

Once that was in place, I got greedy about one more thing.

The real sky rotates

Watch the night sky long enough and the stars move. The Earth turns, so the whole sky appears to wheel around Polaris once a day — diurnal motion.

Having a background built from a real star catalogue where nothing moved felt like a stuffed specimen. So I set it rotating, in the true directions: counterclockwise for the northern sky, clockwise for the southern.

I settled on one revolution every three minutes. That’s two degrees per second — a 480x time-lapse of the actual sky. If you stare, you can see it turn; if you’re just glancing up from your tasks, it registers as “huh, that looks slightly different.” A background should stay a background.

Decoration must not eat the battery

My first worry was cost. There are hundreds of stars, and recomputing and redrawing them every frame would mean a decoration waking the JS thread forever.

The answer was simple: never redraw the stars. They’re drawn once onto an SVG canvas, and the canvas rotates as a whole. A single rotate transform with useNativeDriver runs the animation on the native thread via the GPU — JavaScript does nothing at all.

Animated.loop(
    Animated.timing(rotation, {
        toValue: 1,
        duration: ROTATION_PERIOD_MS, // 3 minutes
        easing: Easing.linear,
        useNativeDriver: true,
    }),
)

But rotate a rectangular canvas and its corners come up empty — at 45 degrees you’d see starless triangles peeking in at the screen’s corners. So in rotation mode the canvas becomes a square as wide as the screen’s diagonal. At any angle, the screen stays inside the sky.

The teleporting sky bug

After shipping it, I noticed something odd while switching between tabs: every time I came back, the sky had snapped to its original angle.

The cause was simple. When the screen loses focus, rotation stops to save battery — and on stopping, I was resetting the angle to zero. From the sky’s point of view, that’s teleportation. I had built a three-minute slow drift, then undone all of that time with a single tab switch.

The fix is pause and resume: save the progress on stop, continue from that angle on restart. There was one trap — React Native’s Animated won’t tell you the current value when you stop an animation. So I keep the start timestamp and compute the progress from elapsed time at the moment of pausing.

return () => {
    animation.stop();
    // Save progress from elapsed time — freeze at the stopped angle
    const elapsed = (Date.now() - startedAt) / ROTATION_PERIOD_MS;
    progressRef.current = (progressRef.current + elapsed) % 1;
    rotation.setValue(progressRef.current);
};

Now the sky waits exactly where it stopped.

If it can’t be seen, it doesn’t turn

One last detail. On busy days, task cards cover the entire screen and the background is invisible. Rotating a sky nobody can see is pure battery waste.

So when the list content fills the viewport, rotation stops. The sky only turns on days when scrolling reveals empty space — the light days. Which feels like the right rule, in hindsight: when you’re busy, there’s no time for stargazing anyway.

What I learned

It was one background decoration, but three things stuck with me.

Physics: if you start from real data, the motion has to follow real data too. A rotation in the wrong direction is instantly visible to anyone who knows the sky.

State: animations have state. Pausing and resetting are different things, and the slower the animation, the bigger that difference looks.

Budget: decoration isn’t free. The native driver takes the JS cost to zero, and when the sky can’t be seen, even that gets turned off.

A three-minute rotation nobody will consciously notice needed this many rules. I think that was precisely the cost of making it unnoticeable.