ai21.net
언어

액체 유리 웹페이지 히어로

액체 유리 웹페이지 히어로 — 1

프롬프트

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, 로컬 에셋 없음, 빌드 단계 없음)을 만들어 전체 화면 히어로 섹션 하나를 포함하세요: 중앙에 회전 가능한 3D 유리 직육면체가 있고, 그것이 거대한 배경 헤드라인을 색 분산과 함께 굴절시키는 검은색 "Design World" 랜딩 히어로입니다. 아래 사양대로 정확히 재현하세요.

==================================================
1. 외부 리소스 (다음 URL을 정확히 사용)
==================================================
- 폰트: Google Fonts Poppins, 굵기 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">
- 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, 둥근 캡/조인.
prev path: "M19 12H5M11 6l-6 6 6 6" next path: "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 = 속이 빈 링, 기본적으로 가운데 것이 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 }), clearColor #000, outputColorSpace SRGBColorSpace, pixelRatio = min(devicePixelRatio, 2).
카메라: PerspectiveCamera(fov 30, aspect W/H, 0.1, 100)를 (0,0,10)에 두고 원점을 바라봅니다.

4a. 배경 헤드라인 (오프스크린 2D 캔버스에 그린 뒤 → CanvasTexture)
- Canvas 크기 = 뷰포트 × 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, baseline alphabetic.
- 중심 x = W*(mobile ? 0.5 : 0.505); 중심 y = H*(mobile ? 0.45 : 0.468).
줄 간격 = fs*1.07; 캡 높이 ≈ fs*0.7. 줄 i (0..2)의 베이스라인 y = cy + cap/2 + (i-1)*gap.
- 텍스처: colorSpace SRGB, LinearFilter, 밉맵 없음. 리사이즈 시와 document.fonts 'loadingdone' 시에 다시 그립니다.
- 자체 bgScene 안에서 전체 화면 쿼드(PlaneGeometry(2,2), ShaderMaterial, 버텍스는 position.xy를 그대로 출력, 프래그먼트는 texture2D(uTex, vUv) 다음에 #include <colorspace_fragment>를 별도 줄로; depthTest/depthWrite false; frustumCulled false)로 표시합니다.
- 첫 그리기 전에 폰트를 기다립니다: Promise.race([Promise.all([document.fonts.load('800 100px Poppins'), document.fonts.ready]), 2500ms 타임아웃]) 후 layout() + 루프 시작.

4b. 모델 로딩
- GLTFLoader.load(MODEL_URL). gltf.scene을 순회하며 각 메시마다: 지오메트리를 복제하고 'uv','color','tangent' 속성을 삭제한 뒤 mergeVertices(g, 1e-4), computeVertexNormals(), applyMatrix4(mesh.matrixWorld)를 적용합니다. 모든 파트를 mergeGeometries로 병합합니다.
- 지오메트리를 바운딩 박스 중심에 맞추고 가장 큰 치수가 1이 되도록 균일하게 스케일합니다.
- 씬 그래프: scene → pivot (화면 위치 + 스케일) → spinner (회전) → 큐브 메시. 로드되면 #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.
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.
- spinner 초기 회전: Euler(-0.42, 0.62, 0.18).

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 (드로잉 버퍼 크기).
n = normalize(vNormal); if (uBackside > 0.5) n = -n; // gl_FrontFacing을 사용하지 마세요. three.js는 BackSide에서 와인딩을 뒤집습니다
eye = normalize(vEye).
LOOP = 16회 반복; slide = i/LOOP * 0.045.
각 밴드에 대해 iorR/Y/G/C/B/P로 refract(eye, n, 1.0/ior)를 계산하고 uTexture를 다음 위치에서 샘플링합니다
uv + refr.xy * (uRefractPower + slide*k) * uChromatic, 여기서 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; 채도 조정: mix(luma(color), color, uSaturation), luma 가중치 (0.2125, 0.7154, 0.0721).
스페큘러 (Blinn-Phong, 광원 벡터 = normalize(-light), 뷰 = -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)
프레넬: 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); 그다음 #include <colorspace_fragment>를 별도 줄에 둡니다.
유니폼 값:
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. 렌더 파이프라인 (매 프레임)
- 드로잉 버퍼 크기(W*DPR × H*DPR)에 맞춘 두 개의 WebGLRenderTarget, type HalfFloatType, 기본(선형) 색 공간: rtBack, rtFront.
- backMat.uTexture = rtBack.texture; frontMat.uTexture = rtFront.texture.
1) bgScene을 rtBack에 렌더링
2) bgScene을 rtFront에 렌더링한 다음 (autoClear=false) backMat을 사용한 큐브를 rtFront에 렌더링
3) bgScene을 화면에 렌더링한 다음 (autoClear=false, clearDepth) frontMat을 사용한 큐브를 화면에 렌더링
- 이렇게 하면 다음과 같은 룩이 나옵니다: 큐브를 통해 보이는 텍스트는 확대되고 반전되며 파란색/노란색 프린지로 갈라지고, 큐브의 안쪽 둥근 모서리가 희미하게 보이며, 베벨 처리된 모서리는 흰색으로 빛납니다.

