枠の中でスクロール・クリックして動きを試せます。端末指定は実寸表示です。幅が足りない場合は横にスクロールできます。
再現プロンプト
架空のヘッドホン「AXIS ONE」をAI生成の実写風プロダクト動画で語るページ。空中分解・ドライバーマクロ・側面スイングの3本の動画を各121枚の連番WebPに分解し、Apple式にスクロールでcanvasへスクラブ描画する(<video>のシークと違い遅延ゼロ)。ヒーローはターンテーブル回転の往復ループ動画、分解ビートは映像内のパーツ位置に注釈が追従し、右端のフィルムゲージが実フレーム番号を読み続ける。 配色・構成・動きの数値をまとめた指示書です。 お使いのAIに貼り、素材やお店の情報を伝えて実装してください。仕上がりは利用するAIと素材によって変わります。
Build a single self-contained HTML file (inline CSS + JS, GSAP + ScrollTrigger via CDN) for a one-page product site for a fictional reference headphone brand "AXIS ONE" ("AXIS AUDIO — Reference Series"). The page reads as scrubbed film — because it literally is: real AI-generated product footage is pre-split into WebP frame sequences and drawn onto <canvas> by scroll position (the Apple AirPods technique). Never scrub a <video> element's currentTime with scroll — seeking is asynchronous and decodes from the nearest keyframe, so it lags and stutters; pre-decoded stills have zero seek latency. The page scrolls with the normal window scrollbar (no inner container needed in the exported file).
## Footage (AI video generation → frame extraction)
Start from four text-free photorealistic stills of one consistent fictional headphone (matte black shell, brushed-silver accents, pure black studio background, 16:9): (1) assembled three-quarter hero, (2) vertically exploded parts view, (3) extreme driver macro, (4) dead-on side profile. Generate them with any image model, then animate them with an image-to-video model (Seedance 1.0 Pro via fal.ai queue API, 1080p / 5s / 24fps; Kling or Vidu work the same way):
- **Disassembly clip** — use FIRST + LAST frame guidance: first frame = the assembled hero still, last frame = the exploded still. Prompt: "Studio product film of premium black over-ear headphones floating against a pure black background. The headphones slowly and smoothly disassemble in mid-air: ear cups, cushions, driver units and inner rings separate vertically into a perfectly aligned exploded view, each part drifting apart at slow constant speed while the camera shifts gently from a three-quarter angle to a straight frontal view. Soft studio rim lighting. Photorealistic 4K product commercial." First/last-frame guidance is what guarantees the exploded end state actually matches your still — prompt-only disassembly drifts.
- **Turntable clip (hero)** — first frame = hero still, last frame = side-profile still, prompt a slow constant-speed turntable rotation. Then make it a seamless ambient loop by concatenating it with its own reverse (ffmpeg: split → reverse (trim 1 frame) → concat → H.264 CRF 24 + faststart). This clip is NOT scrubbed — it plays as a plain autoplaying muted <video> behind the hero, the one job <video> does smoothly.
- **Driver macro clip** — image-to-video from the macro still: "Camera pushes in very slowly at constant speed while a soft specular light sweep travels across the metal mesh… no rotation, product perfectly still."
- **Side profile clip** — first frame AND last frame = the same side-profile still (forces the shot to return to a perfect profile and makes it loop-safe); prompt an almost-imperceptible weightless drift with a rim-light sweep. Expect the model to sway out to three-quarter mid-clip and settle back — that reads as "every angle" and lands on "profile, unmistakable".
- Every prompt ends with: "Strictly no text, no watermark, no logo, no hands, no people, no background change, no color shift, no flickering, no camera shake, no sudden movement, no extra objects, product never deforms." Motion must be SLOW and CONSTANT-SPEED — eased or bouncy motion scrubs badly.
- **Frame extraction**: split the disassembly / driver / side clips (not the hero loop) into numbered WebP stills, two sizes — frames/{beat}/lg/NNN.webp at 1280px and frames/{beat}/sm/NNN.webp at 640px (ffmpeg → PNG → cwebp -q 72; 5s @ 24fps = 121 frames per beat; dark footage compresses to roughly 1–6MB per beat at lg). Pick lg/sm once at load from viewport width ≤768px.
## Art direction
- Background #0A0D12 (near-black navy), text #F5F5F5, muted #ABABAB, single accent #7DD3FC (sky blue) for the gauge, callout dots/borders, active caption glow and CTA border.
- Typography: system sans-serif, uppercase eyebrows 0.24–0.3em letter-spacing, headings 700 weight clamp(1.8rem,5vw,3.2rem), body 0.9–1.05rem #ABABAB. Numeric readouts (frame counter, specs) use a monospace stack (ui-monospace, SF Mono, Menlo), tabular-nums.
- A thin film-grain overlay (repeating-linear-gradient scanlines, 3px pitch, opacity 0.035, flickering 0.4s steps(2) infinite) sits above the page for constant "projector" idle motion, disabled under reduced motion.
## Section 1: Hero / Intro
Non-pinned, 100vh, centered. The palindrome turntable loop fills the section as an object-fit:cover <video autoplay muted loop playsinline> at 55% opacity (poster = the hero still; also set el.muted = true from JS before play() — some browsers ignore the attribute and block autoplay) beneath a radial-gradient overlay (rgba(22,34,44,.55) at 30% top fading to the base colour). Contents above it: eyebrow "AXIS AUDIO — REFERENCE SERIES", H1 "Sound, engineered / frame by frame." (two lines), body copy ("Axis One is told in three studio films, scrubbed live on your own scroll bar — a headphone disassembled, examined up close and turned in profile, one frame at a time."), and a bouncing "↓ start the reel" hint. A soft 340px radial-gradient halo (accent 28% opacity, blurred 4px) floats behind the heading with a 10s ease-in-out alternate translate(±10px,∓6px) scale(1↔1.06) loop. Entrance is a "projector-strobe" reveal — not a translate/fade-up: each '[data-flicker]' element starts opacity 0, filter 'brightness(2.4) contrast(1.6)', animating to opacity 1 / 'brightness(1) contrast(1)' over 0.85s, ease 'steps(6)', 0.14s stagger, on scroll past 90% into view (toggleActions play/reverse).
## Section 2 (Hero signature): The Reel — three canvas-scrubbed frame sequences
A single full-viewport section pins (pin:true, scrub:true, anticipatePin:1, invalidateOnRefresh:true) for REEL_BEAT_COUNT × REEL_BEAT_SCRUB = 3 × 2.4 = 7.2 viewport-heights ('end: () => "+=" + window.innerHeight × 7.2'), stacking three beats as absolutely-positioned layers — disassembly, driver macro, side profile — each layer holding one <canvas> inside its own "parallax" wrapper div, with the beat's matching still as a CSS background poster behind the canvas (shows through until frames stream in; the disassembly beat's poster is the ASSEMBLED hero still, because that's what its frame 1 looks like).
**Smoothing (anti-jitter — implement exactly)**: ScrollTrigger's onUpdate only writes 'progress.target = self.progress' into a ref, never a style. One rAF loop (also driving pointer parallax) runs continuously: 'progress.current += (target−current)×SCRUB_LERP' (SCRUB_LERP=0.12); every visual derives from smoothed "current", never the raw target — decoupling DOM writes from irregular scroll/onUpdate timing. The gauge tape uses the same lerp. Only opacity/transform/canvas-draws mutate per frame.
- **Frame streaming**: load all three sequences up-front as plain HTMLImageElement objects (the browser keeps them compressed and manages decode) — beats load in order (disassembly first), each with a small parallel window (~10 requests). When drawing, if the exact frame hasn't arrived yet, draw the nearest already-loaded EARLIER frame (frames stream in order, so this self-heals).
- **Canvas scrub**: per beat i with smoothed global progress g, a trapezoid crossfade window (span [i/3,(i+1)/3], smoothstep ramps across a CROSSFADE_OVERLAP=0.07 zone at each shared boundary; first/last beats stay open-ended) sets the layer's opacity, and the beat's own clamped local progress li picks frame index round(li×(N−1)) (N=121). Redraw ONLY when the index actually changes (or after resize/late-arriving frames). All sequences share 16:9, so one resize pass sizes every canvas backing store (devicePixelRatio capped at 2) and computes a single cover-rect (dw = max(cw, ch×16/9), dh = dw÷(16/9), centered offsets); drawImage(img, dx, dy, dw, dh) after clearRect. Skip drawing layers at opacity ≤0.001 (except their first paint).
- **Part-annotation callouts (disassembly beat only)**: five dot+label pills positioned in FOOTAGE-frame percent — "Aramid-wrapped headband" (50,9,right,0.34), "Machined aluminum shell" (27,46,left,0.5), "Memory-foam ear cushion" (60,46,right,0.62), "40mm driver assembly" (63,73,right,0.76), "Gold-plated terminals" (37,90,left,0.88) — matching where the parts settle in the exploded end state. The label layer div is fitted to the drawn cover-rect (left/top/width/height in px) on every resize, so the dots stay pinned to the parts at any viewport aspect. Label opacity = smoothstep((li0−threshold)/0.14) × the beat's own crossfade opacity — labels reveal one at a time as the parts separate on film, fully reversible.
- **Pointer parallax**: on pointermove over the reel, relX/relY = pointer position −0.5 becomes a shared target; the rAF loop lerps it (POINTER_PARALLAX_LERP=0.12) and applies 'translate(x×10px, y×10px) rotate(x×1.4deg)' to each beat's parallax wrapper — the canvases drift gently under the cursor between scroll ticks. pointerleave resets to (0,0); the loop pauses via IntersectionObserver when the demo is off-screen.
- **Chrome sync, with hysteresis**: raw beat = floor(g×3), only *committed* (rewriting eyebrow, gauge chapter, caption texts) once g moves BEAT_HYSTERESIS=0.025 past the boundary in the travel direction, so boundary dithering can't flicker text. The gauge frame readout is REAL: localFrame = round(li_committed×120), globalFrame = committedBeat×121 + localFrame, shown as "037 / 363" (monospace, text-only, never a counted tween). Active caption = min(3, floor(localFrame/(121/4))), revealed via the shutter clip-path technique (opacity 0→1 + 'clip-path: inset(50% 0 50% 0)'→'inset(0 0 0 0)', 0.3s/0.4s cubic-bezier(.65,0,.35,1)).
- Beats/eyebrows: 00 · DISASSEMBLY ("Five parts, one seamless build." → "CNC-milled aluminum yoke, tolerance ±0.02mm." → "Driver housings, seated by hand." → "One object. Zero visible seams."), 01 · DRIVER DETAIL ("40mm beryllium-coated diaphragm." → "Tuned across 20Hz–20kHz." → "Neodymium magnet, matched to ±0.5dB." → "Sound you can see, up close."), 02 · SILHOUETTE ("One continuous silhouette, every angle." → "Matte titanium, brushed in one direction." → "No seam survives close inspection." → "Axis One — profile, unmistakable.").
## Section 3: Specification Reel (non-pinned interlude)
Centered, background #0d1218. Eyebrow "SPECIFICATION REEL", H2 "Every gram, measured on camera.", a 3-column grid (1px accent-tinted gridlines) of 6 cells: DRIVER "40mm Beryllium-coated", WEIGHT "268g", BATTERY "38h · ANC on", CHARGE "USB-C · 10min → 4h", FREQUENCY "20Hz – 20kHz", CONNECT "Bluetooth 5.3 LE Audio" — static text, no numbered rows, no count-up. Entrance "flash-in": '[data-flash]' starts opacity 0, 'scaleX(0.92)' (origin 0% 50%), 'brightness(2.2)', to opacity 1/scaleX(1)/brightness(1) over 0.5s, ease 'steps(4)', 0.08s stagger, at 78% viewport entry, reversing on leave-back.
## Persistent chrome: the Film Gauge
A fixed widget on the right edge (vertical-centered, hidden below 640px width): a vertical chapter label (writing-mode vertical-rl, rewritten on committed-beat change), a 24×220px window with a sprocket-hole "tape" (repeating-linear-gradient perforations, 16px pitch), backgroundPositionY = -(current×900)px where "current" is a page-wide ScrollTrigger progress (content, "top top"→"bottom bottom", scrub:true) run through the same SCRUB_LERP smoothing, plus a glowing accent pointer line (2px, box-shadow glow, 2.2s ease-in-out alternate opacity 0.6↔1). Below it, the live monospace frame counter "037 / 363".
## Section 4: Footer CTA
Centered, radial-gradient background (#16222c to base). H2 "The reel ends. The sound doesn't.", Q1-shipping body copy, a pill CTA "Reserve Axis One →" (1px accent border, transparent fill, hover fills solid accent + dark text, translateY(-2px), brightness(1.15), 0.3s ease), a mono log line "REEL 363 / 363 · COMPLETE". Same flash-in entrance as the spec section.
## Scroll animations (shared rules)
- The reel's ScrollTrigger (pin:true, scrub:true, anticipatePin:1, invalidateOnRefresh:true, 'end: () => "+=" + (window.innerHeight × 7.2)') only ever reports a raw progress target each tick — all perceptible smoothing comes from the shared rAF lerp (SCRUB_LERP = 0.12), never from a numeric scrub-tween value.
- Non-pinned reveals (intro flicker, spec/footer flash-in) use toggleActions "play none none reverse" at start "top 90%"/"top 78%" — one-shot per direction, not scrub. Apart from the quantized (but 121-step fine) frame advance, every derived value (crossfade opacity, label opacity, parallax) is a continuous function of scroll progress, staying smooth and instantly reversible on scroll-up.
## Performance & accessibility
- Frames are static local assets loaded once; per scrub tick the only work is one drawImage per visible canvas (and none when the index hasn't changed), keeping the reel GPU-cheap. A ResizeObserver calls ScrollTrigger.refresh() on resize; every GSAP context/ScrollTrigger is torn down on unmount.
- Respect 'prefers-reduced-motion: reduce': un-pin the reel and render the three beats as normal-flow stacked sections (min-height 92vh) showing each beat's still (disassembly shows the exploded still, all callouts visible immediately); captions as a static list; the hero renders the static hero image instead of the video loop; disable grain, halo and gauge-pointer animations, freeze the gauge showing "STATIC"; flicker/flash entrances become plain 0.35–0.4s opacity fades ('power1.out').
- Decorative <canvas>/<video>/<img> elements carry 'aria-hidden' (and empty alt where applicable); captions, eyebrows and specs stay real, readable text, never conveyed only through imagery.
## Current video loading contract
This supersedes earlier blanket autoplay/preload instructions. Keep a real still frame from each background clip as its loading, failure and reduced-motion poster. Do not attach the MP4 URL or call play until the actual picture/scene is visible and needed. Use muted, loop, playsInline and initial preload=none; pause offscreen and in hidden tabs, and remove the source on teardown. A late play promise must not restart a stopped clip or pause a newer owner of the element. After rejection, keep the poster and retry on re-entry instead of retrying each animation frame.
## Action flow and content
Implement the following local planning experience for the primary CTA, in the visual language of the page. Use a real button opening a labelled modal dialog. Choosing a pricing card must preselect that exact plan. A navigation link may move to another section; a planning CTA must lead to its review flow.
Use this content for the dialog, including the option details and notes:
{
"slug": "scrub-frames",
"brand": "AXIS ONE",
"accent": "#815d3c",
"title": "A closer listen",
"intro": "Review the listening hardware and choose a finish before keeping your product shortlist.",
"facts": [
[
"Driver",
"40 mm beryllium-coated"
],
[
"Battery & weight",
"38 hours with ANC · 268 g"
],
[
"Charging",
"USB-C · 10 minutes for 4 hours of listening"
],
[
"Connection",
"Bluetooth 5.3 LE Audio · 20 Hz–20 kHz"
]
],
"groups": [
{
"label": "Finish",
"options": [
{
"title": "Graphite · $349",
"detail": "Dark shells with a soft matte surface. Includes hard case, USB-C cable and audio cable."
},
{
"title": "Chalk · $349",
"detail": "A pale neutral finish with the same driver, battery and accessories."
}
]
}
],
"notes": [
"These are fictional product specifications and prices. The sample does not accept deposits or offer a shipment date; tax, delivery and return terms would be confirmed before a real order."
]
}
- Explain before input that this is a fictional brand sample: choices can be reviewed and saved, with no external submission, booking, payment or registration. Use each brand's language. Do not launch a mail client or present an installer for a fictional product.
- Give every option group a legend, inputs explicit labels and errors linked descriptions. Validate options against the allowed values, requested dates as actual dates from today onward, and free text as 10–2,000 trimmed characters. Blank spaces alone must fail. Tracking accepts the sample reference TG-2048 only.
- Focus the first invalid field and preserve every entered value. On success, show the selected option WITH its full detail, requested date or brief, relevant facts and notes. An edit action returns to the preserved form. Do not claim a real reservation, purchase or account has succeeded.
- Save the complete review as UTF-8 text. A browser download request is not proof of a file being written. On download failure, retain the full text in a labelled, selectable textarea and offer retry. Do not show a false success state.
- Constrain modal content to the viewport with internal scrolling. Support keyboard and touch, trap focus while open, close on Escape and restore focus to the exact initiating control. Keep these operations available with motion disabled.
- If a separate project-contact CTA is present, let it collect a project brief and review it locally; a product selection CTA must retain the product choice. Verify both flows independently.