Free
✨ Creative
Cast & Render
Single-file scroll-scrubbed landing for a 3D studio: scrolling scrubs a fixed all-intra background video frame by frame while three text panels cross-fade over it, with no framework and no build step.
1.2k views 797 copies 3.1k từ
Nội dung prompt
# Cast & Render — Scroll-Scrubbed Video Landing
Build a single, self-contained `index.html` — one file, no build step, no dependencies, no frameworks. All CSS in one `<style>` block in `<head>`, all JS in one IIFE `<script>` at the end of `<body>`.
It is a scroll-scrubbed landing page for a fictional 3D studio called **Cast & Render**. Scrolling does not move the page content: it scrubs a fixed, full-screen background video frame by frame, while three text panels cross-fade in and out over it.
## 1. Document head
- `<!DOCTYPE html>`, `<html lang="en">`, `<meta charset="UTF-8">`
- `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">` — `viewport-fit=cover` is required for the safe-area insets used below
- `<title>Cast & Render — 3D Object Studio</title>`
- Preconnects, in this order: `https://fonts.googleapis.com`; `https://fonts.gstatic.com` (`crossorigin`); `https://d2ol7oe51mr4n9.cloudfront.net` (`crossorigin`)
- Font: `<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500&display=swap" rel="stylesheet">` — only Inter Tight, only 400 and 500. Body stack `'Inter Tight','Helvetica Neue',Helvetica,Arial,sans-serif`, base weight 400.
## 2. Custom properties and reset
```css
:root{
--fg:#0d0c0b;
--fg-soft:rgba(13,12,11,.64);
--fg-faint:rgba(13,12,11,.42);
--shade:#f2f0ec;
--rule:rgba(13,12,11,.16);
--ease:cubic-bezier(.22,.61,.36,1);
--pill-bg:#0a0908;
--pill-fg:#ffffff;
}
html{ -webkit-text-size-adjust:100%; }
```
`body`: background `var(--shade)`; colour `var(--fg)`; the font stack above; weight 400; `-webkit-font-smoothing:antialiased`; `-moz-osx-font-smoothing:grayscale`; `overflow-x:hidden`.
## 3. Markup order inside `<body>`
1. `<div class="boot" id="boot">` with `<div class="bar"><i id="bootBar"></i></div>` and `<p id="bootPct">LOADING 0%</p>`
2. `<div class="stage">` with `<video id="clip" muted playsinline preload="auto" disablepictureinpicture></video>` (**no** `src` — JS sets it), then `<div class="veil"></div>`, then `<div class="grain"></div>`
3. `<i class="meter" id="meter"></i>`
4. `<header class="chrome">` with `<div class="mark"><span class="mark-star" aria-hidden="true">✵</span> Cast & Render</div>` and `<nav class="nav">` holding `<a href="#board">Works</a>`, `<a href="#visit">About</a>`, `<a class="pill" href="#order">Start a brief</a>`
5. `<main class="panels">` with three `<section class="panel" data-panel>`
6. `<footer class="foot">112 Render Lane · Tue–Sun, 9am till sold out</footer>`
7. `<div class="track"></div>` — the only thing that gives the page height
8. The `<script>`
## 4. Fixed background video
```css
.stage{ position:fixed; inset:0; z-index:0; overflow:hidden; background:var(--shade); }
.stage video{ position:absolute; top:50%; left:50%; width:100%; height:100%;
transform:translate(-50%,-50%) scale(1.02); object-fit:cover; filter:contrast(1.02); will-change:transform; }
```
`.veil` — `position:absolute; inset:0; pointer-events:none;` with three background layers in exactly this order. The footage is high-key white, so this is a deliberately light wash, heavier at the top and bottom edges where the nav and footer sit:
```css
background:
linear-gradient(to bottom, rgba(242,240,236,.62) 0%, rgba(242,240,236,.12) 22%, rgba(242,240,236,.12) 78%, rgba(242,240,236,.66) 100%),
radial-gradient(100% 80% at 50% 48%, rgba(242,240,236,0) 0%, rgba(242,240,236,.34) 100%),
rgba(242,240,236,.20);
```
`.grain` — fine SVG turbulence noise so the flat wash doesn't band: `position:absolute; inset:-50%; opacity:.13; mix-blend-mode:multiply; pointer-events:none;` and
```css
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='140' height='140' filter='url(%23n)' opacity='.5'/></svg>");
```
(The `#` in `url(#n)` must be percent-encoded as `%23n`.)
## 5. Chrome, footer, scroll meter
`.chrome` — `position:fixed; left:0; right:0; top:0; z-index:40; display:flex; align-items:center; justify-content:space-between; gap:12px;` padding `max(14px, calc(env(safe-area-inset-top, 0px) + 12px)) clamp(16px, 3.4vw, 44px) 14px;` background `linear-gradient(to bottom, rgba(242,240,236,.94) 0%, rgba(242,240,236,.78) 72%, rgba(242,240,236,0) 100%);`
`.mark` — `display:flex; align-items:center; gap:9px; font-size:15px; letter-spacing:-.012em; color:var(--fg); min-width:0; flex-shrink:1;` · `.mark-star{ opacity:.85; }`
`.nav` — `display:flex; align-items:center; gap:clamp(14px, 2.4vw, 32px); flex-shrink:0;` · `.nav a:not(.pill)` — `color:var(--fg); text-decoration:none; font-size:14.5px; letter-spacing:-.008em; opacity:.88; transition:opacity .3s var(--ease);`, hover → opacity 1.
`.pill` — `display:inline-flex; align-items:center; justify-content:center; height:40px; padding:0 21px; border-radius:999px; background:var(--pill-bg); color:var(--pill-fg); font-size:14.5px; font-weight:500; letter-spacing:-.008em; text-decoration:none; white-space:nowrap; opacity:1; border:1px solid rgba(10,9,8,.12); box-shadow:0 1px 0 rgba(255,255,255,.10) inset; -webkit-text-fill-color:var(--pill-fg); transition:transform .4s var(--ease), background .3s var(--ease), color .3s var(--ease);`
`.nav .pill{ color:#fff; -webkit-text-fill-color:#fff; }` — required so the nav's inherited link colour never bleeds into the dark pill on mobile Safari. `.pill:hover, .pill:focus-visible` → `transform:translateY(-2px); background:#000; color:#fff; -webkit-text-fill-color:#fff;` · `.pill:focus-visible` → `outline:2px solid var(--fg); outline-offset:2px;`
`.foot` — `position:fixed; bottom:0; left:0; right:0; z-index:40; display:flex; justify-content:center;` padding `14px clamp(16px, 4vw, 24px) max(16px, calc(env(safe-area-inset-bottom, 0px) + 12px));` `font-size:12px; line-height:1.45; letter-spacing:.02em; color:var(--fg-faint); text-align:center; pointer-events:none;` background `linear-gradient(to top, rgba(242,240,236,.92) 0%, rgba(242,240,236,.72) 72%, rgba(242,240,236,0) 100%);`
`.meter` — `position:fixed; top:0; left:0; z-index:50; height:2px; width:100%; transform:scaleX(0); transform-origin:0 50%; background:var(--fg); opacity:.55;`
## 6. Panel layer
- `.panels` — `position:fixed; inset:0; z-index:20; pointer-events:none;`
- `.panel` — `position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center;` padding `max(104px, calc(env(safe-area-inset-top, 0px) + 88px)) clamp(20px, 5vw, 60px) max(96px, calc(env(safe-area-inset-bottom, 0px) + 80px));` `opacity:0; will-change:opacity,transform;` (JS drives opacity and transform every frame)
- `.eyebrow` — `display:flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:6px 12px; font-size:12.5px; letter-spacing:.045em; color:var(--fg-soft); margin-bottom:clamp(16px,2vw,22px); max-width:min(46ch, 100%); text-align:center;`
- `h1` — `font-weight:400; font-size:clamp(34px, 7.1vw, 104px); line-height:.98; letter-spacing:-.036em; max-width:15ch; text-wrap:balance;`
- `.sub` — `margin-top:clamp(18px,2.2vw,28px); font-size:clamp(15px,1.28vw,19px); line-height:1.5; letter-spacing:-.008em; color:var(--fg-soft); max-width:min(46ch, 100%); text-wrap:pretty;`
- `.cta` — `margin-top:clamp(28px,3.4vw,44px); pointer-events:auto; width:100%; display:flex; justify-content:center;` · `.cta .pill` — `height:48px; padding:0 27px; font-size:15px; max-width:min(100%, 320px);`
## 7. Panel copy (verbatim)
| | eyebrow | h1 | sub | CTA |
|---|---|---|---|---|
| 1 | `Objects studio <span>·</span> No. 112 Render Lane` | `Built at four.<br />Out by seven.` | Six kinds of mesh, one render farm, and a queue that starts before the sun does. | `#board` → **View the reel** |
| 2 | `Across the studio` | `Flat, never bent.` | The mesh should still be clean when it reaches the viewport. We export to order, never before. | `#visit` → **Tour our space** |
| 3 | `The surface` | `Smooth enough to<br />hold a light pass.` | Custom surface shaders whipped every morning, spread to the edge and weighed by the quarter pound. | `#order` → **Start a brief** |
## 8. Preloader
```css
.boot{ position:fixed; inset:0; z-index:90; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:16px; background:var(--shade);
transition:opacity .7s var(--ease), visibility .7s; }
.boot.done{ opacity:0; visibility:hidden; }
.boot p{ font-size:12.5px; letter-spacing:.05em; color:var(--fg-faint); }
.bar{ width:150px; height:1px; background:var(--rule); overflow:hidden; }
.bar i{ display:block; height:100%; width:100%; background:var(--fg); transform:scaleX(0); transform-origin:0 50%; }
```
The text reads `LOADING 0%` → `LOADING 100%`, uppercase, driven by JS.
## 9. Scroll track
`.track{ position:relative; z-index:1; height:560vh; min-height:3200px; }` — the entire page height. Nothing else contributes scroll length.
## 10. Responsive (exact, in this order)
- **`max-width:900px`** — strengthen the veil so dark type stays legible on small, bright screens:
```css
.veil{ background:
linear-gradient(to bottom, rgba(242,240,236,.72) 0%, rgba(242,240,236,.18) 24%, rgba(242,240,236,.18) 76%, rgba(242,240,236,.74) 100%),
radial-gradient(100% 80% at 50% 48%, rgba(242,240,236,0) 0%, rgba(242,240,236,.40) 100%),
rgba(242,240,236,.24); }
```
- **`max-width:720px`** — `.nav a:not(.pill){display:none}` · `.mark{font-size:14px}` · `.nav .pill{height:38px; padding:0 16px; font-size:13.5px}` · `h1{max-width:12ch; font-size:clamp(30px, 9.8vw, 52px)}` · `.sub{font-size:15px; max-width:34ch}` · `.panel{padding: max(92px, calc(env(safe-area-inset-top, 0px) + 76px)) 18px max(88px, calc(env(safe-area-inset-bottom, 0px) + 72px))}` · `.foot{font-size:11px; max-width:34ch}`
- **`max-width:420px`** — `.mark-star{display:none}` · `.nav .pill{height:36px; padding:0 14px; font-size:13px}` · `h1{max-width:11ch; font-size:clamp(28px, 10.5vw, 40px)}` · `.eyebrow{font-size:11px; letter-spacing:.04em; max-width:28ch}` · `.cta .pill{width:100%; max-width:280px; height:44px; padding:0 20px; font-size:14px}`
- **`max-height:520px` and `orientation:landscape`** — `.panel{padding: max(72px, calc(env(safe-area-inset-top, 0px) + 56px)) 24px max(64px, calc(env(safe-area-inset-bottom, 0px) + 48px))}` · `h1{font-size:clamp(28px, 8vh, 44px)}` · `.sub{margin-top:12px; font-size:14px}` · `.cta{margin-top:16px}`
## 11. JavaScript — `(function(){ "use strict"; … })();`
```js
var VIDEO_URL = "https://d2ol7oe51mr4n9.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/45567745-d826-44a2-a5ce-7ef670944e60.mp4";
```
Use exactly this URL. It is 1920×1080, 10.04s, 241 frames, and **all-intra** — every frame is a keyframe, which is why a scroll scrub can land on an exact frame instantly. Note this in a comment.
Refs: `clip`, `boot`, `bootBar`, `bootPct`, `meter`, and `panels = [].slice.call(document.querySelectorAll("[data-panel]"))`.
**Cue table.** Each panel owns a slice of scroll, `[fadeInStart, fadeInEnd, fadeOutStart, fadeOutEnd]` in 0..1 page progress. The gaps between one panel's `fadeOutEnd` and the next one's `fadeInStart` are deliberate dead zones — video only — so two panels are never readable at once:
```js
var CUES = [
[0.00, 0.00, 0.15, 0.23],
[0.35, 0.43, 0.57, 0.65],
[0.77, 0.85, 1.10, 1.20]
];
var DRIFT = 22; // px of counter-scroll travel per panel
```
**Helpers** — `clamp(v,a,b)`; `smooth(t){ return t*t*(3-2*t); }`; `ramp(p,a,b)`: if `b <= a` return `p >= b ? 1 : 0`, else `smooth(clamp((p-a)/(b-a),0,1))`.
**State** — `progress` (0..1 down the page), `seekTo` (target video time), `seekAt` (actual, eased), `duration`, `ready`.
`readScroll()`: `max = document.documentElement.scrollHeight - window.innerHeight; progress = max > 0 ? clamp(window.pageYOffset / max, 0, 1) : 0; if (duration) seekTo = progress * duration;`
`paint()`: set `meter.style.transform = "scaleX(" + progress + ")"`; for each panel `enter = ramp(progress, c[0], c[1])`, `leave = ramp(progress, c[2], c[3])`, `o = enter * (1 - leave)`, `y = (1 - enter) * DRIFT - leave * DRIFT`; set `opacity = o`, `transform = "translate3d(0," + y + "px,0)"`, and `pointerEvents = o > 0.6 ? "auto" : "none"`.
`frame()` — a rAF loop, never cancelled:
```js
if (ready && duration){
var gap = seekTo - seekAt;
if (Math.abs(gap) > 0.0008){
seekAt += gap * 0.115; // easing factor — do not change
if (clip.readyState >= 2 && !clip.seeking){
try { clip.currentTime = seekAt; } catch (e) {}
}
}
}
paint();
requestAnimationFrame(frame);
```
Easing `currentTime` toward the scroll target, rather than snapping, is what turns a jumpy scrub into a smooth one.
**Boot.** `setProgress(f)` sets `bootBar.style.transform = "scaleX(" + f + ")"` and `bootPct.textContent = "LOADING " + Math.round(f * 100) + "%"`. `start()` — idempotent via a `started` flag — sets `ready = true`, adds `done` to `boot`, calls `readScroll()`, sets `seekAt = seekTo`.
`attach(src)` — idempotent via an `attached` flag. Bind listeners **before** setting `src`: `loadedmetadata` → `duration = clip.duration || 0; clip.pause(); readScroll(); seekAt = seekTo;` then `try { clip.currentTime = seekAt; } catch(e){}`; `loadeddata`, `canplaythrough` and `error` → `start`. Then `clip.src = src; clip.load();` and `setTimeout(start, 12000);` so a stalled decode never strands the page behind the preloader.
`preload()` — fetch the mp4 as a fully buffered blob first, because seeking inside a buffered blob is near instant while range requests over the network are a slideshow:
- Create an `AbortController` if available
- A 15000ms `bail` timer: if not yet attached, abort the fetch, `setProgress(1)`, and `attach(VIDEO_URL)` to stream directly instead
- `fetch(VIDEO_URL, {signal})`; throw if `!res.ok || !res.body`
- Read `content-length` into `total`; pump `res.body.getReader()` recursively, pushing each chunk and accumulating `got`, calling `setProgress(total ? got / total : Math.min(got / 11e6, 0.95))` per chunk — 11e6 is the approximate byte size, used when the server sends no content-length
- On `r.done` resolve `new Blob(chunks, { type:"video/mp4" })`
- `.then(blob)` → `clearTimeout(bail); setProgress(1); attach(URL.createObjectURL(blob));`
- `.catch()` → `clearTimeout(bail); setProgress(1); attach(VIDEO_URL);` (CORS failure, abort, offline)
**iOS unlock.** iOS will not paint a frame from a video that has never been played, so nudge it once on the first interaction and pause immediately:
```js
function unlock(){
var p = clip.play();
if (p && p.then) p.then(function(){ clip.pause(); }).catch(function(){});
else clip.pause();
}
["touchstart","pointerdown","wheel","keydown"].forEach(function(ev){
window.addEventListener(ev, unlock, { once:true, passive:true });
});
```
**Wiring**, in this order at the end of the IIFE:
```js
window.addEventListener("scroll", readScroll, { passive:true });
window.addEventListener("resize", readScroll);
readScroll();
paint();
preload();
requestAnimationFrame(frame);
```
## 12. Requirements to hold to
- Exactly one file. No React, GSAP, Lenis, ScrollTrigger, Tailwind or bundler.
- Nothing scrolls visually except the video scrub and the panel cross-fades — header, footer, video and panels are all `position:fixed`.
- Panels are driven imperatively from the rAF loop — no IntersectionObserver, no CSS scroll-timeline, no CSS transitions on `.panel` opacity/transform.
- Keep the light, off-white editorial look: paper `#f2f0ec`, near-black ink `#0d0c0b`, tight negative tracking on display type, grain over the whole stage.
- Mobile: touch targets ≥36px tall, the nav collapses to the single dark pill, the pill's white text forced with `-webkit-text-fill-color`, all safe-area insets respected, no horizontal overflow at 320px.
- Include the short explanatory comments described above.
## Quality Bar
- **The video claims are all true, and the scrub works exactly as designed.** Checked against the file itself: 1920×1080, H.264 High, 24fps, 10.04s, **241 frames, every one an I-frame**, no B-frames, no audio track. Driven in headless Chrome 153, the preloader lifted via the blob path and the scrubbed video settled within **0.02 of a frame** of its scroll target at every one of eleven sampled positions from top to bottom. The cue table does what it says: panel 1 alone to 0.15, video-only dead zones at 0.29 and 0.71, panel 3 holding to the end.
- **The reset never zeroes the UA margins, and they overwrite the spacing the spec so carefully sets.** The spec gives every gap an explicit clamp — `.eyebrow` margin-bottom up to 22px, `.sub` margin-top up to 28px — but the `h1` keeps its default `0.67em`, which at desktop size is **68.5px top and bottom**, and in a flex column margins don't collapse. Measured at 1440×900: eyebrow→headline **91px** and headline→sub **97px** instead of 22 and 28. It also pushes panel content past its padding into the fixed header and footer — overflowing by **16px at 1280×720** (an ordinary laptop), **27px in iPhone landscape** and **49px at 740×360**, where the eyebrow ends up underneath the header. Adding `h1, p { margin: 0 }` restores exactly 22/28px and makes every panel fit at every size tested.
- **The footer and the preloader text can never meet contrast.** `--fg-faint` is ink at 42%, which on the flat paper `#f2f0ec` — the best background it will ever sit on — composites to just **2.79:1**. The 12px footer measured **100% of pixels below 4.5:1 at every scroll position**; the 12.5px `LOADING` line sits on flat paper, so it is exactly 2.79:1 every time it is shown. Reaching 4.5:1 on this paper needs an ink alpha of at least **0.572**.
- **The subhead dips wherever the footage goes dark behind it.** `--fg-soft` (64% ink) passes on paper at 5.68:1, but the veil is deliberately thinnest in the middle — exactly where the panels' text sits — and the fractured ring's cracks and flying fragments pass behind it. Across 18 positions spanning all three panels, **5.74%** of subhead pixels fell below 4.5:1, at 11 of 18 positions (worst 2.67:1); the eyebrow 1.35% (worst 3.22:1). The headline is effectively clean, and the nav links measure 12.47:1. A slightly heavier centre stop in the radial layer would hold the copy.
- **The preload strategy fails worst on exactly the connections it is meant for.** The blob exists to avoid range-request scrubbing on slow networks — but a slow network is where the 15-second bail fires. Downloading the 10,591,831-byte file from this machine took **32.0 seconds**; at that rate the bail would have fired at about **4.96 MB (47%)**, **thrown that away**, and restarted as a direct range-request stream — the slideshow mode the whole design exists to prevent — after 15 seconds behind a preloader. If that stream then stalls, the 12-second fallback adds more: up to **27 seconds** before the page appears. Hand the partial download to the video instead of discarding it (MediaSource), or show the page on the stream immediately and upgrade to the blob when it lands.
- **The easing is frame-rate dependent.** `seekAt += gap * 0.115` runs once per animation frame, so the same scroll settles at a different speed on every display. Measured at 60Hz, a top-to-bottom jump took **734ms** to land within one frame of the target, matching the model (the gap shrinks to 0.885× per frame, about 45 frames). A 120Hz screen gets there in half the time and a 30Hz low-power mode in double. Scale the factor by elapsed time — `1 - Math.pow(1 - 0.115, dt / (1000/60))` — if the feel is meant to be the same everywhere.
- **Invisible links take keyboard focus.** `pointer-events:none` stops clicks, not focus, and all three panels stay in the DOM. At the top of the page, Tab lands on **Tour our space** and panel 3's **Start a brief** while both of their panels are at opacity 0 — verified by focusing each link. Set `inert` on panels below the same 0.6 threshold the code already uses for pointer events. There are also **three `<h1>` elements** on the page; make panels 2 and 3 `<h2>`.
- **Every link on the page goes nowhere — and as written, can't.** `#board`, `#visit` and `#order` match no element (verified), so all six links — the three in the nav and the three CTAs — do nothing. More fundamentally, the page has no elements to anchor to: its "sections" are scroll percentages of an empty `.track`. In-page navigation here has to be a `scrollTo()` to a cue's progress value, not a fragment.
- **The star in the mark is never set in the brand font.** `✵` is U+2735, and no Inter Tight subset covers it — checked against every `unicode-range` Google serves. It always falls back to a system symbol font, so the logo mark changes face per platform and can render as a missing-glyph box where none has it. Draw it as an inline SVG.
- **Every declared pill size renders larger than stated.** The spec never sets `box-sizing:border-box`, so the padding and the 1px border are added outside the declared box: the 40/38/36px nav pill measures **42/40/38px**, the 48/44px CTA **50/46px**, and the ≤420px CTA's `max-width:280px` comes out **322px** wide at 390 and 360px. Still above the 36px touch-target floor, but none of the stated sizes is what ships.
- **On mobile the footer is pinned to the left edge.** Below 720px it gets `max-width:34ch`, but it is a `position:fixed` box with `left:0; right:0` and no auto margins — so the max-width simply trims it from the right. Measured, it spans x 0→248 and its centre sits **71px left of the viewport's centre at 390px** (56px at 360, 36px at 320), with its fade gradient truncated along with it — despite `justify-content:center` and `text-align:center`. Add `margin-inline:auto`. (Relatedly, the eyebrow's `·` separator is its own flex item, so when the eyebrow wraps — it does at every width under 420px — the dot is left hanging at the end of the first line.)
- **The loop never rests.** `frame()` calls `paint()` every animation frame for the life of the page, writing opacity, transform and pointer-events on three panels plus the meter — **ten style writes per frame, ~600 per second**, even with nobody scrolling. Skip `paint()` when `progress` hasn't changed since the last frame.
- **The copy is a bakery's, with the nouns swapped.** "Built at four. Out by seven." "Six kinds of mesh." "Whipped every morning, spread to the edge and weighed by the quarter pound." "We export to order." "Tue–Sun, 9am till sold out." A 3D studio doesn't sell out, whip, spread or weigh anything by the quarter pound; the fiction slips on every line. Rewrite it for the studio it claims to be.
- **What was specified well.** The font request asks for exactly the two weights the page uses and nothing more — rarer than it should be. The self-hosted subset of those two is **8,232 bytes** against Google's 44,916 (18.3%). All-intra is also the right call, and worth pricing honestly: at the same quality it costs **1.74×** a normal 48-frame GOP at 1080p and **2.58×** at 720p. Resolution is the lever that keeps the technique — the same clip all-intra at 1280×720 is **3,212,449 bytes, 30% of the source**, with every frame still a keyframe.
Có thể bạn cũng thích
Free
Featured
Vectrus Energy
🛬 Landing Page
Scroll-tied cinematic section: a 500vh track scrubs an aerial clip frame by frame through a WebCodecs frame bank, with three sequential copy blocks.
4.9k 535CursorClaude CodeLovable
Free
Wanderful
✈️ Travel
Cinematic travel hero: a full-bleed loop drifting under a GSAP mouse parallax, with a liquid-glass pill navbar and two fade-up copy blocks.
4.8k 445CursorClaude CodeLovable
Free
Featured
Aurora Weather Dashboard
📊 Dashboard
Liquid-glass weather dashboard on a 1357x871 unit grid: frosted panels over a storm photo and a chart that draws itself, then fills.
4.5k 527CursorClaude CodeLovable
Free
Featured
Fastshot Landing
🤖 AI
Single-screen AI app-builder landing: full-bleed dawn video, glass composer card and a pixel-specified absolute toolbar.
4.0k 639CursorClaude CodeLovable