==================================================
5. 인터랙션 & 애니메이션
==================================================
- 드래그로 회전 (캔버스의 포인터 이벤트, setPointerCapture):
dx = Δx*0.008, dy = Δy*0.008; 월드 공간 회전으로 적용: spinner.quaternion에 axisAngle(Y, dx)를 선곱한 다음 axisAngle(X, dy)를 선곱합니다.
16.67ms 프레임마다 속도를 추적하고, 드래그하는 동안 캔버스에 "dragging" 클래스를 추가합니다.
- 놓은 뒤 관성: 속도를 계속 적용하며 매 프레임 0.94^(dt*60)로 감쇠합니다.
- 유휴 드리프트: 놓은 뒤 0.6초 후, Y축 기준 0.0035 rad/frame과 X축 기준 0.0012 rad/frame의 일정한 느린 회전을 블렌딩합니다(블렌드는 1초에 걸쳐 0→1로 증가). 페이지 로드 시점부터도 실행됩니다.
- Prev/Next 화살표 버튼: 큐브를 Y축 기준으로 -90°/+90° 부드럽게 회전시킵니다(이징: 매 프레임 remaining*min(1, 0.09*dt*60)을 적용하고 remaining < 0.0005가 될 때까지). 관성을 취소하며, 드래그하면 스핀이 취소됩니다.
- 점: 점을 클릭하면 그 점만 .active가 됩니다(속이 빈 링).
- 호버 전환: 내비 링크 밑줄 와이프(.35s), 화살표와 CTA 버튼은 흰색 배경 / 검은색 콘텐츠로 반전(.25s).
- 프레임 dt를 0.05s로 클램프합니다. window 리사이즈 시 레이아웃(렌더러 크기, 카메라 aspect, 렌더 타깃 크기, uResolution, 헤드라인 캔버스, 큐브 위치/스케일)을 다시 계산합니다.

==================================================
6. 레이아웃 참고 (데스크톱 1440×806)
==================================================
- 로고는 (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 글꼴에 Bold "Design.".
- "Explore Now" 버튼은 (657, 691), 높이 48px; 1.5px 선이 오른쪽으로 이어져 거대한 외곽선 "07"에 닿고, 그 "0"은 x ≈ 1140에서 시작하며 "7"은 오른쪽과 아래 가장자리에서 잘립니다.
- 순수 검은색 배경, 모든 UI는 흰색.

모바일(≤767px 또는 세로 모드): 헤드라인과 큐브는 50% x / 45% y에 중심을 두고, 헤드라인은 최대 90% 너비, 화살표는 오른쪽 내비 아래로 이동하며, 태그라인은 CTA 행 위에 놓이고, CTA 행은 하단에서 24px에 고정되며 "07"은 오른쪽 화면 밖으로 일부 나갑니다.

출력: 완전한 index.html 하나만, 그 외에는 없습니다.

출처

원작자
viktoroddy
게시일
2026-10-02(由推文 ID 反解)
원문 출처
원문 보기 ↗
수록 출처
zhuyansen/awesome-opus-5.5-video · 2106061619176620344 · 라이선스 미표명