Three Google Flow clips play as one continuous film behind the whole page: a roll-in, a turn into the close-up, and a breakdown into the ring's layers. Scroll position picks the frame, so it plays forwards and backwards exactly with your thumb. The copy, glass cards and stats fade in over it, and the specs slide up over a dimmed ring at the end.
Nothing to attach. The prompt streams the three ring clips straight from thinkingods.com, so just paste it, save what your AI gives back as
index.html and open it in Chrome. See the live build ↗ first. Want everything offline? Download the project: it has the finished index.html, the clips in an assets/ folder and a README with the four URLs to swap.The prompt
Paste this into Claude (or any AI that writes code), save the file it gives you as index.html and open it. Nothing to install, no server.
Build ONE self-contained file: index.html. No build step, no npm, no framework, no Tailwind, no JS libraries. Everything lives in a single document: inline <style>, inline SVG icons and vanilla JavaScript. The only things loaded from outside are two Google Fonts, three MP4 clips and one image hosted on thinkingods.com (listed below). The page is a dark, scroll-driven product landing page for a fictional white ceramic smart ring called VELA. One white ring sits in a fixed, full-screen film behind the whole page and never leaves the screen: it rolls in on load, turns into a centred close-up as you scroll, then slides apart into its six layers. Text overlays fade in and out over it, then the specs and buy sections slide up over a dimmed version of it. <title>: "VELA Ring". Language: en.
MEDIA (hosted: use these exact URLs, never generate, redraw or substitute them)
CLIP_INTRO https://thinkingods.com/demos/vela-ring/assets/ring-intro.mp4 1600x900, 2.7s. The ring rolls in from the right, tumbles and settles upright right of centre. The ease-out is baked into the file, so play it at normal speed. The ring is at rest from 1.85s.
CLIP_TURN https://thinkingods.com/demos/vela-ring/assets/ring-turn.mp4 1600x900, 7.8s. Starts on the exact last frame of CLIP_INTRO; the camera arcs round and the ring turns to face you, centred and larger.
CLIP_EXPLODE https://thinkingods.com/demos/vela-ring/assets/ring-explode.mp4 1600x900, 7.9s. Starts on the exact last frame of CLIP_TURN; the layers slide out along the ring's axis into a diagonal line: white ceramic shell, titanium ring, copper flex circuit, brushed inner frame, LED sensor module, clear inner ring.
RING_STILL https://thinkingods.com/demos/vela-ring/assets/ring-hero.webp 1920x1080, the hero pose (used inside the small dial).
All three clips are all-intra (every frame is a keyframe), so setting currentTime to any value shows that frame immediately. They are never shown as <video> elements on screen: they are decoded off-screen and drawn into one canvas.
FONTS
<link rel="preconnect" href="https://fonts.googleapis.com"> and <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>, then the stylesheet https://fonts.googleapis.com/css2?family=Michroma&family=Sora:wght@300;400;500;600&display=swap
Michroma = every headline, every big number and the brand word (always uppercase). Sora = all other text, weight 300 by default.
TOKENS (:root)
color-scheme:dark; --bg:#0A0A0B; --ink:#F2F2F0; --ink-2:rgba(242,242,240,.66); --ink-3:rgba(242,242,240,.42); --line:rgba(255,255,255,.12); --glass:rgba(22,22,24,.46); --lime:#B9F53C;
--f-display:"Michroma","Eurostile","Arial Black",system-ui,sans-serif; --f-body:"Sora","Helvetica Neue",Arial,system-ui,sans-serif;
--gut:max(20px, env(safe-area-inset-left,0px)); --gut-r:max(20px, env(safe-area-inset-right,0px))
Add <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">.
*{box-sizing:border-box} html,body{background:var(--bg)} body{margin:0;color:var(--ink);font-family:var(--f-body);font-weight:300;font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:inherit;text-decoration:none} button{font:inherit;color:inherit;background:none;border:0;cursor:pointer} :focus-visible{outline:1.5px solid var(--ink);outline-offset:3px;border-radius:999px}
.display{font-family:var(--f-display);font-weight:400;text-transform:uppercase;letter-spacing:.01em;line-height:1.08;margin:0} .thin = color var(--ink-3)
DOM ORDER
1) <div id="stage" aria-hidden="true"><canvas id="cv"></canvas></div>
2) <div id="clips" aria-hidden="true"> (position:fixed; width:1px; height:1px; overflow:hidden; opacity:0; pointer-events:none) that holds the three <video> elements
3) header.nav 4) nav.social 5) button.chev 6) <main id="top"> holding <section id="film"> and three overlay layers (hero, design, breakdown) 7) <div class="after"> holding #specs, #buy and the footer 8) <div class="loader" id="loader">Loading film</div>
STAGE (the fixed film)
#stage{position:fixed;inset:0;z-index:0} #stage canvas{width:100%;height:100%;display:block;transition:opacity .6s}
#stage::after = a vignette: absolute, inset 0, pointer-events none, background radial-gradient(120% 90% at 50% 45%, transparent 55%, rgba(0,0,0,.55) 100%).
body.dim #stage canvas{opacity:.28}
#film{position:relative;z-index:10;height:620vh} is an empty scroll track. Scroll progress p = clamp(scrollY / (film.offsetHeight - innerHeight), 0, 1).
VIDEO ENGINE (vanilla JS)
Create one <video> per clip with muted=true, playsInline=true, preload="auto", also setAttribute("muted","") and setAttribute("playsinline",""), no crossorigin attribute, and append it to #clips. Keep a per-video _target time.
seek(key, frac): remember key as the current clip; if the video has no duration yet, return; t = clamp(frac*duration, 0, duration-0.04); store it as _target; if |currentTime - t| < 0.02 just draw, otherwise set currentTime = t unless the video is already seeking.
On every "seeked": if this clip is the current one, draw it; then if |currentTime - _target| > 0.02 set currentTime = _target again (the user scrolled on during the seek). This chain is what keeps scrubbing smooth in both directions.
On every "loadeddata": count it, set the loader text to "Loading film N%", and add .done to the loader (opacity 0, transition .6s) when all three have loaded. Loader: fixed, left 50%, bottom 26px, translateX(-50%), z-index 35, 9.5px, letter-spacing .16em, uppercase, var(--ink-3).
DRAWING (canvas)
Size the canvas to clientWidth/Height x devicePixelRatio (cap DPR at 2). Skip the redraw when the clip, its currentTime and the framing values are unchanged. Fill with #0A0A0B, then draw the video frame (videoWidth x videoHeight) like this:
Landscape (canvas height <= width x 1.05): scale = max(W/vw, H/vh) x LZ (cover, then zoomed by LZ); x = (W - dw)/2 - LX x W; y = (H - dh)/2 + LY x H.
Portrait: scale = (W/vw) x Z; x = clamp(W/2 - FX x dw, W - dw, 0) so the ring stays in view; y = (H - dh)/2.
Whenever the drawn frame does not reach the canvas edges, fade it into the background: if y > 0, draw linear gradients from #0A0A0B to transparent over the top 18% and the bottom 18% of the frame's height and fill the bars above and below with #0A0A0B; if x > 0, do the same over 8% of its width on the left and right and fill the side bars. No hard edges ever.
ONE TIMELINE (p is the scroll progress)
p < 0.08 CLIP_TURN at frac 0 (the hero, identical to the last intro frame)
0.08 to 0.30 CLIP_TURN frac (p-0.08)/0.22
0.30 to 0.56 CLIP_TURN frac 1 (hold on the centred close-up)
0.56 to 0.86 CLIP_EXPLODE frac (p-0.56)/0.30
p >= 0.86 CLIP_EXPLODE frac 1 (hold on the exploded line)
FRAMING per phase (t clamped 0..1, lerp = linear): p < 0.30: FX = lerp(0.66, 0.50, (p-0.08)/0.22), Z = lerp(1.70, 1.55, same t), LZ 1, LY 0, LX 0. 0.30 to 0.56: FX 0.50, Z 1.55, LZ 1, LY 0, LX 0. p >= 0.56 with t = (p-0.56)/0.30: FX 0.50, Z = lerp(1.55, 1.02, t), LZ = lerp(1, 0.84, t), LY = lerp(0, 0.05, t), LX = lerp(0, 0.02, t). (The camera pulls back as the ring comes apart so the whole line fits, with room for the text in the empty corners.)
Before the intro finishes the canvas shows the intro clip.
INTRO (on load)
When CLIP_INTRO fires "canplay" (or straight away if readyState >= 3): if the page is already scrolled (scrollY > 4) skip the intro and reveal the hero at once; otherwise set currentTime 0, call play(), and draw it into the canvas every animation frame until it ends. HERO_SETTLE = 1.85: the first frame on which currentTime >= 1.85, reveal the hero copy (below). If play() is rejected, jump to the last frame and reveal. A click on the play button (#replay) while scrollY < 40 replays the whole intro, hiding the hero copy first.
OVERLAYS (fixed layers over the film)
.layer{position:fixed;inset:0;z-index:10;pointer-events:none;padding:96px calc(var(--gut-r) + 56px) 48px var(--gut);transition:opacity .5s} .layer .in{position:relative;height:100%} interactive children carry pointer-events:auto.
Every animated element has class .fx plus data-in and data-out (values below). .fx{opacity:var(--o,0);transform:translateY(calc((1 - var(--o,0)) * 28px));visibility:hidden} .fx.on{visibility:visible}
On every scroll frame set --o: o = clamp((p - in)/0.05, 0, 1); if p > out then o = max(0, 1 - (p - out)/0.04). Add .on while o > 0.01.
Hero elements use data-in="-1": they are fully visible (o = 1) at the top of the page, but until the intro has revealed them they stay at 0.
HERO REVEAL: add body.revealing, which gives .fx a transition of opacity .8s and transform .8s, both cubic-bezier(.22,.61,.36,1). Then mark the hero elements visible one by one, 70ms apart, in DOM order (title, sub, CTA, play, pulse card, recovery card), so each fades up 28px. When the last one is in, the intro counts as done; remove body.revealing 900ms later so scroll-driven fades stay instant.
When the .after block's top edge is above 55% of the viewport, add body.dim (canvas to .28 opacity, every .layer to opacity 0).
CHROME
.nav: fixed, top env(safe-area-inset-top), left 0, right 0, z-index 40, padding 0 var(--gut-r) 0 var(--gut). Inner row height 64px, flex, space-between, gap 24px.
Brand <a href="#top">: flex, gap 10px, Michroma 11px, letter-spacing .18em, text "VELA" after an 18px SVG (viewBox 0 0 20 20, stroke currentColor 1.6, no fill): circle cx 10 cy 10 r 7.5, plus a filled path "M10 2.5a7.5 7.5 0 0 1 0 15" (half-moon).
Links (centre): About, Design, Inside, Specs, Contact. 10px, letter-spacing .16em, uppercase, var(--ink-2), gap 44px, hover var(--ink). About, Design and Inside smooth-scroll to p = 0, 0.43 and 0.93 of the film; Specs goes to #specs, Contact to #buy.
Right: a 3x3 dot grid (3px dots, 3px gap, var(--ink-2), padding 8px), decorative.
.social: fixed, right var(--gut-r), top 50%, translateY(-50%), z-index 30, column, gap 26px. Three 34px round links "IG", "IN", "YT" (9px, letter-spacing .08em, var(--ink-2), 1px var(--line) border, background rgba(10,10,11,.4); hover var(--ink) with a var(--ink-3) border), opening instagram.com, linkedin.com and youtube.com in a new tab.
.chev: fixed, right var(--gut-r), bottom calc(22px + env(safe-area-inset-bottom)), 34px circle, 1px var(--line) border, var(--ink-2), a 12px chevron-down SVG (path "M2 4.5 6 8.5l4-4", stroke 1.4). Click: smooth-scroll to the next stop of [0, 0.43, 0.93] after the current p (+0.02); past the last stop go to #specs. Once the .after block is above 20% of the viewport the chevron rotates 180deg (transition .4s) and clicking it scrolls to the top.
HERO LAYER (data-in -1)
h1.display.h-title (data-out 0.07, aria-label "Forged by a band of light"): three block spans "Forged" / "by a band" / "of light"; the second indented padding-left .62em. Absolute, left 0, top 8%, font-size clamp(30px,4.6vw,74px).
p.h-sub (data-out 0.07): "A white ceramic smart ring that reads your heart rate, oxygen and recovery from the inside of your finger." Absolute, left .4em, top calc(8% + clamp(30px,4.6vw,74px) x 3.5), max-width 30ch, 14px, var(--ink-2).
div.h-cta (data-out 0.07): absolute, left .4em, bottom 6%, flex, gap 10px. a.buy "Buy now" (#buy): 46px tall, padding 0 26px, pill, background var(--lime), color #0A0A0B, weight 600, 12px, letter-spacing .08em, uppercase. Then a.arrow (#specs, aria-label "See specs"): 46px circle, 1px var(--line) border, background rgba(10,10,11,.4), 14px up-right arrow SVG (path "M4 10 10 4M5 4h5v5", stroke 1.3).
button.play#replay (data-out 0.05, aria-label "Replay the ring film"): absolute, left 49%, top 47%, 62px circle, background rgba(20,20,22,.5), backdrop-filter blur(14px), 1px var(--line) border, 16px filled triangle (path "M3 1.8v12.4L13.6 8z") nudged 3px right.
GLASS CARDS: .card{position:absolute;width:200px;padding:14px 16px 12px;border-radius:18px;background:var(--glass);backdrop-filter:blur(20px) saturate(1.3);border:1px solid var(--line)}. Top row: label (9px, letter-spacing .14em, uppercase, var(--ink-2)) left, and a 22px round 1px-bordered chip with a 9px up-right arrow (path "M2.5 7.5l5-5M3.5 2.5h4v4") right.
Card 1 .c-pulse (data-out 0.06), right 2%, top 6%: label "Pulse check"; value "72" (Michroma 30px, tabular numbers) with a small "BPM" (Sora 9px, .12em, var(--ink-3), vertical-align top, margin-left 6px). Every 2.4s the number becomes a random whole number from 70 to 74. Below: an SVG chart, viewBox 0 0 168 56, preserveAspectRatio none, 100% wide, 56px tall. Every animation frame rebuild a 41-point line: x = i/40 x 168, y = 40 - 22 x (i/40) - 6 sin(0.55i + T) - 5 sin(0.21i + 0.6T), with T growing 0.02 per frame. Stroke var(--lime) 1.6, round caps; the same path closed to the bottom ("L168,56 L0,56 Z") filled with a vertical gradient from #B9F53C at .35 opacity to 0; a 3px #F2F2F0 dot on point 30.
Card 2 .c-rec (data-out 0.06), right 16%, bottom 4%, width 178px: label "Recovery"; an 84px-tall half gauge, SVG viewBox 0 0 150 84 with two arcs "M15 78a60 60 0 0 1 120 0", stroke-width 8, round caps: a track in rgba(255,255,255,.1) and a lime arc with pathLength 100 and stroke-dasharray "86 100". Centred at its foot: "86" in Michroma 24px with a small "%".
DESIGN LAYER (hold on the close-up)
h2.display.m-title (data-in 0.30, data-out 0.53): "Zirconia" then on a new line a .thin span "ceramic shell". Absolute, left 0, top 10%, font-size clamp(26px,3.3vw,50px).
p.m-copy (data-in 0.34, data-out 0.53): "Polished to a mirror finish and fired for strength. Light enough to sleep in, hard enough to train in." Absolute, left .3em, top calc(10% + clamp(26px,3.3vw,50px) x 2.5 + 18px), max-width 28ch, var(--ink-2).
.stats: absolute, right 0, bottom 8%, grid, gap 22px, right-aligned. Three stats, data-in 0.36 / 0.39 / 0.42, all data-out 0.53: "3.4" + small "G" / Weight, "7" + "DAYS" / Battery, "100" + "M" / Water resistant. Number Michroma clamp(26px,2.6vw,38px); unit Sora 10px .14em var(--ink-3); label 10px .16em uppercase var(--ink-3), margin-top 6px.
BREAKDOWN LAYER (hold on the exploded line)
h2.display.x-title (data-in 0.86, data-out 2): "Train hard " + .thin "track", line break, "every " + .thin "move". Absolute, left 0, top 2%, font-size clamp(24px,3vw,46px).
.x-info (data-in 0.89, data-out 2): absolute, right 11%, bottom 2%, flex, align centre, gap 24px, max-width 480px.
Left of it a 132px .dial: a canvas (264x264 backing) with 60 ticks drawn round the edge, starting at 12 o'clock and going clockwise: every 5th tick is long (from radius R-6 to R-24, 3px wide), the others short (R-6 to R-16, 2px). The first 38 ticks are rgba(242,242,240,.85), the rest rgba(242,242,240,.22). Inside it, inset 26px, a round thumbnail (#141416 background, 1px var(--line) border, overflow hidden) holding RING_STILL with object-fit cover, object-position 62% 50%, scale(1.9) so only the ring shows.
Right of the dial: h3 "Six layers, one ring" (15px, weight 500, margin 0 0 10px) and p (12.5px, var(--ink-2), max-width 40ch): "A ceramic shell over a titanium core, a flex circuit and curved battery, the optical sensor array and a clear inner window that sits against your skin."
AFTER THE FILM (.after{position:relative;z-index:12}, scrolls up over the dimmed film)
#specs.panel: padding 120px calc(var(--gut-r) + 56px) 40px var(--gut); inner .wrap max-width 1180px, centred.
Eyebrow "Specifications" (10px, .18em, uppercase, var(--ink-3)), then h2.display "Small enough " + .thin "to forget" (clamp(26px,3.4vw,48px), margin 16px 0 48px, max-width 20ch).
A 4-column grid (gap 12px) of glass cells (padding 26px 22px, radius 20px, background var(--glass), backdrop-filter blur(22px), 1px var(--line) border): "7" DAYS / "Battery with continuous heart-rate tracking.", "3.4" G / "Weight, in sizes 6 to 13.", "100" M / "Water resistant for swims and showers.", "250" HZ / "Optical sampling during workouts." Numbers Michroma clamp(28px,3vw,44px) with an 11px unit; text 13px var(--ink-2), margin-top 12px.
Then a 3-column grid (gap 12px, margin-top 12px) of six layer cards (padding 18px 20px, radius 16px, 1px var(--line) border, background rgba(10,10,11,.35)): a 9.5px .16em uppercase var(--ink-3) key and a weight-500 value: "Layer 1 · outer" Zirconia ceramic shell; "Layer 2" Titanium core ring; "Layer 3" Flex circuit and MCU; "Layer 4" Curved 7-day battery; "Layer 5" Optical sensor array; "Layer 6 · inner" Clear epoxy window.
#buy.final: centred, padding 140px var(--gut-r) 120px var(--gut). Eyebrow "Pre-order · ships in 4 weeks", h2.display "Wear the light" (clamp(30px,5vw,72px), margin 16px auto 18px, max-width 14ch), p "Free sizing kit with every order. From ₹24,900." (var(--ink-2), max-width 40ch, margin 0 auto 34px), then the same Buy now pill and arrow circle, centred, gap 10px.
footer: padding 26px calc(var(--gut-r) + 56px) 34px var(--gut), 1px var(--line) top border, flex space-between, wrap, 11px, var(--ink-3), background rgba(10,10,11,.6): "VELA Ring · concept by Thinking Gods" and "Visuals generated in Google Flow".
RESPONSIVE
max-width 980px: hide the nav links and the Recovery card; stats gap 14px.
max-width 640px: hide .social; .layer padding 84px var(--gut-r) 28px var(--gut); .panel padding 96px var(--gut-r) 32px var(--gut); footer padding 24px var(--gut-r) 30px var(--gut); hero title top 2% and sub top calc(2% + clamp(30px,4.6vw,74px) x 3.5) at 13px; play button left calc(50% - 31px), top 50%; pulse card right 0, top auto, bottom 14%, width 170px; CTA bottom 2%; design title top 2%, copy top calc(2% + clamp(26px,3.3vw,50px) x 2.5 + 14px) at 13px; stats left 0, right 0, bottom 2%, three columns, left-aligned; breakdown title top 2%; .x-info left 0, right auto, bottom 2%, gap 16px; dial 96px with the thumbnail inset 18px; its paragraph 12px; specs grid 2 columns; layer cards 2 columns. On phones the canvas uses the portrait framing above, so the ring stays in frame at every point of the scroll.
ACCESSIBILITY
The stage, the clips container, decorative SVGs and the dot grid are aria-hidden. The hero h1 carries the full sentence as aria-label. Every icon-only link or button has an aria-label. @media (prefers-reduced-motion:reduce): skip the intro playback (jump to the last frame, then show the hero copy with no stagger), smooth scrolling becomes instant, the chart and BPM counter stay still, and the canvas fade has no transition.
FALLBACKS
If a clip has not loaded yet, nothing is drawn for it (the background stays #0A0A0B) and the rest of the page still works. If the fonts are slow, the page renders with the fallback stacks. Scroll is listened to passively and every update runs inside one requestAnimationFrame per frame. Resizing redraws the current frame.
DONE WHEN
1) On load the white ring rolls in from the right on a black studio floor, tumbles and settles upright right of centre, and the moment it stops the headline, sub, Buy now, play button and both glass cards fade up one after another. 2) Scrolling turns the same ring into a centred close-up, with "Zirconia ceramic shell" and the three stats fading in and out. 3) Scrolling on slides the ring apart into its six layers while the camera pulls back, and "Train hard track every move" with the tick dial appears in the empty corners. 4) Scrolling back up plays everything in reverse, frame for frame, with no jumps. 5) The specs and buy sections slide up over the dimmed ring, and the ring never leaves the screen. 6) The play button replays the intro; the chevron and nav jump between the stops. 7) On a 390px-wide phone the ring stays in frame through every phase and nothing scrolls sideways.Make it yours
| Swap | How |
|---|---|
| Your own product | Make three clips in Google Flow from one start image: a roll-in (or film it rolling out and reverse it), a turn into a close-up, and a breakdown. Each clip's first frame must be the previous clip's last frame. Encode them all-intra with the ffmpeg line in the README and swap the four URLs. |
| Your own words | Change the brand, the three headlines, the stats and the specs. Every line of copy is listed verbatim in the prompt, so edit it there before you paste. |
| Your own pacing | Change the scroll stops in the timeline (0.08, 0.30, 0.56, 0.86), the film length 620vh, or HERO_SETTLE to bring the hero copy in earlier or later. |


