EXPERIENCE PARIS
Case study · Interaction & front-end design

Turning a 12-second clip into a page you fly through

A scroll-driven narrative landing page where a 300-frame flight sequence — cabin window, cloud layer, then the Eiffel Tower — plays behind the content, controlled entirely by the scrollbar rather than a play button.

Role
Interaction design, front-end implementation
Surface
Marketing landing page, desktop-first responsive
Stack
Canvas 2D, IntersectionObserver, vanilla JS — no framework, no dependencies
Deliverable
Working page, generated from a build script
Status
Prototype — measured, not yet user-tested
01 — The brief

Make a static layout feel like motion, without turning it into a video

The raw material was a short flight clip exported as 300 sequential JPGs at 1920×1080. The narrative runs in one continuous move: an aircraft cabin window, out through a dense cloud layer, and finally the Eiffel Tower standing clear above the deck at night.

The obvious implementation is a looping background video. That was rejected early. A video plays on its own clock, which means the story runs whether or not the reader is there for it — and on a marketing page the reader is the one setting the pace. Tying the sequence to scroll position makes the reveal a consequence of reading rather than something happening alongside it.

Frame 1: view through an aircraft cabin window onto a moonlit cloud layer
Frame 1 — the cabin window
Frame 150: dense cloud with no landmark visible
Frame 150 — inside the cloud
Frame 195: the tower's spire first emerging from cloud
Frame 195 — first emergence
Final frame: the Eiffel Tower lit and clear above a flat cloud deck under stars
Resolve — the tower, clear
To be supplied — research

This case study documents design and engineering reasoning. It does not claim user research that was not conducted. To make it portfolio-complete, add:

  • Who the page is for, and what they were doing immediately before landing on it
  • The commercial goal this page serves, and how success was defined
  • Any competitive or comparative review that informed the scroll-narrative approach
  • Usability findings — particularly whether the reveal reads as intentional or as a loading artefact
02 — Constraints

Fixed inputs that shaped every decision after

ConstraintConsequence for the design
300 discrete frames, ~10 MB total Everything must be resident before it can be scrubbed. Rules out lazy-loading mid-sequence and forces a decision about what to show while loading.
Scroll is the only transport control No play, pause, or seek. The scrollbar becomes the timeline, so its length is a design parameter, not an accident of content height.
Source clip ends mid-cloud The last frame is a tight, busy composition. The intended resolve — a wide, calm shot — is a separate still that does not cut cleanly from it.
Content sections supplied as a fixed 1472 px layout Needed to become fluid without redrawing the design, and needed to stop painting over the sequence behind it.
Page must survive JS failure Any reveal or hide behaviour has to fail open — visible content, degraded motion — never a blank page.
03 — Design decisions

Seven decisions, and the reasoning behind each

Rendering model

One canvas, not 300 elements

The sequence is painted into a single <canvas> sized to the viewport, redrawn as the frame index changes. The alternative — 300 stacked <img> tags toggling visibility — puts 300 nodes in the layout and compositing tree and asks the browser to do work proportional to the sequence length on every frame.

Why it matters

Cost per rendered frame becomes constant instead of scaling with sequence length, which is what keeps the scrub responsive on a long sequence.

Motion quality

Separate what the scrollbar says from what the screen shows

Scroll events arrive irregularly — a trackpad flick delivers a burst, a mouse wheel delivers discrete jumps. Binding frame index directly to scroll position inherits that irregularity and the sequence stutters in step with the input device.

Instead the scroll handler only records a target. A requestAnimationFrame loop eases the rendered position toward it at 12% per frame, and snaps when the gap becomes imperceptible.

Why it matters

Playback gets its own motion curve rather than inheriting the input device's. The sequence glides to a stop instead of halting the instant fingers leave the trackpad.

Failure behaviour

Never freeze on a frame that hasn't arrived

The first implementation returned early when the requested frame hadn't decoded yet. Scrolling quickly while the tail of the preload was still in flight left the canvas stuck on an early cloud frame — the tower reveal, the entire point of the page, simply never painted.

The renderer now falls back to the nearest frame that has decoded, searching outward from the requested index, and swaps in the exact frame the moment it lands.

Why it matters

Degradation became temporal precision rather than a dead screen. A slightly stale frame reads as normal playback; a frozen one reads as a broken page.

Narrative continuity

Bridge two compositions instead of cutting between them

The sequence's last frame is a close, turbulent cloudscape. The intended payoff is a wide, still, star-lit shot of the tower. Appending the still as "frame 301" produces a hard cut — two unrelated compositions colliding at the exact moment the page is meant to feel resolved.

