소개 페이지를 스킬 전체 경로로 다시 만들고, 배운 것을 스킬에 반영
방향을 글래스로 바꿀 때 2단계 재결정을 하고도 전체 경로로 올리지 않았다.
레퍼런스 없이 재질만 갈아끼운 결과가 스위스 미니멀 뼈대에 유리를 바른
페이지였고, design.md 는 초판 결정을 그대로 담고 있었다. 0단계부터 다시 돌았다.
1단계 — 레퍼런스 3개를 computed style 까지 실측
R1 zed.dev 컨테이너 1120px 과 전체폭이 교대 · radius 2px 621회
R2 lusion.co 3D 는 액자 안, 텍스트는 액자 밖 · 배경 흰색 · h1 36px
R3 Codrops DOM 이 구조를 소유하고 WebGL 은 uniform 만 만진다
2단계 — 프리셋 swiss-minimal → dark-instrument, 컨셉 "검사대 위의 물체"
유리는 액자에만 남긴다(헤더 · 검사대 · 명령창)
4단계 — 히어로 재구성. 캔버스를 배경에서 빼 액자 안으로, 텍스트는 밖으로
판독기가 통과한 게이트를 표시한다(값이 바뀔 때만 DOM 을 쓴다)
파이프라인을 2열로 열어 625px → 1113px
5단계에서 실제로 걸린 것
게이트 3 radius 4종 → 2종 (3px / 14px, pill 제거)
게이트 1 base.css 토큰 밖 rgb() 7곳 → 토큰
게이트 7 헤더 English 2.59:1 → ink 로 상향
카운트 히어로 h1 4줄 → 2줄
셰이더 판이 정면이라 프레넬이 구조적으로 0. 법선을 굽히고 UV 로 테두리
셰이더 gl_FragCoord 를 렌더 타겟 크기로 나눠 UV 가 1 을 넘고 있었다
수치 페이지가 실은 "타입 단계 5단계"가 실측 9종이었다. lang 속성이
스케일을 재정의해 링크 하나가 16px 로 새고 있었다
측정 대비 스크립트가 반투명 배경을 불투명으로 계산했다. 알파 합성으로 교정
스킬 반영
three.md §0-A 배치 원리(3D 는 액자 안) · 프레넬 0 함정 · gl_FragCoord 함정
preflight.md 대비 측정 시 알파 합성. 검사 도구가 틀리면 통과도 의미가 없다
최종: 하드 게이트 12/12, 대비 실패 0, 320px 오버플로 0, 테스트 22개 통과
This commit is contained in:
parent
eb3ca69b5d
commit
9648d1d82a
15 changed files with 576 additions and 306 deletions
|
|
@ -32,6 +32,12 @@ export interface HeroSceneOptions {
|
|||
/** CSS 커스텀 프로퍼티 이름 */
|
||||
accentVar: string;
|
||||
glowVar: string;
|
||||
/**
|
||||
* 통과한 게이트가 바뀔 때만 부른다. 프레임마다 부르지 않는다 —
|
||||
* R3(Codrops HAOQI) 에서 가져온 규칙이다. DOM 은 구조를 소유하고
|
||||
* WebGL 은 셰이더만 만진다. 매 프레임 DOM 을 쓰면 그 경계가 무너진다.
|
||||
*/
|
||||
onGate?: (index: number, total: number) => void;
|
||||
}
|
||||
|
||||
const NOISE = `
|
||||
|
|
@ -64,7 +70,7 @@ export function mountHeroScene(opts: HeroSceneOptions): void {
|
|||
io.observe(canvas);
|
||||
}
|
||||
|
||||
async function boot({ canvas, accentVar, glowVar }: HeroSceneOptions): Promise<void> {
|
||||
async function boot({ canvas, accentVar, glowVar, onGate }: HeroSceneOptions): Promise<void> {
|
||||
let THREE: typeof import("three");
|
||||
try {
|
||||
THREE = await import("three");
|
||||
|
|
@ -161,7 +167,10 @@ async function boot({ canvas, accentVar, glowVar }: HeroSceneOptions): Promise<v
|
|||
uniforms: {
|
||||
uScene: { value: rt.texture },
|
||||
uTime: { value: 0 },
|
||||
uEdge: { value: new THREE.Color("#ffffff") },
|
||||
// 테두리 광. 흰색을 박지 않고 강조색을 밝게 올려 쓴다 —
|
||||
// 토큰을 바꾸면 유리 테두리도 같이 따라와야 한다.
|
||||
uEdge: { value: accent.clone().lerp(new THREE.Color(1, 1, 1), 0.55) },
|
||||
uResolution: { value: new THREE.Vector2(1, 1) },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
|
@ -183,22 +192,31 @@ async function boot({ canvas, accentVar, glowVar }: HeroSceneOptions): Promise<v
|
|||
uniform sampler2D uScene;
|
||||
uniform float uTime;
|
||||
uniform vec3 uEdge;
|
||||
uniform vec2 uResolution;
|
||||
${NOISE}
|
||||
|
||||
void main() {
|
||||
// 판을 미세하게 굽힌다.
|
||||
// 완전히 평평한 판이 카메라를 정면으로 보면 dot(n, view) 가 1 이라
|
||||
// 프레넬이 구조적으로 0 이 된다. 그러면 유리로 읽히게 하는 신호가
|
||||
// 하나도 남지 않는다 — 이전 판이 흐린 얼룩으로 보였던 이유다.
|
||||
vec3 n = normalize(vNormalW + vec3((vUv - 0.5) * 0.9, 0.0));
|
||||
float fres = pow(1.0 - abs(dot(n, normalize(vViewDir))), 2.2);
|
||||
|
||||
// 판의 테두리. 평면에서 두께가 보이는 곳은 법선이 아니라 UV 경계다.
|
||||
vec2 e = min(vUv, 1.0 - vUv);
|
||||
float edge = 1.0 - smoothstep(0.0, 0.05, min(e.x, e.y));
|
||||
|
||||
// 화면 좌표를 굴절시켜 뒤 텍스처를 샘플링한다
|
||||
vec2 screen = gl_FragCoord.xy / vec2(textureSize(uScene, 0));
|
||||
vec2 screen = gl_FragCoord.xy / uResolution;
|
||||
vec2 flow = vec2(
|
||||
fbm(vUv * 2.4 + uTime * 0.05),
|
||||
fbm(vUv * 2.4 - uTime * 0.04 + 3.1)
|
||||
) - 0.5;
|
||||
vec3 back = texture2D(uScene, screen + flow * 0.07 + vNormalW.xy * 0.035).rgb;
|
||||
vec3 back = texture2D(uScene, screen + flow * 0.06 + n.xy * 0.05).rgb;
|
||||
|
||||
// 가장자리에서 빛이 서는 프레넬
|
||||
float fres = pow(1.0 - abs(dot(normalize(vNormalW), normalize(vViewDir))), 2.6);
|
||||
|
||||
vec3 col = back * 1.06 + uEdge * fres * 0.5;
|
||||
float alpha = 0.20 + fres * 0.55;
|
||||
vec3 col = back * 1.04 + uEdge * (fres * 0.32 + edge * 0.85);
|
||||
float alpha = 0.14 + fres * 0.26 + edge * 0.72;
|
||||
gl_FragColor = vec4(col, alpha);
|
||||
}
|
||||
`,
|
||||
|
|
@ -229,6 +247,9 @@ async function boot({ canvas, accentVar, glowVar }: HeroSceneOptions): Promise<v
|
|||
if (!r.width || !r.height) return;
|
||||
renderer.setSize(r.width, r.height, false);
|
||||
rt.setSize(Math.round(r.width * dpr * 0.6), Math.round(r.height * dpr * 0.6));
|
||||
// gl_FragCoord 는 캔버스의 드로잉버퍼 좌표다. 렌더 타겟은 그 0.6배라
|
||||
// 타겟 크기로 나누면 UV 가 1 을 넘어 텍스처가 잘린다.
|
||||
renderer.getDrawingBufferSize(paneMat.uniforms["uResolution"]!.value);
|
||||
camera.aspect = r.width / r.height;
|
||||
camera.updateProjectionMatrix();
|
||||
bgUniforms.uAspect.value = r.width / r.height;
|
||||
|
|
@ -262,22 +283,24 @@ async function boot({ canvas, accentVar, glowVar }: HeroSceneOptions): Promise<v
|
|||
|
||||
pointer.lerp(pointerTarget, 0.04);
|
||||
|
||||
const rect0 = canvas.getBoundingClientRect();
|
||||
const progress0 = Math.min(Math.max(-rect0.top / (rect0.height * 1.6), 0), 1);
|
||||
bgUniforms.uProgress.value = progress0;
|
||||
// 스크롤 진행도. addEventListener("scroll") 을 쓰지 않는다(하드 게이트 10).
|
||||
// rect 는 프레임당 한 번만 읽는다. 두 번 읽으면 레이아웃을 두 번 강제한다.
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
// 액자가 화면 위로 92% 지나갔을 때 마지막 게이트를 통과하게 잡았다.
|
||||
// 그 뒤로는 액자가 보이지 않으므로 계속 진행시킬 이유가 없다.
|
||||
const travel = rect.height * 0.92;
|
||||
const progress = Math.min(Math.max(-rect.top / travel, 0), 1);
|
||||
|
||||
bgUniforms.uProgress.value = progress;
|
||||
bgUniforms.uTime.value = t;
|
||||
bgUniforms.uPointer.value.copy(pointer);
|
||||
paneMat.uniforms["uTime"]!.value = t;
|
||||
|
||||
// 스크롤 진행도. addEventListener("scroll") 을 쓰지 않는다(하드 게이트 10).
|
||||
// 이미 도는 렌더 루프 안에서 위치를 한 번 읽을 뿐이라 추가 비용이 없다.
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const travel = rect.height * 1.6;
|
||||
const progress = Math.min(Math.max(-rect.top / travel, 0), 1);
|
||||
onGate?.(Math.min(PANE_COUNT - 1, Math.floor(progress * PANE_COUNT)), PANE_COUNT);
|
||||
|
||||
// 카메라가 여섯 개의 문을 차례로 통과한다
|
||||
const depth = (PANE_COUNT - 1) * GATE_GAP;
|
||||
camera.position.z = 8.5 - progress * (depth + 6);
|
||||
camera.position.z = 7.2 - progress * (depth + 5);
|
||||
camera.position.x = (pointer.x - 0.5) * 1.1;
|
||||
camera.position.y = (pointer.y - 0.5) * -0.5;
|
||||
camera.lookAt(camera.position.x * 0.3, 0, camera.position.z - 10);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue