类型:Site模板 · 收录:2026-10-28 23:49:11 · 源码:会员可见
Build a single, standalone index.html file (all HTML, CSS and JS inline, no build step, no frameworks, no local assets) that recreates the following full-screen hero section exactly. It must be fully mobile responsive and work with both mouse and touch.
========================================
1. CONCEPT
========================================
A minimal off-white studio landing hero with a giant black wordmark "CRAFTLINE" centered on screen. As the user moves the cursor (or drags a finger), it leaves a gooey, organic, liquid "ink blob" trail that REVEALS a looping background video of colorful ink clouds (cobalt blue, hot magenta, tangerine orange on black) underneath. The blobs have boiling, noise-warped edges, a pale lavender-grey ghost rim offset slightly up-left, they slowly dissolve after the cursor passes, and fast movements fling off small satellite droplets that drift and fade. Inside the revealed area the wordmark turns WHITE, with a glitchy double-image offset driven by cursor velocity. Outside the blobs the page is plain off-white with black text. The video is never visible except through the blobs.
========================================
2. HEAD / FONTS / META
========================================
- <!doctype html>, <html lang="en">, <meta charset="utf-8">, <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Craftline — Ink Studio Home</title>
- Google Font "Inter Tight", weights 400;500;600;800;900:
<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;600;800;900&display=swap" rel="stylesheet" />
========================================
3. BACKGROUND VIDEO (use this exact URL, loaded remotely)
========================================
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261019_164212_f2ea9659-cd8a-4e4a-a4ea-d20479d1011d.mp4
- The clip is 16:9, 1080p, ~10.04 s, no audio. The server sends Access-Control-Allow-Origin: *.
- Add TWO <video> elements inside the hero, both with this same src, both: muted, playsinline, crossorigin="anonymous", preload="auto". The first one also has autoplay. Do NOT use the loop attribute.
ids: bgVideo, bgVideo2; class "bg-video".
- .bg-video { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; } (they are only used as WebGL texture sources; never shown directly).
- Seamless loop by cross-fading: play video A; when A.currentTime >= duration − 1.4 s (or A.ended), if B is paused set B.currentTime = 0 and play B; mix = min(1, (A.currentTime − (duration − 1.4)) / 1.4). When A.ended or mix >= 1, pause A, swap roles (current index flips), reset mix to 0. Feed mix to the shader as uMix and blend texture(A) → texture(B).
- Call play() on load and again on the first pointerdown (for mobile autoplay policies). Set a hasVideo flag = 1 on the first 'playing' event.
========================================
4. DESIGN TOKENS & BASE CSS
========================================
:root { --bg:#f4f4f2; --ink:#0d0d0d; --muted:#8a8a88; }
* { box-sizing:border-box; margin:0; padding:0; }
html, body { height:100%; }
body { background:var(--bg); color:var(--ink); font-family:"Inter Tight", system-ui, sans-serif; overflow:hidden; -webkit-font-smoothing:antialiased; }
.hero { position:relative; width:100%; height:100vh; height:100svh; overflow:hidden; background:var(--bg); cursor:none; }
========================================
5. HTML STRUCTURE (z-order bottom → top)
========================================
<section class="hero" id="hero">
<h1 class="word word--base" id="wordBase"><span>CRAFTLINE</span><i class="baseline" id="baseline"></i></h1> (z-index 1)
<canvas id="reveal"></canvas> (z-index 2, WebGL)
<div class="ui top-left">
<div class="eyebrow">Design ©26</div>
<p>Where the stale ideas are brushed<br/>under a carpet.</p>
<a class="pill" href="#">Start a project
<svg viewBox="0 0 8 8"><path d="M2 1l3 3-3 3" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
</a>
</div>
<ul class="ui nav">
<li><i></i><a href="#">Main</a></li>
<li><i></i><a href="#">Story</a></li>
<li><i></i><a href="#">Enquire</a></li>
</ul>
<div class="ui bottom-left"><span class="scroll-dash"></span>Browse</div>
<div class="ui bottom-right">Roam to unveil</div>
<video id="bgVideo" ...></video>
<video id="bgVideo2" ...></video>
</section>
<div class="cursor" id="cursor"></div>
========================================
6. COMPONENT CSS (exact values)
========================================
Wordmark:
.word { position:absolute; left:0; right:0; top:50%; transform:translateY(-50%); text-align:center; font-weight:900; letter-spacing:-0.045em; line-height:0.8; white-space:nowrap; user-select:none; pointer-events:none; }
.word span { display:inline-block; }
.word--base { color:var(--ink); z-index:1; }
.baseline { display:inline-block; width:0; height:0; vertical-align:baseline; } (used in JS to read the exact text baseline Y)
Font size is set in JS so the word spans exactly 90% of the hero width: set font-size 100px, measure the span width w, then font-size = 100 * (heroWidth * 0.9) / w px. Re-run on resize and after document.fonts.ready.
Reveal canvas:
#reveal { position:absolute; inset:0; width:100%; height:100%; z-index:2; pointer-events:none; }
UI chrome:
.ui { position:absolute; z-index:4; font-size:13px; line-height:1.25; }
.top-left { top:26px; left:30px; max-width:280px; }
.top-left .eyebrow { font-size:10px; color:var(--muted); margin-bottom:8px; letter-spacing:.02em; }
.top-left p { font-weight:500; color:#3a3a3a; }
.pill { display:inline-flex; align-items:center; gap:8px; margin-top:12px; padding:7px 12px 7px 14px; background:var(--ink); color:#fff; border-radius:999px; font-size:10px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; text-decoration:none; cursor:none; transition:transform .3s cubic-bezier(.2,.8,.2,1); }
.pill:hover { transform:translateX(3px); }
.pill svg { width:8px; height:8px; }
.nav { top:26px; right:30px; list-style:none; }
.nav li { display:flex; align-items:center; gap:8px; margin-bottom:3px; }
.nav li i { display:inline-block; width:14px; height:6px; border-radius:3px; background:#cfcfcc; transition:background .3s, width .3s; }
.nav a { color:#555; text-decoration:none; cursor:none; transition:color .3s; }
.nav li:hover i { background:var(--ink); width:20px; }
.nav li:hover a { color:var(--ink); }
.bottom-left { bottom:24px; left:30px; font-size:10px; color:var(--muted); }
.bottom-right { bottom:24px; right:30px; font-size:10px; color:var(--muted); }
.scroll-dash { display:inline-block; width:18px; height:3px; background:#bdbdba; border-radius:2px; vertical-align:middle; margin-right:8px; }
Custom cursor (desktop):
.cursor { position:fixed; z-index:10; top:0; left:0; width:10px; height:10px; margin:-5px 0 0 -5px; border-radius:50%; background:var(--ink); mix-blend-mode:difference; filter:invert(1); pointer-events:none; transition:width .25s, height .25s, margin .25s; }
.cursor.big { width:34px; height:34px; margin:-17px 0 0 -17px; }
Move it with transform: translate(clientX px, clientY px) on pointermove. Add class "big" on pointerenter of .pill and .nav a, remove on pointerleave.
Mobile (max-width:640px):
.top-left { top:18px; left:16px; max-width:200px; }
.nav { top:18px; right:16px; }
.bottom-left { left:16px; } .bottom-right { right:16px; }
.hero, .pill, .nav a { cursor:auto; }
.cursor { display:none; }
The wordmark auto-fits to 90% of the width on every screen size, so it stays edge-to-edge on phones. Touch drives the blob trail via touchmove (passive listener, using touches[0]).
========================================
7. TRAIL MASK (2D canvas, offscreen)
========================================
- An offscreen <canvas> "trail" at 0.25× the hero size (TRAIL_SCALE = 0.25), filled black initially. On resize, copy the old trail scaled into the new one so blobs survive resizes.
- stamp(x, y, r, a): radial gradient centered at (x,y)*0.25, radius r*0.25, color stops: 0 → rgba(255,255,255,a), 0.5 → rgba(255,255,255,a*0.75), 1 → rgba(255,255,255,0). Fill a circle. Draw with globalCompositeOperation = 'lighter'.
- Every frame (dt = min(48ms, elapsed)/16.667):
• Fade: composite 'source-over', fill whole trail with rgba(0,0,0, 0.022*dt), then switch back to 'lighter'.
• phase += 0.03*dt.
• If the pointer is active: dx,dy = movement since the last frame; dist = hypot. Smooth velocity: vx += (dx − vx)*0.25, vy likewise. speed = hypot(vx,vy).
base = min(W,H) * 0.075
rad = base + min(speed,60)*1.6 + sin(phase*2.3)*base*0.15
steps = max(1, ceil(dist / (rad*0.25))); stamp along the segment at each step with alpha 0.22.
If dist < 0.5 (resting cursor), stamp(mouse, base*1.1, 0.06) to keep the blob alive.
Droplets: if speed > 8 and random < 0.12*dt, spawn one at angle = atan2(vy,vx) + π + (random−0.5)*2.2, positioned rad*1.3 away from the cursor, velocity cos/sin(angle)*(1 + random*2), radius base*(0.18 + random*0.25), life 1.
• Else: vx,vy *= 0.9.
• Update droplets: move by velocity*dt, velocity *= 0.96, life −= 0.012*dt, stamp(x,y,r, 0.35*max(0,life)); remove when life <= 0.
- pointerleave on the hero sets the pointer inactive. The first move after becoming active resets the previous position to the current one (no jump line).
========================================
8. WHITE WORDMARK TEXTURE
========================================
- An offscreen canvas the same size as the WebGL canvas. Set transform to DPR scale, clear, font = `${computed fontWeight} ${fontSize}px ${computed fontFamily}`, fillStyle #fff, textBaseline 'alphabetic'.
- Draw glyph by glyph so the letter-spacing matches the DOM exactly: x starts at the span's left edge (relative to the hero), y = the .baseline element's bottom (relative to the hero); after each char, x += measureText(ch).width + computed letterSpacing (px).
- Re-draw whenever the wordmark is refitted, then re-upload it as a texture.
========================================
9. WEBGL RENDERER (full-screen quad on #reveal)
========================================
- getContext('webgl', { premultipliedAlpha:true, alpha:true, antialias:false }). DPR = min(devicePixelRatio, 1.5). Canvas size = hero size × DPR.
- Two triangles covering clip space; vertex shader: vUv = p*0.5+0.5.
- Textures (all CLAMP_TO_EDGE, LINEAR): unit 0 = trail (re-upload every frame), unit 1 = current video, unit 2 = white-text canvas, unit 3 = next video (uploaded only while uMix > 0). Initialize each with a 1×1 black pixel.
- Uniforms: uTrail, uVideo, uVideoNext, uMix, uRes (canvas px), uVideoRes (videoWidth/Height, or 16,9 before load), uTime (seconds), uHasVideo (0/1), uText, uTextOff.
- Wrap the video texImage2D in try/catch; on error set hasVideo = 0.
- Clear to transparent each frame, then draw.
Fragment shader logic (precision highp; include the standard Ashima/Ian McEwan 2D simplex noise snoise()):
trailAt(uv) = texture2D(uTrail, vec2(uv.x, 1.0-uv.y)).r
coverUv(uv): sr = uRes.x/uRes.y, vr = uVideoRes.x/uVideoRes.y; s = sr>vr ? vec2(1, vr/sr) : vec2(sr/vr, 1); return (uv-0.5)*s+0.5 (object-fit: cover)
asp = vec2(uRes.x/uRes.y, 1); t = uTime*0.25
n = vec2(snoise(uv*asp*3.2 + vec2(t,-t)), snoise(uv*asp*3.2 + vec2(-t,t) + 17.0))
n2 = vec2(snoise(uv*asp*9.0 + t*2.0), snoise(uv*asp*9.0 - t*2.0 + 5.0))
wuv = uv + n*0.03 + n2*0.006 // boiling organic edge
v = trailAt(wuv); mask = smoothstep(0.42, 0.445, v)
vg = trailAt(wuv + vec2(-0.008, 0.010)); rim = smoothstep(0.33, 0.35, vg) * (1.0 - mask) // offset ghost halo
e = 1.5/uRes.x; grad = central differences of trailAt at ±e*4 in x and y
vuv = coverUv(uv + grad*0.06); vuv.y = 1.0 - vuv.y // slight liquid refraction at the edges
vid = mix(texture2D(uVideo,vuv).rgb, texture2D(uVideoNext,vuv).rgb, uMix)
Fallback while loading: fb = mix(vec3(0.10,0.25,1.0), vec3(1.0,0.15,0.6), smoothstep(0.1,0.6, uv.x + n.x*0.2)); fb = mix(fb, vec3(1.0,0.55,0.1), smoothstep(0.55,0.95, uv.x + n.y*0.2)); vid = mix(fb, vid, uHasVideo)
vid = pow(vid, vec3(0.92)) * 1.06 // slight punch
tuv = vec2(uv.x, 1.0-uv.y)
txt = texture2D(uText, tuv - uTextOff).a
txt2 = texture2D(uText, tuv - uTextOff*2.2).a
vid = mix(vid, vec3(1.0), max(txt, txt2*0.35)) // white wordmark + faint glitch ghost
rimCol = vec3(0.86, 0.86, 0.90)
alpha = mask + rim*0.55
color = vid*mask + rimCol*rim*0.55
gl_FragColor = vec4(color, alpha) // premultiplied
Glitch offset (JS, every frame): offX += (vx*0.35 − offX)*0.12; offY += (vy*0.12 − offY)*0.12; uTextOff = (offX/W, offY/H).
========================================
10. BEHAVIOUR CHECKLIST
========================================
- On load: plain off-white page, black 900-weight "CRAFTLINE" edge-to-edge (90% width), UI in the four corners, small inverted-dot custom cursor (desktop).
- Moving the cursor or a finger paints gooey blobs revealing the moving ink video; the edges boil continuously; the pale ghost rim sits up-left of each blob; blobs dissolve over ~1–2 s; fast flicks throw droplets.
- Letters inside blobs are white with a velocity-based split/ghost offset; letters outside stay black.
- The video loops seamlessly via the 1.4 s two-video cross-fade.
- requestAnimationFrame loop; resize keeps everything aligned; no page scroll (body overflow hidden).
- No external JS libraries; only the Google Font and the CloudFront video are loaded remotely.[/hidecontent]