Hum One — scrollytelling 3D product page prompt
Graphic DesignerA pinned scrollytelling product page: one 3D speaker built in code stays on screen while four chapters scroll past, exploding it into parts, charging its battery ring and recolouring it live. Copy the prompt below into Lovable, Bolt, v0 or Cursor.
HUM ONE — PINNED SCROLLYTELLING PRODUCT PAGE (single index.html, three.js, no images)
ROLE
You are a senior creative developer. Build ONE self-contained index.html: an Apple-style, editorial product page for a fictional portable speaker called "Hum One". The speaker is modelled in code with three.js (no image or model files). The product stays pinned in the middle of the screen while four short "chapters" scroll past it; each chapter moves, rotates, explodes or recolours the same 3D object. Very few words, lots of air.
TECH
- Plain HTML/CSS + one <script type="module">. Import map: "three" -> https://unpkg.com/three@0.160.0/build/three.module.js, "three/addons/" -> https://unpkg.com/three@0.160.0/examples/jsm/
- Font: Google Fonts "Inter Tight" weights 200-800. It is the ONLY typeface on the page.
- Renderer: antialias, alpha:true (transparent canvas), pixelRatio min(dpr,2), ACESFilmic tone mapping, exposure 1.05, sRGB output. Environment: PMREM of RoomEnvironment (sigma 0.04). Key light white 1.6 at (-3,5,6); rim light #ffc2a8 1.4 at (5,1,-4).
- Camera: PerspectiveCamera FOV 28, at z=12, looking down -z. worldH = 2*12*tan(14deg). pixels-per-unit ppw = innerHeight/worldH.
LAYERS (z order, back to front)
1. #giant: fixed, centred word "hum." — weight 800, font-size min(36vw,58vh), letter-spacing -.07em, line-height .8, colour rgba(0,0,0,.07). Scales 1+s*.5 and fades to 0 by s=.55 (s = chapter progress, see SCROLL).
2. Fixed full-viewport <canvas id="gl"> (pointer-events none).
3. Four fixed full-screen chapter overlays <section class="ch" id="c0..c3">, each with an inner .in block (pointer-events auto on .in only).
4. Fixed header (72px, padding 0 44px): left wordmark "hum" (800, 24px, -.05em); centre counter "01 / 04" (13px, 500, opacity .6, tabular numbers); right a pill outline link "Buy" (1.5px currentColor border, radius 999, padding 8px 18px) that jumps to #c3.
5. Desktop only: fixed right rail at right:40px, vertically centred: four links "Intro / Sound / Battery / Color", 12px/500, each followed by a 2px bar 18px wide; the active one is opacity 1 with a 34px bar, others opacity .35.
6. Three 3D callouts (desktop only), fixed, each = 9px orange dot (#ff8a5b with 5px halo rgba(255,138,91,.18)) + 1px line rgba(244,242,238,.5) + 190px label (15px/600 title, 13px/400 subtitle at .6 opacity). Labels: "Glow ring — breathes with the beat" (right side), "Twin 40 mm drivers — 360° sound" (left side, right-aligned, row-reversed), "Passive radiator — deep, round bass" (right side).
SCROLL
- <main> contains one spacer div of height calc(100vh + 3*125vh) and four 1px absolute anchors #m0..#m3 at top 0 / 125vh / 250vh / 375vh.
- s = scrollY / (1.25*innerHeight). i0 = min(2, floor(s)); f = smoothstep((s - i0 - .25)/.5). Every per-chapter value lerps from chapter i0 to i0+1 with f, so the object HOLDS still during the first and last quarter of each chapter and glides in the middle.
- Chapter text opacity: v = 1 - smoothstep((|s-j| - .16)/.3); visibility hidden when v<.01; the .in block is translated by (j-s)*50px vertically (text drifts up as you scroll). prefers-reduced-motion: v is 0/1 only and the speaker ignores the cursor.
- Page background (set on body every frame) lerps between chapter colours: #ece9e3 -> #0c0c0e -> #17100c -> (#f1efea mixed 16% with the current speaker colour). If background luminance (.3r+.59g+.11b) < .45 add body.dark (text #f4f2ee), else text #111. Shadow plane hidden on dark chapters.
CHAPTER CONTENT
c0 (bottom centre, 44px from bottom): h1 two lines "Small enough to carry. / Loud enough to fill the room." (clamp(22px,2.3vw,30px), 500, -.03em, lh 1.25). Under it a "SCROLL" cue (12px, .14em tracking, uppercase, .5 opacity) with a 1.5x34px line that scales Y 0.25->1 on a 1.75s sine.
c1 (top centre at 104px): eyebrow "01 · SOUND" (13px/600, .12em, uppercase, .55), h2 "Sound in every direction." (clamp(34px,4.6vw,64px), 700, -.045em, lh 1), lead "Two drivers fire outward through 1,200 tiny holes, so every seat in the room is the good seat." (18px, lh 1.5, .7 opacity, max 430px, centred).
c2 (left 7vw, vertically centred, max 440px): eyebrow "02 · BATTERY"; giant counter <span id=hrs> + small "hours" — counter is weight 200, clamp(120px,17vw,240px), -.07em, colour #ff8a5b, tabular; "hours" is .22em, 500, light. The number = round(24*smoothstep((s-1.5)/.4)), two digits ("00"..."24"). Lead "The ring fills with light as it charges, then dims to a whisper when it needs you." Three facts row (gap 28px, 14px, .75): "2 h / to full", "USB-C / one cable", "IP67 / rain-proof" (value 22px/600).
c3 (left 56vw, vertically centred, width min(440px,38vw)): eyebrow "03 · COLOR", h2 "Make it yours.", five round 46px swatch buttons (gap 14, inner shading insets, outline 2px #111 offset 4px + scale 1.06 on the active one): Peach #ff8a5b "warm and loud", Ink #25242a "quiet luxury", Mist #d9d5cc "soft and calm", Sage #8fa98a "fresh air", Cobalt #3b5bdb "electric". Name line "Peach — warm and loud" (15px/600 + 400 .6 description). Row: "$129" (40px/600) + black pill CTA "Buy Hum One →" (16px/600, padding 16px 28px, soft drop shadow, lifts 2px on hover). Fine print "Free shipping · 30-night trial · 2-year warranty" (13px, .55). Bottom-left of the overlay: "© 2026 Hum Audio" (12.5px, .5).
Swatch logic: hovering a swatch previews its colour (only while s>2.6); clicking sets it; leaving returns to the clicked one. The speaker body colour lerps to the target with 1-exp(-dt*7). The ch3 background tint follows it.
THE SPEAKER (all inside group spk, which sits inside group tilt)
- Body: LatheGeometry (96 segments) of a capsule profile: height H=1.8, radius .95, corner radius .42 (two 12-step quarter arcs). MeshPhysicalMaterial colour #ff8a5b, roughness .32, clearcoat 1, clearcoatRoughness .12, sheen .3.
- Grille: open cylinder r .954, height .8, at y -.16; MeshStandard #1a1416 roughness .6, transparent, depthWrite false, alphaMap = 1024x128 canvas of white dots (6 staggered rows, 96 per row, radius 3.6px, row pitch 21px) on black.
- Top assembly group topG: dark disc (cyl r .47, h .03, #111014 clearcoat .6) at y H/2-.005; glow ring torus (r .5, tube .026) lying flat at H/2+.01 with MeshStandard #ffe1d2 emissive #ff8a5b; two buttons (cyl r .085 h .05, #2a272d) at x ±.16 carrying "–" and "+" bars (ring material).
- Driver group drv (normally scale .001): basket cyl r .72 h .08 (#1b1a1f), cone = open cylinder r .62->.22 h .16 (#2d2b31, double side), orange dust cap half-sphere r .22 (emissive .25), surround torus r .64 tube .04.
- Strap: half-torus (r .3, tube .065, arc 1.25π, #26222a) at (.93,.5,0), rotation (0, π/2, -.62π).
- Shadow: a 3x1.1 plane with a radial black gradient (alpha .5 -> 0) placed under the speaker at z -1.2*scale.
POSES per chapter [x, y (fractions of viewport), size (fraction of innerHeight), rotY, rotX]
Desktop (>900px): c0 [.5,.44,.54,-.55,.22] c1 [.5,.69,.46,.6,.12] c2 [.68,.52,.6,2.2,.78] c3 [.33,.53,.66,5.73,.25]
Mobile/tablet (<=900px): c0 [.5,.45,.38,-.55,.22] c1 [.5,.56,.34,.6,.12] c2 [.5,.52,.36,2.2,.78] c3 [.5,.43,.34,5.73,.25]
size px = min(size*innerHeight, innerWidth*(mobile?1.15:.9)); tilt.scale = sizePx/(3.1*ppw); tilt position = screen point converted with ppw (y flipped) plus a bob of sin(t·2π/7)*.06*scale.
Rotation: spk.rotation.y = rotY + cx*.6 + sin(t·2π/14)*.05; tilt.rotation.x = rotX + cy*.3; tilt.rotation.z = -cx*.08.
Look target (cx,cy smoothed with 1-exp(-dt*6)): cursor offset from the speaker centre / half viewport, clamped ±1, for 4s after the last pointer move; else deviceorientation (gamma/30, (beta-45)/30); else idle drift sin(t·2π/7)*.5, sin(t·2π/3.5)*.25.
CHAPTER EFFECTS
- Exploded view: ex = 1 - smoothstep((|s-1| - .1)/.4). topG.y = ex*.95; drv at y H/2 + ex*.32 with scale ex. Callout anchors (spk-local points projected to screen every frame): ring (0, H/2+.03+topG.y, -.5), driver (0, drv.y+.1, .72), radiator (0, -.2, .96). Dot sits exactly on the point; callout opacity = max(0, ex*1.4-.4); line width 30+80*opacity px.
- Glow: ring emissiveIntensity = (1.2 + .6*breathe)*(1-chg) + chg*(.4 + 2.4*fill), breathe = .5+.5 sin(t·2π/1.75), chg = 1 - smoothstep((|s-2|-.1)/.4), fill = the battery counter fraction. So the ring charges up with the counter.
RESPONSIVE (<=900px)
Header 60px / padding 0 20px; rail and 3D callouts hidden. c1: top 84px, lead hidden, instead a 3-row list pinned to the bottom (orange 7px dot + bold name + small description). c2 and c3: .in becomes a full-height flex column from top 84px to bottom 30px, left/right 24px: c2 = eyebrow + counter (112px) at the top, lead (15.5px) + facts pushed to the bottom; c3 = h2 at top (eyebrow hidden), swatches (40px, gap 12) + name + price/CTA row (space-between, price 32px) + fine print at the bottom; footer hidden. Tablet 600-900px: blocks start at 110px, h2 52px, counter 170px. Nothing may be cropped at 1280x720, 390x844 or 820x1180.
LOOP / HOOKS
Expose window.__frame(t,dt) that does everything above and renders; window.__mouse = {x,y} (updated by pointermove); window.onScroll = noop. Run a requestAnimationFrame loop (dt capped at .05) unless window.__RECORD__ is set. preserveDrawingBuffer true. Resize updates renderer + camera aspect.
COPY TONE: calm, confident, short. No other sections, no nav links beyond the rail, exactly two calls to action (header "Buy" and "Buy Hum One →").
© 2026 Graphic Designer · @Graphics_designerr. All rights reserved.