Skip to content
FrontendCanvasPerformanceMotion

How we redesigned Intervolutions: a cinematic hero drawn in Canvas

David Abellán · · 2 min read
How we redesigned Intervolutions: a cinematic hero drawn in Canvas

Our site worked, but it did not tell anyone who we are. So we rebuilt it from scratch, with the same method we bring to a client project. This is the diary of how we put together the hero — the hardest part, and the most memorable — without giving up a frame of performance.

The starting point

We were coming from a background with green gradients and some connected constellation-style particles. It did the job, but it was not ours. We wanted something that said engineering + creativity at first glance: depth, slow and sophisticated motion, and an editorial, almost movie-trailer edge.

We set three rules before touching a line of code: dark and clean (midnight navy, not flat black), cobalt and cyan as the brand colors, and ambient motion that lifts the headline instead of competing with it.

Five layers, each with its own tool

The key to a background like this is not cramming everything into one place. We split it into layers and gave each one the tech that performs best: base gradient and vignette in pure CSS (zero cost), ambient glows as radial-gradient, and, most important, the orbital rings and particles in a single <canvas>.

The key decision

Reserve the Canvas for the one thing that truly benefits (dozens of moving dots and arcs) and leave the rest in static CSS. A DOM full of blur-animated divs would have been unmanageable.

Text that decodes itself

The tagline is "From zero to hero." We added a scramble effect: on load, "hero" and the whole subtitle decode from random characters to the final text, like a terminal. The important detail is that we made it framerate-independent: each character has its own window in milliseconds, so the decode always takes ~0.9s even on a slow machine.

// each character resolves by time, not by frames
const start = Math.random() * 420;               // ms
const end = start + 160 + Math.random() * 460;   // ends ~580–1040 ms
queue.push({ from: old[i] || '', to: newText[i] || '', start, end });

The war for 60 fps

This is where the project got interesting. The first version ran at 5 fps. Five. We measured layer by layer and found the culprits: mix-blend-mode and filter: blur() on large elements. Every frame, the compositor had to redo the blur of the entire screen because something (the canvas) was changing underneath.

5→60
fps after optimization
0
mix-blend-mode / blur at runtime
1
requestAnimationFrame loop

The fixes: switch the glows from filter: blur to radial-gradient (one paint, not a filter that re-rasterizes), remove all the mix-blend-mode, and move the rings from CSS (which rotated a conic mask and produced a subtle jitter) to Canvas, where they redraw cleanly on every frame.

90% of the performance of an animated background isn't in your algorithm: it's in not forcing the compositor to redo expensive layers every frame.

Mental note, commit at 2 a.m.

The result

A hero that shows from the very first pixel (the background slides under the transparent header, seamlessly), with a light that turns slowly around the headline, drifting particles, and the text decoding. And it respects prefers-reduced-motion: if you've asked for less motion, it freezes on a perfectly legible static frame.

What we took away

Performance and beauty aren't at odds, but they do fight. The version that's both pretty AND fast came from measuring instead of assuming: profiling each layer saved us hours of blind tweaking.

David Abellán
Co-founder · Engineering
Co-founder of Intervolutions. Architecture, code and infrastructure since 2010, with projects delivered in seven countries.
What we do about this when the project is yours

Ready to build something big?

Pick what you want to build, add a few extras, and watch your plan take shape in real time. No emails, no waiting. Go ahead — play.