The final 20% of the scroll instead holds the last frame and dissolves into the still on a smoothstep curve, driven by the same eased progress value as the sequence itself.

Why it matters

Sharing one progress value means the dissolve inherits the identical inertia as the frames. Two separately-tuned curves would read as two different animations stitched together.

Scope of motion

Let the flight run the whole page, not just the hero

The sequence initially completed inside a tall hero, then handed off to conventional black sections. That split the page into "the animated bit" and "the rest".

Mapping progress to the full document instead — and making every section shell transparent — means the flight continues behind the content the entire way down. The tower resolves as the reader reaches the footer.

Why it matters

The reveal becomes the reward for reading the whole page, which aligns the motion with the commercial intent instead of front-loading it.

Typography

Put the headline where the image isn't

The supplied layout pinned the headline 280 px from the top of a 900 px frame — which landed it directly on the cabin window's brightest edge. Centring the block optically and breaking the headline into three tapering lines (363 / 227 / 145 px) sits it inside the window's dark aperture.

On short viewports the type scales down rather than the block repositioning, so it stays centred instead of drifting into the two pinned cards.

Legibility

Gradient for depth, dark where the words are

The service cards needed the brand blue without putting body copy on a mid-tone. The gradient runs top-to-bottom — saturated blue across the top third where only the category chip sits, resolving to near-black under the heading and paragraph.

Contrast was measured across the entire band the copy occupies (50–94% of card height), not sampled at one convenient point: 17.5:1 at worst, against a 7:1 AAA threshold.

Why it matters

A diagonal gradient matching the testimonial panel would have left saturated blue in one bottom corner — decorative consistency at the cost of the thing the card exists to do.

04 — How it's put together

One progress value drives everything

Scroll position normalises to 0…1 across the document. That single eased value derives the frame index, the dissolve opacity, and nothing else — so every moving part on the page shares one clock.

Scroll progress Frames 1 → 300 Dissolve 0% 80% 100% Content passing overhead Hero Trusted brands About + services Testimonial Features → footer Tower first emerges ≈ frame 190 · fully clear at the footer
300
Frames, preloaded before scrub
8.4px
Scroll per frame at 1440×900
17.5:1
Worst-case contrast under card copy
33
Elements with staggered reveals
0
Runtime dependencies

Reveals on top of the flight

Content animates in as it enters the viewport — headline lines in sequence, then subtext, CTA, and cards, 90 ms apart within each group. Two implementation traps were worth recording because both produce bugs that look like design problems:

Transition direction. Declaring the transition on the hidden state means elements animate into hiding when the class is applied after first paint — a visible fade-to-blank on load. The transition belongs on the settled state, so hiding is instant and only the reveal animates.

Specificity. .rv.in and .rv.ln carry equal weight, so source order decides. With the settled rule written first, headline lines would have stayed permanently offset.

05 — Accessibility & resilience

A motion-heavy page has to be safe to fail

ConditionBehaviour
Reduced-motion preference Reveal system exits before marking anything. All content renders in its settled state; no element is ever hidden.
JavaScript fails or is blocked Hiding classes are applied by script, never authored into the markup — so a script failure leaves the page fully readable rather than blank.
No IntersectionObserver Explicit guard reveals every element immediately instead of stranding content off-screen.
Frames still downloading Nearest decoded frame is drawn; the exact frame swaps in on arrival.
Body copy over gradient Measured 17.5:1 minimum across the full band the text occupies — AAA with margin.
Known gap — not yet addressed

Reduced-motion currently disables the reveals, but the scroll sequence itself still plays. For a user who set that preference because motion causes discomfort, a full-viewport flight sequence is the larger problem. The honest fix is to hold a single static frame — the resolved tower — and skip the scrub entirely. That is the first thing I would ship next.

06 — What I'd do next

Where this goes with more time

Static frame under reduced motion

Hold the resolved tower and bypass the sequence entirely, rather than only quieting the content reveals.

Cut the payload

~10 MB of JPEGs is the page's dominant cost. WebP or AVIF, plus a lower-resolution set for small viewports, is the obvious first pass.

Earn back frame density

At 8.4 px per frame the scrub is fast. More page length — or fewer, better-chosen frames — would give the reveal more room to breathe.

Validate the core assumption

The whole design rests on the belief that a scroll-controlled reveal reads as intentional. That needs testing against the possibility it reads as slow loading.

To be supplied — outcomes

No performance, engagement, or conversion data exists for this build. Add measured results here once live — scroll depth, time on page, completion rate of the sequence, and whether the reveal correlates with any downstream action.