提示詞
Build a single standalone index.html file (inline CSS + inline JS, no local assets, no build step) containing ONE full-screen hero section: a black "Design World" landing hero with a rotatable 3D glass cuboid in the centre that refracts a huge background headline with chromatic dispersion. Recreate it exactly to the spec below.
==================================================
1. EXTERNAL RESOURCES (use these exact URLs)
==================================================
- Font: Google Fonts Poppins, weights 300/400/500/600/700/800
<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=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
- Three.js r169 via importmap:
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js"
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
Imports: GLTFLoader (loaders/GLTFLoader.js), mergeVertices + mergeGeometries (utils/BufferGeometryUtils.js), RoundedBoxGeometry (geometries/RoundedBoxGeometry.js).
- 3D model (rounded cube GLB, CORS-enabled, load directly, do NOT download or replace):
const MODEL_URL = 'https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260929_212926_92423081-b0e4-4f5a-b650-14af6c05c058.glb';
If it fails to load, fall back to new RoundedBoxGeometry(1, 1, 1, 8, 0.12).
Page <title>: "Design World — Explore New Ideas".
==================================================
2. PAGE STRUCTURE (HTML)
==================================================
<section class="hero" id="hero">
<canvas id="scene" aria-label="Rotatable glass cube. Drag to rotate."></canvas>
<div class="ui">
<h1 class="sr-only">Explore New Ideas</h1>
<header class="nav">
<a href="#" class="logo"><i class="logo-mark"></i><b>Design</b><span>World</span></a>
<ul class="nav-links"><li><a>Home</a></li><li><a>Portfolio</a></li><li><a>Contact Us</a></li></ul>
</header>
<div class="arrows">
<button class="arrow" id="prev">[left-arrow SVG]</button>
<button class="arrow" id="next">[right-arrow SVG]</button>
</div>
<nav class="dots"><button class="dot"></button><button class="dot active"></button><button class="dot"></button></nav>
<p class="tagline">Let's Build the<br>Future of <strong>Design.</strong></p>
<div class="cta-row">
<a href="#" class="cta">Explore Now</a>
<span class="cta-line"></span>
<span class="count" aria-hidden="true">07</span>
</div>
<div class="loader" id="loader">Loading model</div>
</div>
</section>
Arrow SVGs: viewBox 0 0 24 24, fill none, stroke currentColor, stroke-width 2.6, round caps/joins.
prev path: "M19 12H5M11 6l-6 6 6 6" next path: "M5 12h14M13 6l6 6-6 6"
IMPORTANT: The big "Explore / New / Ideas" headline is NOT HTML text. It is drawn onto a 2D canvas inside WebGL so the glass cube can refract it (see section 4). The HTML only has a visually hidden <h1> for accessibility.
==================================================
3. CSS (exact values)
==================================================
:root { --bg:#000; --fg:#fff; --pad-x: clamp(20px, 6.95vw, 120px) }
html, body { height:100%; background:#000; color:#fff; font-family:'Poppins',sans-serif; -webkit-font-smoothing:antialiased }
.hero { position:relative; width:100%; height:100vh; height:100svh; min-height:520px; overflow:hidden; background:#000 }
#scene { position:absolute; inset:0; width:100%; height:100%; display:block; cursor:grab; touch-action:none }
#scene.dragging { cursor:grabbing }
.ui { position:absolute; inset:0; pointer-events:none; z-index:2 } /* drags pass through to the canvas */
.ui a, .ui button { pointer-events:auto }
.sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap }
NAV
.nav { position:absolute; top:clamp(24px,4.7vh,40px); left:var(--pad-x); right:var(--pad-x); display:flex; align-items:center; justify-content:space-between }
.logo { display:flex; align-items:center; gap:8px; color:#fff; text-decoration:none; font-size:15px; letter-spacing:-0.01em }
.logo-mark { width:20px; height:40px; background:#fff; border-radius:0 20px 20px 0 } /* white "D" half-circle */
.logo b { font-weight:700 } .logo span { font-weight:400 }
.nav-links { display:flex; gap:clamp(20px,4.1vw,60px); list-style:none }
.nav-links a { color:#fff; text-decoration:none; font-size:15px; font-weight:500; position:relative }
.nav-links a::after { content:''; position:absolute; left:0; right:0; bottom:-4px; height:1px; background:currentColor; transform:scaleX(0); transform-origin:right; transition:transform .35s ease }
.nav-links a:hover::after { transform:scaleX(1); transform-origin:left }
ARROWS (top-right, left of the dots column)
.arrows { position:absolute; top:18.9%; left:76.7%; display:flex; gap:32px; transform:translate(-19px,-50%) }
.arrow { width:38px; height:38px; border-radius:50%; border:2.5px solid #fff; background:transparent; color:#fff; display:grid; place-items:center; cursor:pointer; transition:background .25s, color .25s }
.arrow svg { width:18px; height:18px }
.arrow:hover { background:#fff; color:#000 }
PAGINATION DOTS (vertical, right edge)
.dots { position:absolute; right:calc(var(--pad-x) - 7px); top:49.4%; transform:translateY(-50%); display:flex; flex-direction:column; gap:33px }
.dot { width:14px; height:14px; border-radius:50%; border:2px solid #fff; background:#fff; cursor:pointer; transition:background .25s }
.dot.active { background:transparent } /* active = hollow ring, middle one active by default */
TAGLINE (bottom-left)
.tagline { position:absolute; left:var(--pad-x); bottom:clamp(40px,7.5vh,70px); font-size:clamp(24px,2.65vw,44px); line-height:1.2; font-weight:300; letter-spacing:-0.01em }
.tagline strong { font-weight:700; display:block }
CTA ROW (button → horizontal line → giant outlined "07" bleeding off the right edge)
.cta-row { position:absolute; left:45.6%; right:-1vw; top:88.7%; transform:translateY(-50%); display:flex; align-items:center }
.cta { flex:none; padding:0 15px; height:48px; display:inline-flex; align-items:center; border:1.5px solid rgba(255,255,255,.85); border-radius:6px; background:rgba(0,0,0,.15); color:#fff; font-size:14px; font-weight:400; text-decoration:none; cursor:pointer; transition:background .25s, color .25s }
.cta:hover { background:#fff; color:#000 }
.cta-line { flex:1; height:1.5px; background:rgba(255,255,255,.8); min-width:40px }
.count { flex:none; font-size:clamp(140px,19.8vw,360px); font-weight:400; line-height:1; letter-spacing:-0.02em; color:transparent; -webkit-text-stroke:1.5px rgba(255,255,255,.9); transform:translateY(6%); user-select:none }
LOADER
.loader { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); font-size:12px; letter-spacing:.2em; text-transform:uppercase; opacity:.6; transition:opacity .6s }
.loader.done { opacity:0 }
RESPONSIVE
@media (max-width:900px), (max-aspect-ratio:1/1) {
.arrows { left:auto; right:var(--pad-x); top:15%; transform:translateY(-50%); gap:14px }
.tagline { bottom:clamp(150px,20vh,220px) }
.cta-row { left:var(--pad-x); right:-3vw; top:auto; bottom:24px; transform:none }
.count { font-size:clamp(120px,22vw,200px) }
}
@media (max-width:640px) {
.nav-links { gap:16px }
.nav-links a, .logo { font-size:13px }
.dots { gap:20px; right:16px }
.dot { width:10px; height:10px }
.cta-row { right:-8vw }
.count { font-size:120px }
}
@media (max-width:420px) { .nav-links li:nth-child(2) { display:none } } /* hide "Portfolio" */
No horizontal page scroll at any width.
==================================================
4. WEBGL SCENE (Three.js)
==================================================
Renderer: WebGLRenderer({ canvas:#scene, antialias:true, alpha:false }), clearColor #000, outputColorSpace SRGBColorSpace, pixelRatio = min(devicePixelRatio, 2).
Camera: PerspectiveCamera(fov 30, aspect W/H, 0.1, 100) at (0,0,10) looking at origin.
4a. Background headline (drawn into an offscreen 2D canvas → CanvasTexture)
- Canvas size = viewport × DPR, filled #000.
- mobile = (W < 768) || (W/H < 1).
- fontSize fs = min(H*0.21, W*(mobile ? 0.21 : 0.118)); font = `800 ${fs}px Poppins`.
If the widest line is wider than W*(mobile ? 0.9 : 0.5), scale fs down to fit.
- Lines: "Explore", "New", "Ideas". Fill #e9e9e9, textAlign center, baseline alphabetic.
- Centre x = W*(mobile ? 0.5 : 0.505); centre y = H*(mobile ? 0.45 : 0.468).
Line gap = fs*1.07; cap height ≈ fs*0.7. Line i (0..2) baseline y = cy + cap/2 + (i-1)*gap.
- Texture: colorSpace SRGB, LinearFilter, no mipmaps. Redraw on resize and on document.fonts 'loadingdone'.
- Show it with a fullscreen quad (PlaneGeometry(2,2), ShaderMaterial, vertex outputs position.xy directly, fragment = texture2D(uTex, vUv) followed by #include <colorspace_fragment> ON ITS OWN LINE; depthTest/depthWrite false; frustumCulled false) in its own bgScene.
- Wait for fonts before first draw: Promise.race([Promise.all([document.fonts.load('800 100px Poppins'), document.fonts.ready]), 2500ms timeout]) then layout() + start loop.
4b. Model loading
- GLTFLoader.load(MODEL_URL). Traverse gltf.scene; for each mesh: clone geometry, delete 'uv','color','tangent' attributes, mergeVertices(g, 1e-4), computeVertexNormals(), applyMatrix4(mesh.matrixWorld). Merge all parts with mergeGeometries.
- Centre the geometry on its bounding box and scale uniformly so its largest dimension = 1.
- Scene graph: scene → pivot (screen position + scale) → spinner (rotation) → cube mesh. On load add class "done" to #loader.
4c. Cube placement (in layout(), on load and on resize)
- visH = 2*tan(fov/2)*10; visW = visH*aspect.
- Screen-fraction centre: sx = mobile ? 0.5 : 0.517, sy = mobile ? 0.45 : 0.488.
pivot.position = ((sx-0.5)*visW, (0.5-sy)*visH, 0).
- On-screen edge length px = min(H*0.44, W*(mobile ? 0.45 : 0.29)); pivot.scale = (px/H)*visH.
- Initial spinner rotation: Euler(-0.42, 0.62, 0.18).
4d. Glass material — custom ShaderMaterial, screen-space refraction with 6-band chromatic dispersion, rendered in two passes (back faces, then front faces).
Vertex shader:
varying vec3 vNormal; varying vec3 vEye;
worldPos = modelMatrix*position; mvPos = viewMatrix*worldPos; gl_Position = projectionMatrix*mvPos;
vNormal = normalize(normalMatrix*normal); vEye = normalize(https://mvpos.xyz/);
Fragment shader:
uv = gl_FragCoord.xy / uResolution (drawing-buffer size).
n = normalize(vNormal); if (uBackside > 0.5) n = -n; // do NOT use gl_FrontFacing, three.js flips winding for BackSide
eye = normalize(vEye).
LOOP = 16 iterations; slide = i/LOOP * 0.045.
For each band compute refract(eye, n, 1.0/ior) with iorR/Y/G/C/B/P and sample uTexture at
uv + refr.xy * (uRefractPower + slide*k) * uChromatic, with k = 1 (R), 1 (Y), 2 (G), 2.5 (C), 3 (B), 1 (P):
r = tex(R).x*0.5
y = (tex(Y).x*2 + tex(Y).y*2 - tex(Y).z)/6
g = tex(G).y*0.5
c = (tex(C).y*2 + tex(C).z*2 - tex(C).x)/6
b = tex(B).z*0.5
p = (tex(P).z*2 + tex(P).x*2 - tex(P).y)/6
R = r + (2p + 2y - c)/3; G = g + (2y + 2c - p)/3; B = b + (2c + 2p - y)/3
color += vec3(R,G,B)
color /= LOOP; saturation adjust: mix(luma(color), color, uSaturation) with luma weights (0.2125, 0.7154, 0.0721).
Specular (Blinn-Phong, light vector = normalize(-light), view = -eye):
spec = specular(uLight, uShininess, uDiffuseness) + 0.6 * specular(vec3(1,1,-1), uShininess*0.6, uDiffuseness*0.5)
spec(x) = pow(max(dot(n,halfVec),0), shininess) + max(0,dot(n,lightVec))*diffuseness
color += spec * (backside ? 0.35 : 1.0)
Fresnel: f = pow(1.0 + dot(eye, n), uFresnelPower); color = mix(color, vec3(1), f * (backside ? 0.25 : 0.55)).
color += vec3(0.004, 0.005, 0.007). gl_FragColor = vec4(color,1); then #include <colorspace_fragment> on its own line.
Uniform values:
uIorR 1.15, uIorY 1.16, uIorG 1.18, uIorC 1.22, uIorB 1.22, uIorP 1.22,
uRefractPower 0.30 (front) / 0.22 (back), uChromatic 0.5, uSaturation 1.08,
uShininess 90, uDiffuseness 0.02, uFresnelPower 5.0, uLight (-1, 1, 1),
uBackside 0 (frontMat, side FrontSide) / 1 (backMat, side BackSide).
4e. Render pipeline (every frame)
- Two WebGLRenderTargets sized to drawing buffer (W*DPR × H*DPR), type HalfFloatType, default (linear) colour space: rtBack, rtFront.
- backMat.uTexture = rtBack.texture; frontMat.uTexture = rtFront.texture.
1) render bgScene → rtBack
2) render bgScene → rtFront, then (autoClear=false) cube with backMat → rtFront
3) render bgScene → screen, then (autoClear=false, clearDepth) cube with frontMat → screen
- This gives the look: text seen through the cube is magnified, mirrored and split into blue/yellow fringes, inner rounded edges of the cube are faintly visible, and the bevelled edges glow white.
==================================================
5. INTERACTION & ANIMATION
==================================================
- Drag to rotate (pointer events on the canvas, setPointerCapture):
dx = Δx*0.008, dy = Δy*0.008; apply as world-space rotation: premultiply spinner.quaternion by axisAngle(Y, dx) then axisAngle(X, dy).
Track velocity per 16.67ms frame; add class "dragging" to the canvas while dragging.
- Inertia after release: keep applying velocity, damped by 0.94^(dt*60) per frame.
- Idle drift: 0.6s after release, blend in a constant slow rotation of 0.0035 rad/frame around Y and 0.0012 rad/frame around X (blend ramps 0→1 over 1s). It also runs from page load.
- Prev/Next arrow buttons: smoothly rotate the cube -90°/+90° around Y (ease: each frame apply remaining*min(1, 0.09*dt*60) until remaining < 0.0005). Cancels inertia; dragging cancels the spin.
- Dots: clicking a dot makes it the only .active one (hollow ring).
- Hover transitions: nav link underline wipe (.35s), arrow and CTA buttons invert to white bg / black content (.25s).
- Clamp frame dt to 0.05s. Recompute layout (renderer size, camera aspect, render-target sizes, uResolution, headline canvas, cube position/scale) on window resize.
==================================================
6. LAYOUT REFERENCE (desktop 1440×806)
==================================================
- Logo at (100, 38), 40px tall. Nav links right-aligned, right edge ≈ 1340, vertical centre ≈ 58.
- Arrow circles centred at ≈ (1105, 152) and (1175, 152).
- Dots at x ≈ 1340, y ≈ 351 / 398 / 445 (middle one hollow).
- Headline "Explore / New / Ideas" centred at x ≈ 727, ~170px ExtraBold, baselines ≈ 272 / 445 / 637.
- Glass cube occupies ≈ x 510–1000, y 145–600, slightly right of centre, overlapping the headline.
- Tagline "Let's Build the / Future of / Design." at left 100, bottom ≈ 60px, ~38px, Light with Bold "Design.".
- "Explore Now" button at (657, 691), 48px tall; a 1.5px line runs right to a huge outlined "07" whose "0" starts at x ≈ 1140 and whose "7" is clipped by the right and bottom edges.
- Pure black background, all UI white.
Mobile (≤767px or portrait): headline and cube centred at 50% x / 45% y, headline up to 90% width, arrows move under the nav on the right, tagline sits above the CTA row, and the CTA row is pinned 24px from the bottom with "07" partly off-screen on the right.
Output: one complete index.html, nothing else.中文翻譯
構建一個獨立、自包含的單文件 index.html(內聯 CSS + 內聯 JS,不使用任何本地資源,也不需要構建步驟),其中只包含一個全屏首屏區塊:一個黑色的 "Design World" 着陸頁首屏,中央有一個可旋轉的 3D 玻璃長方體,它折射出一個帶有色散的巨大背景標題。請嚴格按照下面給出的規格,將這一效果精確復刻出來,不要偏離規格。
==================================================
1. 外部資源(請使用以下確切的 URL 地址)
==================================================
- 字體:使用 Google Fonts 提供的 Poppins 字體,可用字重爲 300/400/500/600/700/800(通過下方的 link 標籤引入):
<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=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
- 通過 importmap(導入映射)引入 Three.js r169 版本,配置如下:
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js"
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
導入以下模塊:GLTFLoader(模型加載器,路徑爲 loaders/GLTFLoader.js)、mergeVertices(合併重複頂點)與 mergeGeometries(合併多個幾何體),後兩者的路徑爲 utils/BufferGeometryUtils.js;RoundedBoxGeometry(帶圓角的立方體幾何體,路徑爲 geometries/RoundedBoxGeometry.js)。
- 3D 模型(圓角立方體 GLB 文件,已啓用跨域資源共享 CORS,請直接加載,不要下載或替換):
const MODEL_URL = 'https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260929_212926_92423081-b0e4-4f5a-b650-14af6c05c058.glb';
如果模型加載失敗,則回退到用 new RoundedBoxGeometry(1, 1, 1, 8, 0.12) 創建的圓角立方體。
頁面 <title> 標題爲:"Design World — Explore New Ideas"。
==================================================
2. 頁面結構(HTML 標記)
==================================================
<section class="hero" id="hero">
<canvas id="scene" aria-label="Rotatable glass cube. Drag to rotate."></canvas>
<div class="ui">
<h1 class="sr-only">Explore New Ideas</h1>
<header class="nav">
<a href="#" class="logo"><i class="logo-mark"></i><b>Design</b><span>World</span></a>
<ul class="nav-links"><li><a>Home</a></li><li><a>Portfolio</a></li><li><a>Contact Us</a></li></ul>
</header>
<div class="arrows">
<button class="arrow" id="prev">[left-arrow SVG]</button>
<button class="arrow" id="next">[right-arrow SVG]</button>
</div>
<nav class="dots"><button class="dot"></button><button class="dot active"></button><button class="dot"></button></nav>
<p class="tagline">Let's Build the<br>Future of <strong>Design.</strong></p>
<div class="cta-row">
<a href="#" class="cta">Explore Now</a>
<span class="cta-line"></span>
<span class="count" aria-hidden="true">07</span>
</div>
<div class="loader" id="loader">Loading model</div>
</div>
</section>
箭頭 SVG:viewBox 爲 0 0 24 24(視口範圍),fill 爲 none(不填充),stroke 爲 currentColor(跟隨當前文字顏色),stroke-width 爲 2.6(描邊寬度),線端與連接處都設爲 round(圓頭)。
prev(上一個)按鈕的路徑爲:"M19 12H5M11 6l-6 6 6 6";next(下一個)按鈕的路徑爲:"M5 12h14M13 6l6 6-6 6"。
重要:大的 "Explore / New / Ideas" 標題不是 HTML 文本,而是被繪製到 WebGL 內的一個 2D 畫布上,這樣玻璃立方體才能折射它(見第 4 節)。HTML 中只有一個 <h1> 元素,並且出於可訪問性考慮被視覺隱藏,頁面上不會出現可見的標題文字。
==================================================
3. CSS 樣式(精確數值)
==================================================
:root { --bg:#000; --fg:#fff; --pad-x: clamp(20px, 6.95vw, 120px) }
html, body { height:100%; background:#000; color:#fff; font-family:'Poppins',sans-serif; -webkit-font-smoothing:antialiased }
.hero { position:relative; width:100%; height:100vh; height:100svh; min-height:520px; overflow:hidden; background:#000 }
#scene { position:absolute; inset:0; width:100%; height:100%; display:block; cursor:grab; touch-action:none }
#scene.dragging { cursor:grabbing }
.ui { position:absolute; inset:0; pointer-events:none; z-index:2 } /* 拖拽事件會穿透到下層畫布 */
.ui a, .ui button { pointer-events:auto }
.sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap }
頂部導航欄
.nav { position:absolute; top:clamp(24px,4.7vh,40px); left:var(--pad-x); right:var(--pad-x); display:flex; align-items:center; justify-content:space-between }
.logo { display:flex; align-items:center; gap:8px; color:#fff; text-decoration:none; font-size:15px; letter-spacing:-0.01em }
.logo-mark { width:20px; height:40px; background:#fff; border-radius:0 20px 20px 0 } /* 白色大寫字母 "D" 的半圓造型 */
.logo b { font-weight:700 } .logo span { font-weight:400 }
.nav-links { display:flex; gap:clamp(20px,4.1vw,60px); list-style:none }
.nav-links a { color:#fff; text-decoration:none; font-size:15px; font-weight:500; position:relative }
.nav-links a::after { content:''; position:absolute; left:0; right:0; bottom:-4px; height:1px; background:currentColor; transform:scaleX(0); transform-origin:right; transition:transform .35s ease }
.nav-links a:hover::after { transform:scaleX(1); transform-origin:left }
箭頭按鈕(右上角,位於圓點列的左側)
.arrows { position:absolute; top:18.9%; left:76.7%; display:flex; gap:32px; transform:translate(-19px,-50%) }
.arrow { width:38px; height:38px; border-radius:50%; border:2.5px solid #fff; background:transparent; color:#fff; display:grid; place-items:center; cursor:pointer; transition:background .25s, color .25s }
.arrow svg { width:18px; height:18px }
.arrow:hover { background:#fff; color:#000 }
分頁圓點(豎直排列,位於頁面右邊緣)
.dots { position:absolute; right:calc(var(--pad-x) - 7px); top:49.4%; transform:translateY(-50%); display:flex; flex-direction:column; gap:33px }
.dot { width:14px; height:14px; border-radius:50%; border:2px solid #fff; background:#fff; cursor:pointer; transition:background .25s }
.dot.active { background:transparent } /* active 表示空心圓環,默認中間那個爲激活態 */
標語文字(左下角)
.tagline { position:absolute; left:var(--pad-x); bottom:clamp(40px,7.5vh,70px); font-size:clamp(24px,2.65vw,44px); line-height:1.2; font-weight:300; letter-spacing:-0.01em }
.tagline strong { font-weight:700; display:block }
CTA(行動號召)行(按鈕 → 水平線 → 巨大的描邊 "07",從右邊緣溢出)
.cta-row { position:absolute; left:45.6%; right:-1vw; top:88.7%; transform:translateY(-50%); display:flex; align-items:center }
.cta { flex:none; padding:0 15px; height:48px; display:inline-flex; align-items:center; border:1.5px solid rgba(255,255,255,.85); border-radius:6px; background:rgba(0,0,0,.15); color:#fff; font-size:14px; font-weight:400; text-decoration:none; cursor:pointer; transition:background .25s, color .25s }
.cta:hover { background:#fff; color:#000 }
.cta-line { flex:1; height:1.5px; background:rgba(255,255,255,.8); min-width:40px }
.count { flex:none; font-size:clamp(140px,19.8vw,360px); font-weight:400; line-height:1; letter-spacing:-0.02em; color:transparent; -webkit-text-stroke:1.5px rgba(255,255,255,.9); transform:translateY(6%); user-select:none }
模型加載提示
.loader { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); font-size:12px; letter-spacing:.2em; text-transform:uppercase; opacity:.6; transition:opacity .6s }
.loader.done { opacity:0 }
響應式佈局規則
@media (max-width:900px), (max-aspect-ratio:1/1) {
.arrows { left:auto; right:var(--pad-x); top:15%; transform:translateY(-50%); gap:14px }
.tagline { bottom:clamp(150px,20vh,220px) }
.cta-row { left:var(--pad-x); right:-3vw; top:auto; bottom:24px; transform:none }
.count { font-size:clamp(120px,22vw,200px) }
}
@media (max-width:640px) {
.nav-links { gap:16px }
.nav-links a, .logo { font-size:13px }
.dots { gap:20px; right:16px }
.dot { width:10px; height:10px }
.cta-row { right:-8vw }
.count { font-size:120px }
}
@media (max-width:420px) { .nav-links li:nth-child(2) { display:none } } /* 隱藏 "Portfolio" 導航項 */
在任何屏幕寬度下,都不得出現頁面的水平滾動條。
==================================================
4. WebGL 三維場景搭建(Three.js)
==================================================
渲染器:使用 WebGLRenderer({ canvas:#scene, antialias:true, alpha:false })(WebGL 渲染器);其中 canvas 指定 #scene 畫布,antialias 開啓抗鋸齒,alpha:false 關閉透明通道。clearColor 爲 #000,即把清屏色設爲純黑;outputColorSpace 爲 SRGBColorSpace,即輸出使用 sRGB 色彩空間;pixelRatio = min(devicePixelRatio, 2),即像素比取設備像素比與 2 中的較小值;渲染器使用 #scene 作爲繪製畫布,並按要求設置清屏色與輸出色彩空間。
相機:使用 PerspectiveCamera(fov 30, aspect W/H, 0.1, 100)(透視相機);其中 fov 爲 30,aspect 取 W/H,0.1 與 100 分別是近裁剪面和遠裁剪面。相機位於 (0,0,10),朝向原點,採用透視投影。
4a. 背景大標題(繪製到離屏的 2D 畫布 → CanvasTexture 畫布紋理)
- 畫布尺寸 = 視口 × DPR(設備像素比),填充爲 #000。
- mobile = (W < 768) || (W/H < 1).
- fontSize fs = min(H*0.21, W*(mobile ? 0.21 : 0.118)); font = `800 ${fs}px Poppins`.
如果最寬的一行寬度大於 W*(mobile ? 0.9 : 0.5),就把 fs(字號)縮小以適配。
- 三行文字分別爲:"Explore"、"New"、"Ideas"。填充色爲 #e9e9e9,textAlign 設爲 center(居中),基線爲 alphabetic(字母基線)。
- 中心 x 座標 = W*(mobile ? 0.5 : 0.505);中心 y 座標 = H*(mobile ? 0.45 : 0.468)。
行間距 = fs*1.07;大寫字母高度 ≈ fs*0.7。第 i 行(i 取 0..2)的基線 y 座標 = cy + cap/2 + (i-1)*gap(cap 爲大寫字母高度,gap 爲行間距)。
- 紋理:colorSpace 設爲 SRGB,過濾方式爲 LinearFilter,不使用 mipmap。在窗口尺寸變化時以及 document.fonts 觸發 'loadingdone'(字體加載完成)事件時重新繪製。
用一個全屏四邊形來顯示這張紋理:幾何體用 PlaneGeometry(2,2)(2×2 的平面),材質用 ShaderMaterial(着色器材質);頂點着色器把 position.xy(頂點位置)直接作爲輸出位置,片元着色器用 texture2D(uTex, vUv)(按紋理座標採樣)取色,並且必須在它後面的單獨一行接上 #include <colorspace_fragment>(色彩空間轉換);depthTest(深度測試)與 depthWrite(深度寫入)都設爲 false;frustumCulled(視錐剔除)設爲 false;把這個四邊形放在它自己的 bgScene(背景場景)中,使它只負責顯示這張背景紋理。
首次繪製前先等待字體就緒:Promise.race([Promise.all([document.fonts.load('800 100px Poppins'), document.fonts.ready]), 2500ms 超時]);也就是等待 Poppins 的 800 字重加載完成、並等待 document.fonts.ready,最長等待 2500ms;就緒後執行 layout(),然後啓動渲染循環。
4b. 模型加載與幾何體合併處理
調用 GLTFLoader.load(MODEL_URL) 加載模型。遍歷 gltf.scene;對每個 mesh(網格):克隆它的 geometry(幾何體),刪除 'uv'、'color'、'tangent' 三個屬性,執行 mergeVertices(g, 1e-4)(以 1e-4 的容差合併重複頂點)、computeVertexNormals()(重新計算頂點法線)、applyMatrix4(mesh.matrixWorld)(把網格的世界矩陣應用到幾何體上)。最後用 mergeGeometries 把所有部件合併成一個幾何體,作爲立方體網格的唯一幾何體使用。
- 將幾何體按包圍盒居中,並等比縮放,使其最大尺寸 = 1。
- 場景圖層級:scene(場景)→ pivot(樞軸,負責屏幕位置 + 縮放)→ spinner(旋轉節點,負責旋轉)→ cube mesh(立方體網格)。加載完成後給 #loader 添加 "done" 類。
4c. 立方體的放置(在 layout() 中執行,加載時和窗口尺寸變化時各調用一次)
- visH = 2*tan(fov/2)*10; visW = visH*aspect.
- 屏幕比例中心:sx = mobile ? 0.5 : 0.517(橫向中心比例),sy = mobile ? 0.45 : 0.488(縱向中心比例),sx 與 sy 均爲 0..1 的屏幕比例。
pivot.position = ((sx-0.5)*visW, (0.5-sy)*visH, 0).
- 屏幕上顯示的邊長 px = min(H*0.44, W*(mobile ? 0.45 : 0.29));pivot.scale = (px/H)*visH(px 爲屏幕上顯示的像素邊長,visH 爲可見高度)。
- spinner 的初始旋轉:Euler(-0.42, 0.62, 0.18),即繞 X/Y/Z 軸的歐拉角。
4d. 玻璃材質 —— 使用自定義 ShaderMaterial(着色器材質),實現屏幕空間折射,並帶有 6 個波段的色散;整個材質分兩遍渲染(先渲染背面,再渲染正面)。
頂點着色器的寫法如下:
varying vec3 vNormal; varying vec3 vEye;
worldPos = modelMatrix*position; mvPos = viewMatrix*worldPos; gl_Position = projectionMatrix*mvPos;
vNormal = normalize(normalMatrix*normal); vEye = normalize(https://mvpos.xyz/);
片元着色器的寫法如下:
uv = gl_FragCoord.xy / uResolution(gl_FragCoord 爲片元座標,uResolution 爲繪圖緩衝區的尺寸)。
n = normalize(vNormal); if (uBackside > 0.5) n = -n; // 不要使用 gl_FrontFacing,因爲 three.js 會爲 BackSide 翻轉三角形繞序,使法線方向相反
eye = normalize(vEye).
循環 LOOP = 16 次迭代;slide = i/LOOP * 0.045(i 爲當前迭代序號)。
對每個波段,用 iorR/Y/G/C/B/P 計算折射向量 refract(eye, n, 1.0/ior),並在以下 uv 位置採樣 uTexture(ior 爲該波段的折射率):
uv + refr.xy * (uRefractPower + slide*k) * uChromatic,其中 k 的取值爲 1 (R)、1 (Y)、2 (G)、2.5 (C)、3 (B)、1 (P)(refr 爲折射向量,k 爲各波段的係數):
r = tex(R).x*0.5
y = (tex(Y).x*2 + tex(Y).y*2 - tex(Y).z)/6
g = tex(G).y*0.5
c = (tex(C).y*2 + tex(C).z*2 - tex(C).x)/6
b = tex(B).z*0.5
p = (tex(P).z*2 + tex(P).x*2 - tex(P).y)/6
R = r + (2p + 2y - c)/3; G = g + (2y + 2c - p)/3; B = b + (2c + 2p - y)/3
color += vec3(R,G,B)
color /= LOOP;飽和度調整:mix(luma(color), color, uSaturation),其中亮度 luma 的權重爲 (0.2125, 0.7154, 0.0721)。
高光(Blinn-Phong 模型,光照向量 = normalize(-light),視線方向 = -eye,halfVec 爲半程向量):
spec = specular(uLight, uShininess, uDiffuseness) + 0.6 * specular(vec3(1,1,-1), uShininess*0.6, uDiffuseness*0.5)
spec(x) = pow(max(dot(n,halfVec),0), shininess) + max(0,dot(n,lightVec))*diffuseness
color += spec * (backside ? 0.35 : 1.0)
菲涅爾效應:f = pow(1.0 + dot(eye, n), uFresnelPower);color = mix(color, vec3(1), f * (backside ? 0.25 : 0.55))(backside 表示當前是否爲背面)。
color += vec3(0.004, 0.005, 0.007)。gl_FragColor = vec4(color,1);然後在單獨一行中寫入 #include <colorspace_fragment>。
Uniform(統一變量,即從着色器外部傳入的參數)取值如下:
uIorR 1.15, uIorY 1.16, uIorG 1.18, uIorC 1.22, uIorB 1.22, uIorP 1.22,
uRefractPower 0.30(用於正面)/ 0.22(用於背面),uChromatic 0.5,uSaturation 1.08,
uShininess 90, uDiffuseness 0.02, uFresnelPower 5.0, uLight (-1, 1, 1),
uBackside 0(用於 frontMat,side 爲 FrontSide)/ 1(用於 backMat,side 爲 BackSide)。
4e. 渲染管線(每一幀執行一次)
創建兩個 WebGLRenderTarget(渲染目標),尺寸與繪圖緩衝區一致(W*DPR × H*DPR),類型爲 HalfFloatType(半浮點類型),使用默認的線性色彩空間:分別命名爲 rtBack、rtFront,用於後續的兩遍渲染。
- backMat.uTexture = rtBack.texture; frontMat.uTexture = rtFront.texture.
1) 將 bgScene(背景場景)渲染到 rtBack
2) 將 bgScene 渲染到 rtFront,然後用 backMat 渲染立方體(autoClear=false)到 rtFront
3) 將 bgScene 渲染到屏幕,然後用 frontMat 渲染立方體(autoClear=false,clearDepth)到屏幕
- 這樣就得到該效果:透過立方體看到的文字會被放大、鏡像,並分裂成藍色/黃色色邊;立方體內側的圓角邊緣隱約可見,倒角邊緣則會發白光,整體呈現玻璃折射的觀感。
==================================================
5. 交互與動畫
==================================================
- 拖拽旋轉(在畫布上監聽 pointer 指針事件,並調用 setPointerCapture 捕獲指針):
dx = Δx*0.008,dy = Δy*0.008;作爲世界空間旋轉應用:先用 axisAngle(Y, dx)、再用 axisAngle(X, dy) 左乘 spinner.quaternion,從而在屏幕拖動方向上旋轉立方體(Δx、Δy 爲指針位移量)。
按每 16.67ms 一幀跟蹤速度;拖拽期間給畫布添加 "dragging" 類,以切換光標樣式。
- 釋放後的慣性:持續施加當前速度,每幀按 0.94^(dt*60) 衰減,直到速度趨近於零。
- 空閒漂移:釋放 0.6s 後,混入恆定的緩慢旋轉,繞 Y 軸 0.0035 rad/幀、繞 X 軸 0.0012 rad/幀(混合係數在 1s 內從 0 漸變到 1)。從頁面加載起它也會運行。
- Prev/Next(上一個/下一個)箭頭按鈕:繞 Y 軸將立方體平滑旋轉 -90°/+90°(緩動:每幀應用 remaining*min(1, 0.09*dt*60),直到 remaining < 0.0005)。該操作會取消慣性;拖拽則會取消該旋轉。
- 圓點:點擊某個圓點後,它成爲唯一帶 .active 類的圓點(空心圓環)。
- 懸停過渡:導航鏈接的下劃線掃過(.35s),箭頭和 CTA 按鈕反轉爲白色背景 / 黑色內容(.25s)。
- 將幀 dt 鉗制到 0.05s。在窗口尺寸變化(resize)時重新計算佈局(渲染器尺寸、相機寬高比、渲染目標尺寸、uResolution、標題畫布、立方體位置/縮放)。
==================================================
6. 佈局參考(桌面端 1440×806)
==================================================
- Logo(標識)位於頁面座標 (100, 38) 處,高 40px。導航鏈接右對齊,右邊緣 ≈ 1340,垂直中心 ≈ 58。
- 箭頭圓圈的中心位於頁面座標 ≈ (1105, 152) 和 (1175, 152) 處。
- 圓點位於 x ≈ 1340,y ≈ 351 / 398 / 445 處(中間一個爲空心)。
- 標題 "Explore / New / Ideas" 以 x ≈ 727 爲中心,約 170px 的 ExtraBold(特粗體),基線分別位於 ≈ 272 / 445 / 637 處。
- 玻璃立方體佔據 ≈ x 510–1000、y 145–600 的範圍,略偏中心右側,與標題重疊。
- 標語 "Let's Build the / Future of / Design." 位於左側 100、底部 ≈ 60px 處,約 38px,Light(細體)字重,其中 "Design." 爲 Bold(粗體)。
- "Explore Now" 按鈕位於 (657, 691) 處,高 48px;一條 1.5px 的線向右延伸到巨大的描邊 "07",其 "0" 從 x ≈ 1140 開始,其 "7" 被右邊緣和下邊緣裁切。
- 純黑背景,所有 UI 元素均爲白色。
移動端(≤767px 或豎屏):標題和立方體以 x 50% / y 45% 爲中心,標題最寬可達 90% 寬度;箭頭移到導航下方、位於右側;標語位於 CTA 行上方;CTA 行固定在距底部 24px 處,"07" 有一部分位於右側屏幕之外。
輸出:一個完整的 index.html,不要包含其他內容。