The idea Most apps give you a static background.
I wanted Pairly to feel alive instead, so I built "Atmosphere": a real-time animated Canvas layer that sits behind every chat, fully tunable by the user, speed, density, opacity, brightness, saturation, all live.
There are currently over 40 atmospheres in the system, from calm ones like Snow and Fireflies to more elaborate ones like a black hole accretion disk called Abyss.
The interesting part wasn't drawing pretty particles.
It was making that work smoothly on a five-year-old Android phone without draining the battery in ten minutes.
Two rendering paths, not one Atmosphere isn't a single renderer, it's a small internal package () with two shared engines that every individual atmosphere builds on: , a generic particle system for anything made of many independent objects: snow, fireflies, sakura petals. , a raw draw-loop hook for continuous scenes that aren't particle-based, like Abyss's swirling accretion disk.
Both engines centralize every "don't destroy the device" concern in one place, so individual atmospheres never have to think about it.
Here's 's frame loop: fires at the display's native rate (often 90-120Hz on phones now), but that doesn't mean you should draw every single time it fires.
This accumulator pattern throttles actual drawing down to the target FPS from the device's performance profile, instead of trusting rAF's raw rate.
Profiling the device before drawing anything Before any atmosphere renders a single frame, it checks the device: That directly scales particle count: So a user's "density" slider is really a request, the actual particle count is that request scaled down by what the device can handle.
A low-end phone silently gets 45% of the particles a high-end desktop would, without the user ever seeing a settings toggle for it.
There's just a small note in the UI: "We've simplified effects to keep things smooth on this device." The DPR trap One line that's easy to skip and expensive if you do: Modern phones report of 3 or
4.
If you size your canvas buffer at native resolution on a low-end 4x-DPR phone, you're asking it to fill 16x the pixels of a naive 1x canvas, every frame.
Capping DPR at 1.25 for low-end devices was one of the single biggest performance wins in this whole system.
Two other cheap wins Pause when the tab isn't visible: No point animating a background nobody's looking at.
Respect : Users who've told their OS they don't want motion get one static frame instead of a continuous animation.
Accessibility setting handled, animation budget saved.
The preview grid is intentionally not Canvas The atmosphere picker shows a grid of live preview tiles, potentially ten or more on screen simultaneously.
Running ten independent Canvas/rAF loops just for a picker grid would be wasteful.
So previews use a completely separate, CSS-only component: Cheap CSS keyframes standing in for the real Canvas animation, good enough to give a sense of the effect without the actual rendering cost.
Right tool for a grid of thumbnails versus a full-screen live background.
An actual atmosphere: Abyss Most atmospheres are particle systems, but a few, like Abyss (a black hole with a swirling accretion disk), are hand-drawn continuous scenes on instead.
It layers three things every frame: rotating accretion rings drawn as radial-gradient strokes, particles spiraling inward and warping as they approach center, and a static photon ring + event horizon on top: does a lot of the visual work here, overlapping particles and rings add their light together instead of just painting over each other, which is what gives it that glowing look instead of a flat one.
The weekend unlock, no database required Most atmospheres are premium.
Every weekend, one random premium atmosphere goes free for everyone.
The fun detail: this needed zero database writes or scheduled jobs.
The pick is deterministic, derived from the week number modulo the number of premium atmospheres.
Every client computes the same answer independently.
No cron job, no row to write, no cache to invalidate.
It just falls out of the math.
What's next Still expanding the library (40+ atmospheres so far, from calm ones like Fireflies to elaborate ones like Abyss), and continuing to tune the performance profiling as more device data comes in.
If you're building something with live, user-tunable Canvas effects, I'd genuinely like to hear how you approached the low-end-device problem, that was the actual hard part here, not the visuals.
Try it live at pairly.chat.