类型:Site模板 · 收录:2026-10-04 12:33:24 · 源码:会员可见
Build a single standalone index.html file (inline <style> and <script>, no frameworks, no build step) for a full-screen hero section called "IMAGINE". Replicate every detail below exactly.
═══════════════════════════════════════
1. CONCEPT
═══════════════════════════════════════
A 100vh hero. The visible surface is an off-white "paper" layer with the word IMAGINE in huge black letters. Beneath the paper sits a looping video of the word IMAGINE made of inflated balloons (it morphs through chrome/violet foil → gold foil → red latex → holographic opal → neon lime jelly → pastel pink/blue satin → back to chrome) on a pure black background. When the user moves the mouse (or drags a finger on mobile), a gooey, liquid ink-like trail cuts holes in the paper and shows the balloon video underneath. The trail has wobbly liquid edges, flings small ink droplets that drip down with gravity, smears the black letters along the stroke direction at its rim, and melts back closed in about 1.5 s. The black headline is sized and positioned to line up exactly with the balloon word in the video, so the reveal looks like the letters turning into balloons.
═══════════════════════════════════════
2. VIDEO SOURCE (use this exact URL)
═══════════════════════════════════════
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260928_183857_1e589ec6-07af-4ef6-8995-3b2e5844b5a8.mp4
1920×1080, 16:9, 24 fps, 10 s, H.264, no audio, loops seamlessly.
<video class="hero__video" id="heroVideo" src="[URL above]" autoplay muted loop playsinline preload="auto" aria-hidden="true"></video>
Do NOT add a crossorigin attribute and do NOT sample the video in WebGL/canvas. The video is a plain DOM element under the canvas, so no CORS is needed.
═══════════════════════════════════════
3. FONTS (Google Fonts)
═══════════════════════════════════════
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500&family=Outfit:wght@800&display=swap" rel="stylesheet" />
- Display (headline): "Outfit" 800, letter-spacing -0.02em, line-height 1
- Body/UI: "Inter Tight" 400 and 500
═══════════════════════════════════════
4. HEAD
═══════════════════════════════════════
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Imagine — Creative Studio</title>
<meta name="description" content="A creative studio for ideas with lift. Move across the page to see what's underneath." />
═══════════════════════════════════════
5. DESIGN TOKENS (:root)
═══════════════════════════════════════
--paper: #f3f3ee; (off-white top layer)
--ink: #0b0b0b; (headline black)
--void: #000000; (page + video background)
--gutter: clamp(16px, 2.6vw, 40px);
--display: "Outfit", "Arial Black", system-ui, sans-serif;
--body: "Inter Tight", "Helvetica Neue", Arial, sans-serif;
Global: * · html, body { background:#000 } · body { font-family:var(--body); color:var(--ink); -webkit-font-smoothing:antialiased; overflow-x:hidden }
═══════════════════════════════════════
6. HTML STRUCTURE (exact order = stacking order)
═══════════════════════════════════════
<section class="hero" id="hero" aria-label="Imagine">
<video class="hero__video" id="heroVideo" ...></video> ← z 0
<div class="hero__paper"><h1 class="hero__title" id="heroTitle">IMAGINE</h1></div> ← z 1 (no-JS fallback + accessible h1)
<canvas class="hero__reveal" id="heroReveal" aria-hidden="true"></canvas> ← z 2 (WebGL paper + headline with holes)
<div class="chrome"> ← z 3 (UI)
<div class="intro">
<p class="intro__lede">Not a template, a feeling.<br />Because every idea starts as air.</p>
<a class="cta" href="#contact">Book a call <svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2 8h11m0 0L8.5 3.5M13 8l-4.5 4.5" stroke="currentColor" stroke-width="1.5"/></svg></a>
</div>
<div class="foot">
<span>Creative studio in Paris</span>
<nav class="links" aria-label="Social"><a href="#">LinkedIn</a><span aria-hidden="true">/</span><a href="#">Instagram</a></nav>
</div>
<p class="hint" id="hint" aria-hidden="true"><span class="mouse">Move to inflate</span><span class="touch">Touch to inflate</span></p>
</div>
</section>
═══════════════════════════════════════
7. CSS (exact values)
═══════════════════════════════════════
.hero { position:relative; height:100vh; height:100svh; min-height:480px; overflow:hidden; background:#000; isolation:isolate; touch-action:pan-y; -webkit-tap-highlight-color:transparent }
.hero__video { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0; pointer-events:none; background:#000 }
.hero__paper { position:absolute; inset:0; z-index:1; background:var(--paper); display:grid; place-items:center }
.hero__title { font-family:var(--display); font-weight:800; font-size:19vw; line-height:1; letter-spacing:-0.02em; color:var(--ink); white-space:nowrap; user-select:none }
.hero.gl-on .hero__paper { background:transparent }
.hero.gl-on .hero__title { color:transparent } (keeps the h1 for screen readers/SEO once WebGL draws it)
.hero__reveal { position:absolute; inset:0; width:100%; height:100%; z-index:2; pointer-events:none; display:block }
.chrome { position:absolute; inset:0; z-index:3; pointer-events:none; mix-blend-mode:difference; color:#fff; padding:var(--gutter); display:flex; flex-direction:column; justify-content:space-between }
(mix-blend-mode difference makes the UI read dark on paper and turn light over the revealed video)
.chrome a { pointer-events:auto }
.intro__lede { font-size:clamp(16px, 1.35vw, 22px); line-height:1.12; letter-spacing:-0.01em; font-weight:400 }
.cta { display:inline-flex; align-items:center; gap:10px; margin-top:clamp(16px, 1.6vw, 26px); padding:11px 16px 11px 18px; border-radius:999px; background:#fff; color:#000; font:500 11px/1 var(--body); letter-spacing:0.06em; text-transform:uppercase; text-decoration:none; transition:transform .35s cubic-bezier(.2,.8,.2,1) }
.cta svg { width:14px; height:14px; transition:transform .35s cubic-bezier(.2,.8,.2,1) }
.cta:hover svg { transform:translateX(3px) }
.cta:active { transform:scale(.97) }
.cta:focus-visible, .links a:focus-visible { outline:2px solid #fff; outline-offset:3px }
.foot { display:flex; justify-content:space-between; align-items:flex-end; gap:16px; font-size:11px; line-height:1.2 }
.links { display:flex; gap:8px; align-items:center; font-weight:500; letter-spacing:0.06em; text-transform:uppercase }
.links a { color:inherit; text-decoration:none }
.links a:hover { text-decoration:underline; text-underline-offset:3px }
.hint { position:absolute; left:50%; bottom:calc(var(--gutter) + 1px); transform:translateX(-50%); font-size:11px; letter-spacing:0.06em; text-transform:uppercase; opacity:.5; transition:opacity .6s ease; white-space:nowrap }
.hint.is-gone { opacity:0 }
.hint .touch { display:none }
@media (hover:none) { .hint .mouse { display:none } .hint .touch { display:inline } }
MOBILE @media (max-width:767px):
.hero__video { inset:auto; left:0; top:50%; width:100%; height:auto; aspect-ratio:16/9; transform:translateY(-50%); object-fit:contain }
(video is full width, height auto, centered vertically; the rest of the hero is black)
.intro__lede { font-size:16px }
.foot { font-size:10px }
.hint { bottom:calc(var(--gutter) + 24px) }
═══════════════════════════════════════
8. JAVASCRIPT — wrap everything in an IIFE
═══════════════════════════════════════
Constants:
- WORD = { x0:0.055, x1:0.945, y0:0.33, y1:0.665 } → where the balloon word sits inside the 16:9 video frame (fractions of video width/height)
- VIDEO_AR = 16/9
- PAPER = [0xf3/255, 0xf3/255, 0xee/255], INK = [0x0b/255, 0x0b/255, 0x0b/255]
- TRAIL_SCALE = 0.35 (trail canvas resolution relative to CSS px)
- DPR = min(devicePixelRatio, 2)
- reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches
(a) Video rect (mirror the CSS so JS knows where the video is on screen):
if W < 768: rect = { x:0, w:W, h:W/VIDEO_AR, y:(H - W/VIDEO_AR)/2 }
else cover: if W/H > VIDEO_AR → w=W, h=W/VIDEO_AR, else h=H, w=H*VIDEO_AR; x=(W-w)/2, y=(H-h)/2
(b) Headline texture: an offscreen 2D canvas (textCv) the same pixel size as the main canvas, ctx.setTransform(DPR,0,0,DPR,0,0).
- Measure "IMAGINE" at probe size 200px with font `800 200px Outfit, "Arial Black", sans-serif`, and ctx.letterSpacing = -0.02*size px when supported. Glyph width gw = actualBoundingBoxLeft + actualBoundingBoxRight, glyph height gh = actualBoundingBoxAscent + actualBoundingBoxDescent.
- wordW = (x1-x0)*rect.w; wordH = (y1-y0)*rect.h; cx = rect.x + (x0+x1)/2*rect.w; cy = rect.y + (y0+y1)/2*rect.h
- scale s = min( min(wordW, W*0.96)/gw , (wordH*1.02)/gh ); size = 200*s
- Re-measure at that size and fillText('IMAGINE') in WHITE (only its alpha is used) so the glyph bounding box is centered on (cx, cy): x = cx - w/2 + actualBoundingBoxLeft, y = cy - h/2 + actualBoundingBoxAscent, textBaseline 'alphabetic'.
- Re-upload this texture on resize and again after document.fonts.load('800 100px Outfit') resolves.
(c) Trail canvas (2D, size W*0.35 × H*0.35). Alpha = hole strength; R/G = stroke velocity (128 = still).
stamp(x, y, r, vx, vy, a):
k = 0.012; R = round(128 + clamp(vx*k, -1, 1)*127); G = round(128 + clamp(vy*k, -1, 1)*127)
radial gradient at (x,y)*0.35, radius r*0.35: stop 0 → rgba(R,G,0,a), stop 0.5 → rgba(R,G,0,a*0.85), stop 1 → rgba(R,G,0,0); fill the circle.
baseRadius = max(W,H) * (W < 768 ? 0.085 : 0.055)
Every frame, with f = dt*60 (frame-rate independent):
1. Fade: globalCompositeOperation 'destination-out', fillRect with rgba(0,0,0, 1 - (1-0.03)^f), then back to 'source-over'.
2. If the pointer is active and moved (dist > 0.5px):
- smooth velocity: vx += (dx/f - vx)*0.5 (same for vy); speed = hypot(vx,vy)
- R = baseRadius * (0.75 + min(speed/45, 1)*0.75)
- interpolate from the previous point to the current one in steps = ceil(dist/(R*0.22)); at each step stamp(x, y, R, vx*6, vy*6, 0.9)
- unless reduceMotion: spawn n = min(3, floor(speed/14)) droplets: angle = atan2(vy,vx) + (rand-0.5)*1.4, speed sp = speed*(0.35 + rand*0.45), position = pointer ± (rand-0.5)*R, radius R*(0.25 + rand*0.45), life 1. Cap at 90 droplets (drop the oldest).
- if not moving: vx,vy *= 0.8
3. Droplets: x += vx*f; y += vy*f; vx *= 0.9^f; vy = vy*0.9^f + 0.25*f (gravity → drips); life -= 0.022*f; r *= 0.985^f; remove when life <= 0 or r < 2; else stamp(x, y, r, vx*6, vy*6, 0.85*life).
(d) Input:
- pointermove on .hero (ignore pointerType 'touch') → onMove(localX, localY); pointerleave (non-touch) → pointer inactive.
- touchstart / touchmove (passive:true) using touches[0]; on touchstart reset the pointer and also stamp(x, y, baseRadius*1.1, 0, 0, 0.95) so a tap alone opens a hole; touchend → inactive.
- onMove: on first move set prev = current; record lastMove = now; add .is-gone to #hint; wake the loop.
(e) WebGL renderer on #heroReveal: getContext('webgl', { premultipliedAlpha:true, alpha:true, antialias:false, depth:false }). Full-screen quad (TRIANGLE_STRIP, -1..1). Two textures, CLAMP_TO_EDGE + LINEAR: unit 0 = headline canvas, unit 1 = trail canvas (re-uploaded every frame). gl.pixelStorei(UNPACK_PREMULTIPLY_ALPHA_WEBGL, false). If WebGL fails, return early and leave the static CSS paper + h1 (no effect). On success add class "gl-on" to .hero.
Vertex shader:
attribute vec2 aPos; varying vec2 vUv;
void main(){ vUv = vec2(aPos.x*.5+.5, .5-aPos.y*.5); gl_Position = vec4(aPos,0.,1.); }
Fragment shader (precision highp; include the standard Ashima/Stefan Gustavson 3D simplex noise `float snoise(vec3)`):
uniform sampler2D uText, uTrail; uniform vec2 uRes; uniform vec3 uPaper, uInk; uniform float uTime;
void main(){
vec2 uv = vUv;
vec2 asp = vec2(uRes.x/uRes.y, 1.);
float n1 = snoise(vec3(uv*asp*2.6, uTime*.35));
float n2 = snoise(vec3(uv*asp*2.6 + 17.3, uTime*.35));
vec4 t = texture2D(uTrail, uv + vec2(n1,n2)*.018); // wobble the trail lookup
float m = t.a + snoise(vec3(uv*asp*7., uTime*.6))*.05; // liquid rim noise
float hole = smoothstep(.40, .47, m); // gooey metaball threshold
vec2 vel = (t.rg - .5)*2.;
float drag = smoothstep(.03, .42, t.a);
vec2 suv = uv - vel*drag*vec2(.12/asp.x, .12); // smear the letters along the stroke
float ink = texture2D(uText, suv).a;
vec3 col = mix(uPaper, uInk, ink);
float a = 1. - hole;
gl_FragColor = vec4(col*a, a); // premultiplied; transparent where the hole is
}
Uniforms: uText=0, uTrail=1, uPaper=PAPER, uInk=INK, uRes=(W,H) in CSS px, uTime=seconds.
(f) Resize (window 'resize' + on init): W = hero.clientWidth, H = hero.clientHeight; canvas size = W*DPR × H*DPR; trail size = max(2, round(W*0.35)) × max(2, round(H*0.35)); recompute rect; gl.viewport; redraw the headline texture; draw one frame so the paper shows immediately.
(g) Render loop sleeps when idle: wake() starts requestAnimationFrame only if not running and the hero is visible. tick: dt = clamp((now-last)/1000, 0.001, 0.05); updateTrail(dt); draw. If now - lastMove > 4500 ms and there are no droplets: stop, clearRect the trail, draw one final clean frame.
(h) IntersectionObserver on .hero: play the video when visible, pause when not. Call video.play().catch(()=>{}) on load.
═══════════════════════════════════════
9. ACCEPTANCE CHECKS
═══════════════════════════════════════
- Desktop: off-white page, black IMAGINE centered at about 89% of viewport width (16:9 screen); moving the mouse opens black liquid blobs showing the balloon letters aligned under the black letters; droplets fling off and drip; the black letters smear at the rim; it all melts closed about 1.5 s after stopping.
- The top-left text, pill button, bottom text and links stay readable over both paper and video (difference blend).
- Mobile (375px): the video is full width, height auto, vertically centered; the headline sits in front of it and lines up; touching or dragging reveals the video; vertical page scroll still works.
- No console errors; works when opened directly from disk via file://.[/hidecontent]