类型:Site模板 · 收录:2026-10-08 21:01:31 · 源码:会员可见
Build a single standalone index.html (inline <style> and <script>, no build step, no libraries, no local assets) containing one 100vh hero section. Use raw WebGL1 for the effects. Follow every value below exactly.
=== ASSETS (use these exact URLs, hot-linked, do not download) ===
FRONT dragon (plain white porcelain, transparent PNG, 1024x1024):
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261004_073040_d706a5ff-b2fc-4330-923c-b16405025697.png
BACK dragon (same dragon, same pose, cobalt-blue cloud motifs + gold kintsugi, transparent PNG, 1024x1024, pixel-aligned with FRONT):
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261004_073023_751a55cc-be81-4346-9f91-3c00987f54ef.png
FLORAL corner (blue-and-white peonies in the top-right corner of a transparent square PNG):
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261004_073106_56d0499c-5908-4447-86b8-465d712ca552.png
=== HEAD ===
<title>Kairo Studio — Rise gently</title>, lang="en", viewport meta "width=device-width, initial-scale=1".
Preconnect to fonts.googleapis.com, fonts.gstatic.com (crossorigin) and https://d8j0ntlcm91z4.cloudfront.net (crossorigin).
Google Fonts link: https://fonts.googleapis.com/css2?family=Jost:wght@400;500&family=Playfair+Display:ital,wght@1,500&display=swap
=== TOKENS (:root) ===
--paper: #f7f5ef; --ink: #111111; --cobalt: #1d4fbf; --sky: #a9cbf3; --gold: #c9a24b;
--dragon: min(112svh, 168vw); /* on-screen side of the square dragon artwork */
=== BASE CSS ===
* { box-sizing: border-box; margin: 0; }
html, body { height: 100%; }
body { background: var(--paper); color: var(--ink); font-family: "Jost", "Century Gothic", sans-serif; -webkit-font-smoothing: antialiased; }
.hero { position: relative; height: 100vh; height: 100svh; min-height: 480px; overflow: hidden; isolation: isolate; background: var(--paper); }
=== MARKUP, in this order inside <header class="hero" id="hero"> ===
1. <canvas class="wash" id="wash" aria-hidden="true">
2. <img class="floral floral--tr" alt="" src=FLORAL>
3. <img class="floral floral--bl" alt="" src=FLORAL>
4. <h1 class="headline"><span>Rise</span><span>gently</span></h1>
5. <div class="dragon" id="dragon"> containing:
<img class="front" id="front" crossorigin="anonymous" src=FRONT alt="A white porcelain dragon coiled in an S-curve; moving the pointer over it reveals cobalt-blue cloud motifs and gold kintsugi seams.">
<img class="back" id="back" crossorigin="anonymous" alt="" src=BACK>
<canvas id="reveal" aria-hidden="true">
6. <nav class="nav" aria-label="Primary"> containing:
<a href="#">Kairo Studio / Est. 2026</a>
<ul> with four <li><a href="#">: Listen, Culture & Makers, Learn, Reserve
<button class="menu" type="button" aria-label="Open menu"> holding a 12x12 inline SVG (viewBox 0 0 12 12, fill currentColor) of a 3x3 dot grid: circles r=1 at cx/cy in {1.5, 6, 10.5}.
=== LAYER CSS (z-order is the whole point: cloud < florals < headline < dragon < nav) ===
.wash { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.floral { position: absolute; z-index: 2; width: min(30vw, 52svh); pointer-events: none; user-select: none; }
.floral--tr { top: -5svh; right: -3vw; }
.floral--bl { bottom: -5svh; left: -3vw; transform: rotate(180deg); }
.headline { position: absolute; z-index: 3; left: 50%; top: 52%; transform: translate(-50%, -50%);
font-family: "Playfair Display", "Bodoni MT", Didot, serif; font-style: italic; font-weight: 500;
font-size: min(24vw, 43svh); line-height: 0.8; letter-spacing: -0.035em; white-space: nowrap;
pointer-events: none; user-select: none; -webkit-user-select: none; }
.headline span { display: block; }
.headline span:nth-child(2) { padding-left: 0.72em; }
.dragon { position: absolute; z-index: 4; left: 50%; top: 50%; width: var(--dragon); height: var(--dragon);
transform: translate(-52%, -47%); pointer-events: none; }
.dragon canvas, .dragon img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.dragon canvas { opacity: 0; transition: opacity 0.5s ease; }
.dragon img { transition: opacity 0.5s ease; }
.dragon .back { opacity: 0; }
.dragon.is-live canvas { opacity: 1; }
.dragon.is-live img { opacity: 0; }
.hero:hover .dragon:not(.is-live) .back { opacity: 1; } /* no-WebGL fallback: crossfade on hover */
.nav { position: absolute; z-index: 5; inset: 0 0 auto 0; display: flex; align-items: center; justify-content: space-between;
gap: 24px; padding: 18px 32px; font-size: 11px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; }
.nav a { color: inherit; text-decoration: none; padding: 6px 0;
background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat;
transition: background-size 0.35s cubic-bezier(.2,.7,.2,1); }
.nav a:hover { background-size: 100% 1px; } /* underline draws in from the left */
.nav ul { display: flex; flex: 1; justify-content: space-around; list-style: none; padding: 0 0 0 4vw; }
.menu { width: 28px; height: 28px; display: grid; place-items: center; border: 1.5px solid var(--ink); border-radius: 6px;
background: transparent; color: var(--ink); cursor: pointer; transition: background 0.25s, color 0.25s; }
.menu:hover { background: var(--ink); color: var(--paper); }
.nav a:focus-visible, .menu:focus-visible { outline: 2px solid var(--cobalt); outline-offset: 4px; }
@media (max-width: 760px) {
.nav { padding: 16px 20px; }
.nav ul { display: none; }
.headline { top: 50%; font-size: min(31vw, 30svh); }
.floral { width: 72vw; }
}
@media (prefers-reduced-motion: reduce) {
.dragon canvas, .dragon img, .nav a, .menu { transition: none; }
}
=== JAVASCRIPT (one IIFE) ===
Constants:
TRAIL = { radius: 0.105, strength: 1.0, dissipation: 0.966, momentum: 9, fluidity: 1.7, simSize: 256 }
reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches
Shared vertex shader (fullscreen quad, TRIANGLE_STRIP, positions [-1,-1, 1,-1, -1,1, 1,1], attribute location 0):
attribute vec2 aPos; varying vec2 vUv;
void main() { vUv = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); }
Shared GLSL noise, prepended to every fragment shader after "precision highp float;":
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x),
mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 5; i++) { v += a * noise(p); p = p * 2.03 + 17.1; a *= 0.5; }
return v;
}
--- A. Watercolour cloud (canvas #wash, its own WebGL context, { premultipliedAlpha: true, antialias: false }) ---
Fragment shader:
uniform vec2 uCenter; uniform float uSide, uTime;
void main() {
vec2 p = (gl_FragCoord.xy - uCenter) / uSide;
p.x *= 0.66;
float t = uTime * 0.02;
vec2 q = p * 2.4;
vec2 w = vec2(fbm(q + t), fbm(q + vec2(5.2, 1.3) - t));
float n = fbm(q * 1.3 + w * 2.2);
float n2 = fbm(q * 3.6 + w * 3.0 + 9.0);
float f = (1.0 - length(p) / 0.45) + (n - 0.5) * 1.25;
float a = smoothstep(0.0, 0.2, f);
a *= smoothstep(-0.05, 0.3, f + (n2 - 0.5) * 1.0);
vec3 sky = vec3(0.663, 0.796, 0.953);
vec3 cobalt = vec3(0.114, 0.310, 0.749);
float depth = smoothstep(0.12, 0.85, f * (0.45 + n2 * 1.15));
vec3 col = mix(sky, cobalt, depth);
col = mix(col, vec3(0.97, 0.98, 1.0), smoothstep(0.6, 0.85, n2) * 0.5);
a *= 0.6 + 0.4 * depth;
float grain = hash(gl_FragCoord.xy) - 0.5;
a = clamp(a + grain * 0.14 * a, 0.0, 1.0);
gl_FragColor = vec4(col * a, a);
}
Resize: scale = min(devicePixelRatio, 1.5) * 0.75; canvas backing size = hero rect * scale.
With r = hero rect and d = .dragon rect:
uCenter.x = (d.left - r.left + d.width * 0.53) * scale
uCenter.y = (r.height - (d.top - r.top + d.height * 0.5)) * scale
uSide = d.width * scale
Draw every frame with uTime = seconds. If reduceMotion, draw once per resize with uTime = 12 and never animate.
--- B. Dragon reveal (canvas #reveal, its own WebGL context, { premultipliedAlpha: true, antialias: false }) ---
Canvas backing size: square, min(2048, dragon rect width * min(devicePixelRatio, 2)).
Upload FRONT and BACK <img> as textures with UNPACK_FLIP_Y_WEBGL = true and UNPACK_PREMULTIPLY_ALPHA_WEBGL = true, LINEAR filtering, CLAMP_TO_EDGE.
Two 256x256 RGBA/UNSIGNED_BYTE framebuffer textures (LINEAR, CLAMP_TO_EDGE, cleared to black) used as a ping-pong trail mask.
Simulation fragment shader (renders into the write target, then swap):
uniform sampler2D uPrev; uniform vec2 uA, uB, uTexel;
uniform float uStrength, uRadius, uDecay, uFrames, uFluid, uTime; varying vec2 vUv;
void main() {
vec2 flow = vec2(noise(vUv * 5.0 + vec2(0.0, uTime * 0.3)),
noise(vUv * 5.0 + vec2(7.3, -uTime * 0.3))) - 0.5;
vec2 uv = vUv - flow * uTexel * uFluid * 2.0;
float c = texture2D(uPrev, uv).r * 0.5
+ (texture2D(uPrev, uv + vec2(uTexel.x, 0.0)).r
+ texture2D(uPrev, uv - vec2(uTexel.x, 0.0)).r
+ texture2D(uPrev, uv + vec2(0.0, uTexel.y)).r
+ texture2D(uPrev, uv - vec2(0.0, uTexel.y)).r) * 0.125;
c = c * uDecay - 0.003 * uFrames;
vec2 pa = vUv - uA, ba = uB - uA;
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
float d = length(pa - ba * h);
c += smoothstep(uRadius, 0.0, d) * uStrength;
gl_FragColor = vec4(vec3(clamp(c, 0.0, 1.0)), 1.0);
}
Uniforms per step: uTexel = (1/256, 1/256); uRadius = 0.105; uFluid = 1.7; uFrames = frames; uDecay = pow(0.966, frames); uA/uB = previous and current brush position; uTime = seconds.
Display fragment shader (renders to the canvas, cleared to transparent):
uniform sampler2D uMask, uFront, uBack; varying vec2 vUv;
void main() {
float m = texture2D(uMask, vUv).r;
float n = fbm(vUv * 11.0);
m = smoothstep(0.1, 0.55, m + (n - 0.5) * 0.55 * smoothstep(0.0, 0.3, m));
gl_FragColor = mix(texture2D(uFront, vUv), texture2D(uBack, vUv), m);
}
--- C. Pointer and frame loop ---
State: aim = [0.5, 0.5] (cursor), brush = [0.5, 0.5] (lagging brush), hasPointer = false, intro = reduceMotion ? 1 : 0.
window 'pointermove' (passive): convert to dragon-square UV: u = (clientX - rect.left) / rect.width, v = 1 - (clientY - rect.top) / rect.height, where rect = .dragon bounding rect (refreshed on resize). On the first move, snap brush to that UV, set hasPointer = true and intro = 1. Always set aim to it. The listener is on window, so the trail follows the cursor across the whole hero.
requestAnimationFrame loop, each frame:
elapsed = (now - last) / 1000; dt = min(0.05, elapsed); time = now / 1000.
Draw the cloud (unless reduceMotion).
If the dragon textures are loaded:
Opening sweep while intro < 1: intro = min(1, intro + dt / 1.6); y = 0.9 - intro * 0.8; aim = [0.53 + sin(y * 9.5 + 1.2) * 0.13, y]; on the very first sweep frame (no pointer yet and intro <= dt) snap brush to aim.
from = copy of brush; k = 1 - exp(-dt * 9); brush += (aim - brush) * k.
moved = distance(from, brush); strength = min(1, moved * 90) * 1.0.
Run the simulation step with (from, brush, strength, frames = min(elapsed, 1) * 60, time), then run the display pass.
Boot: call resize; wait for both dragon images (img.decode(), falling back to onload); upload textures; resize; draw once; add class "is-live" to .dragon; start revealing. If WebGL or image loading fails, log a warning and leave the CSS hover crossfade fallback in place.
window 'resize': refresh rect, resize both canvases.
=== RESULT TO EXPECT ===
Cream page. A soft animated cobalt-to-sky watercolour cloud behind everything. Huge black italic "Rise / gently" (second line indented) sitting behind a tall white porcelain dragon. Blue peonies in the top-right and bottom-left corners. Small letter-spaced uppercase nav across the top with a rounded-square dot-grid menu button. On load, a trail sweeps once down the dragon; moving the cursor paints a soft, drifting, organic-edged trail that reveals the blue-and-gold patterned dragon and fades back to white in about a second. Under 760px the middle nav links hide, the headline shrinks and the florals grow to 72vw.[/hidecontent]