A sportswear campaign delivered as a playable Pong-style tennis game instead of a scrolling page. The viewport is the court: a blurred, grainy lookbook photo fills the screen, white court lines are drawn on top of it, and every rally event (hit, goal, countdown) is answered by a shader ripple and a small camera shake. Each point ends with a circular iris that opens at the ball and reveals a sharp product photo underneath the blur. The page is not a document: it never scrolls, it is a phase machine.
Rebuild: design-inspiration-rebuilds/games-entertainment/the-tie-break (fictional content, generated images, open-source fonts; installed with the optional design-inspiration-rebuilds skill). Run it with npm install && npm run dev.
Design Tokens
| Role | Value |
|---|---|
| White (type, lines, paddles, ball) | #ffffff |
| Ink (blue-black text on light) | #142a48 |
| Navy (plate fallback, loading) | #05152d |
| Photo palette | cobalt, cream, crimson, pale lilac-grey (blurred plates) |
:root {
--white:#fff; --ink:#142a48; --navy:#05152d;
--serif:'Instrument Serif',serif; --sans:'Work Sans',system-ui,sans-serif;
--out-expo:cubic-bezier(.19,1,.22,1);
--spring:cubic-bezier(.19,1.51,.29,.99);
--io-quad:cubic-bezier(.455,.03,.515,.955);
}
/* Poster scaling: every size is in em, root font-size is set from JS.
10px * min(vw/1920, vh/1080) landscape, min(vw/402, vh/732) portrait. */
- Type: a light humanist sans (Work Sans 300/500, original is a similar grotesk) for UI and labels, a high-contrast italic serif (Instrument Serif) for big headlines and score digits.
- Colour is borrowed from the photo. UI is pure white; all chroma comes from the blurred plate behind it, so each point changes the mood of the whole screen.
- No breakpoints: the layout scales as a poster (landscape 1920x1080, portrait 402x732) and portrait gets its own rotated court.
Motion Spec
| What | Trigger | Animation | Duration / easing | Rebuild file |
|---|---|---|---|---|
| Phase machine | click / game events | preloader, home, 4 intro steps, 3-2-1, play, reveal, end; page never scrolls | intro auto-advances every 5s | src/main.js |
| Court line draw-in | after preloader | each segment grows a to b with per-line stagger; net stubs lengthen while playing | 2.2s linear master, per-line power3.out |
src/fx/lines.js |
| Line colour | always | overlay blend with white, lines pick up the photo's hue | — | src/fx/lines.js |
| Blurred plate + grain | always | pre-blurred photo, 4% over-scale, slow sine drift, live film grain, cross-fade after each point | grain 12 fps, drift period ~60s | src/fx/background.js |
| Hit / countdown ripple | paddle hit, 3-2-1 | three exp rings expand and push the UV outward | 800ms / 900ms power2.out |
src/fx/ripples.js |
| Camera shake | hit, countdown, goal | random per-frame offset of the whole stage, linear decay | 300ms, amp .01 / .00585 / .1 | src/fx/shake.js |
| Point iris reveal | goal | circle opens at the ball, sharp photo zooms 3 to 1, radial displacement wave, 3-step glow edge | 2.1s progress, 2.2s zoom, power3.out |
src/fx/iris.js |
| Iris out | continue | inverted mask grows from screen centre, game returns | 1.1s power3.out |
src/fx/iris.js |
| Rally physics | play | ball speed .6 + .045 per hit (max 1.3), exit angle from paddle offset, predictive bot with per-point error | frame-rate normalised, dt <= 50ms | src/game/pong.js |
| Countdown pop | start | digit scales 1.8 to 1 then 1 to .5 | 450ms cubic-bezier(.19,1,.22,1) |
src/main.js |
| Poster scaling | resize | root font-size from min(vw/W, vh/H) | — | src/scale.js |
| UI feedback | click, hit | synthesised Web Audio blips, no sample files | 50-300ms | src/ui/audio.js |
Key excerpts
Court lines, overlay-blended: src/fx/lines.js
float segLine(vec2 p, vec4 s){
vec2 a = s.xy, b = s.zw, ba = b - a;
float h = clamp(dot(p - a, ba) / dot(ba, ba), 0., 1.);
float d = length(p - a - ba * h);
return 1. - smoothstep(uLineTh * .5 - .5, uLineTh * .5 + .5, d); // AA 2px segment
}
vec3 overlayBlend(vec3 b, vec3 s){
return mix(2. * b * s, 1. - 2. * (1. - b) * (1. - s), step(.5, b));
}
Lines are distance fields, so they can grow from a to b by animating the endpoint.
Ripple rings: src/fx/ripples.js
float d = length(p - r.xy) / H;
float rad = r.z * r.w * .09 * 2.2;
float w = exp(-28. * abs(d - rad)) + .5 * exp(-22. * abs(d - rad * .7)) + .3 * exp(-22. * abs(d - rad * .45));
return w * smoothstep(0., .08, r.z) * (1. - r.z);
Three concentric rings, faded in and out by progress; the sum also refracts the UV.
Iris reveal mask and wave: src/fx/iris.js
vec3 r = uIrisP * 1.08 - IR_OFF;
float m = (smoothstep(r.x + .02, r.x - .02, d) + smoothstep(r.y + .02, r.y - .02, d)
+ smoothstep(r.z + .02, r.z - .02, d)) / 3.; // soft 3-step edge
float wave = sin(dr * 35. - uTime * 6.) * exp(-abs(dr) * 40. / uIrisThick);
gsap.to(s, { z: 1, duration: 2.2, ease: 'power3.out' }) // image zoom 3 -> 1
return gsap.to(s, { p: 1, duration: 2.1, ease: 'power3.out' }).then()
Predictive bot: src/game/pong.js
const t = (1 - st.a) / Math.max(.05, st.speed * Math.cos(st.ang))
bot.tgt = fold(st.b + st.speed * Math.sin(st.ang) * (L.fl / L.fw) * t) + bot.err
// ...
function newPoint() { bot.maxV = rnd(.55, .9); bot.diff = rnd(.07, .17) } // difficulty re-rolled per point
fold is a triangle wave that mirrors the trajectory off the side walls.
Camera shake on the DOM stage: src/fx/shake.js
gsap.to(s, { a: 0, duration: .3, ease: 'none', onUpdate: tick, onComplete: tick })
// ...
const px = s.a * innerHeight * .35
el.style.transform = `translate(${(Math.random() - .5) * 2 * px}px,${(Math.random() - .5) * 2 * px}px) scale(1.012)`
prefers-reduced-motion: ripples and shake are skipped, the iris becomes a 0.35s wipe, line draw-in
and intro auto-advance are shortened or slowed.
Why Award-Worthy
- The product is the interaction. A campaign for sportswear is a game you actually play; the brand message arrives only at the end, after you have earned the attention.
- One shader pass carries the whole feel. Photo, grain, court lines, ripples and iris are one fragment shader, so every effect composites with the others and costs one draw call.
- Reward per point. The iris turns each goal into a small reveal of a sharp photograph under the blurred game, a loop that ties play to content.
- Feedback on every event. Hit, countdown and goal each get their own ripple size and shake amplitude; the game feels physical without any 3D scene.
- Poster scaling. An em-based UI driven by one root font-size keeps type and court in exact proportion on every screen, with a separately designed portrait court for phones.
Anti-patterns it avoids: no scrolling page hiding the content, no separate "game" section bolted onto a brochure site, no sample-based audio payload, no UI chrome competing with the court, no breakpoints that reflow the composition.
Credits
- Original: thetiebreak.merci-michel.com by Merci Michel. Design and content are the intellectual property of their creators.
- Recognition: Awwwards Site of the Day, Developer Award.
- This study is an independent clean-room analysis. The rebuild uses invented brand names and copy, AI-generated images and open-source fonts; it contains no original code, assets or copy.