테두리 광을 회전시키고 three.js 모바일 대응을 고친다
회전 테두리
conic 의 각도를 @property 로 돌리면 매 프레임 배경을 다시 칠한다 —
컴포지터에서 못 돌고 메인 스레드가 페인트한다(하드 게이트 8).
정사각형 원뿔 레이어를 마스크 안에서 rotate 시켰다. transform 만 움직인다.
aspect-ratio: 1 이라야 회전 중 밝기가 일정하고,
width: 200% 라야 1178x52 헤더에서 모서리까지 닿는다. 주기 11초.
three.js 모바일
세로 화면에서 게이트가 통째로 안 보이고 있었다.
수직 시야는 화면 비율과 무관하지만 수평 시야는 aspect 에 비례한다.
데스크톱(2.5:1)에서 정한 판 폭 15 가 세로 화면(0.45:1)의 수평 시야
2.5 에 비해 여섯 배라, 첫 판이 화면을 완전히 덮었다.
fit() 에서 camera.aspect 를 갱신할 때 판의 scale.x 도 같이 잡는다.
저사양 판정도 고쳤다. hardwareConcurrency <= 4 만 보면 요즘 폰이 전부
고사양으로 잡힌다(8코어가 흔하다). innerWidth < 760 을 함께 본다.
DPR 1.5 -> 1.25, 캔버스 불투명도 0.7 -> 0.45(세로는 화면 전체를 덮는다).
스킬
svg-filters.md 테두리 광 회전은 각도가 아니라 정사각형 레이어 rotate
three.md 지오메트리를 화면 비율에 맞춰 다시 잡아라
저사양 판정에 화면 폭을 넣어라
This commit is contained in:
parent
54ec6fa582
commit
8267905c0c
8 changed files with 171 additions and 3 deletions
|
|
@ -17,6 +17,7 @@ const { c } = Astro.props;
|
|||
<p class="lead mat-lead reveal">{c.lead}</p>
|
||||
|
||||
<div class="full stage glass par-slow">
|
||||
<span class="rim" aria-hidden="true"></span>
|
||||
<div class="stage-fallback" aria-hidden="true"></div>
|
||||
{/* GPU 가 만드는 재질. WebGL 이 없으면 위 그라디언트가 그대로 결과물이다 */}
|
||||
<canvas id="mat-canvas" class="stage-canvas" aria-hidden="true"></canvas>
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ const { c } = Astro.props;
|
|||
}
|
||||
.gate-canvas { opacity: 0; transition: opacity 1s var(--ease-soft); }
|
||||
.gate-canvas.is-live { opacity: 0.7; }
|
||||
/* 세로 화면에서는 캔버스가 화면 전체를 덮는다. 같은 밝기면 본문이 묻힌다 */
|
||||
@media (max-width: 760px) {
|
||||
.gate-canvas.is-live { opacity: 0.45; }
|
||||
}
|
||||
|
||||
/* 통과한 단계가 켜진다. 3D 와 텍스트를 잇는 유일한 연결이다 */
|
||||
.stop { transition: opacity var(--dur-quick) var(--ease-out); }
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ const FAVICON =
|
|||
|
||||
<header class="site-header">
|
||||
<div class="header-inner glass">
|
||||
<span class="rim" aria-hidden="true"></span>
|
||||
<a class="wordmark" href={lang === "ko" ? "/" : "/en/"} aria-label="designpaca">
|
||||
<svg class="mark" viewBox="0 0 32 32" width="20" height="20" aria-hidden="true">
|
||||
<path d="M11 7v18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
|
|
|
|||
|
|
@ -84,11 +84,15 @@ async function boot({ canvas, accentVar, glowVar, onGate, progressFrom }: GateSc
|
|||
const accent = color(accentVar, "#4FD6C4");
|
||||
const glow = color(glowVar, "#6E7BFF");
|
||||
|
||||
// 저사양이면 판을 줄인다. 코어 수는 거친 지표지만 라이브러리를 더 받지 않는다
|
||||
const lean = (navigator.hardwareConcurrency ?? 4) <= 4;
|
||||
// 저사양 판정.
|
||||
// 코어 수만 보면 요즘 폰(8코어)이 전부 고사양으로 잡힌다. 화면 폭을 같이 본다 —
|
||||
// 좁은 화면은 대개 배터리로 돌아가고, 같은 씬이라도 열이 더 난다.
|
||||
const narrow = innerWidth < 760;
|
||||
const lean = narrow || (navigator.hardwareConcurrency ?? 4) <= 4;
|
||||
const PANE_COUNT = lean ? 3 : 6;
|
||||
|
||||
const dpr = Math.min(devicePixelRatio, 1.5);
|
||||
// 좁은 화면에서는 DPR 도 한 단계 더 내린다. 폰의 DPR 은 3 까지 간다.
|
||||
const dpr = Math.min(devicePixelRatio, narrow ? 1.25 : 1.5);
|
||||
renderer.setPixelRatio(dpr);
|
||||
|
||||
/* ---- 1) 배경을 렌더 타겟에 그린다 ------------------------- */
|
||||
|
|
@ -251,6 +255,20 @@ async function boot({ canvas, accentVar, glowVar, onGate, progressFrom }: GateSc
|
|||
const pointer = new THREE.Vector2(0.5, 0.5);
|
||||
const pointerTarget = new THREE.Vector2(0.5, 0.5);
|
||||
|
||||
// 판이 화면을 덮는 데 필요한 최소 폭.
|
||||
// 수직 시야는 화면 비율과 무관하지만 수평 시야는 aspect 에 비례한다.
|
||||
// 데스크톱(2.5:1)에 맞춘 폭 15 를 세로 화면(0.45:1)에 그대로 쓰면
|
||||
// 수평 시야의 여섯 배가 되어 첫 판이 화면을 완전히 덮는다 —
|
||||
// 게이트가 보이지 않고 그냥 밝은 화면이 된다.
|
||||
const GEO_W = 15;
|
||||
const fitPanes = (aspect: number) => {
|
||||
const halfH = Math.tan((camera.fov * Math.PI) / 360);
|
||||
const viewW = 2 * 7.2 * halfH * aspect;
|
||||
const want = Math.max(6.5, viewW * 1.12);
|
||||
const sx = Math.min(1, want / GEO_W);
|
||||
for (const m of panes) m.scale.x = sx;
|
||||
};
|
||||
|
||||
const fit = () => {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return;
|
||||
|
|
@ -262,6 +280,7 @@ async function boot({ canvas, accentVar, glowVar, onGate, progressFrom }: GateSc
|
|||
camera.aspect = r.width / r.height;
|
||||
camera.updateProjectionMatrix();
|
||||
bgUniforms.uAspect.value = r.width / r.height;
|
||||
fitPanes(camera.aspect);
|
||||
};
|
||||
fit();
|
||||
// window resize 만 듣던 것을 ResizeObserver 로 바꾼다.
|
||||
|
|
|
|||
|
|
@ -247,6 +247,54 @@ a:hover { color: var(--accent); }
|
|||
(실측: computed backdrop-filter 가 none 이 된다).
|
||||
굴절은 재료 섹션의 SVG 타일이 진짜로 보여준다. 여기서는 두께를 만든다. */
|
||||
|
||||
/* ---- 테두리를 도는 빛 --------------------------------------
|
||||
`conic-gradient` 의 각도를 `@property` 로 애니메이션하면 매 프레임 배경을
|
||||
다시 칠한다 — 컴포지터에서 못 돌고 메인 스레드가 페인트한다(하드 게이트 #8).
|
||||
|
||||
대신 **정사각형 원뿔 레이어를 마스크 안에서 회전**시킨다. 움직이는 것은
|
||||
`rotate` 뿐이라 GPU 에서 돈다. 정사각형이라 회전해도 밝기가 균일하고,
|
||||
요소보다 크게(200%) 잡아 납작한 헤더에서도 모서리까지 닿는다. */
|
||||
.rim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
padding: 1px;
|
||||
-webkit-mask:
|
||||
linear-gradient(black 0 0) content-box,
|
||||
linear-gradient(black 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask:
|
||||
linear-gradient(black 0 0) content-box,
|
||||
linear-gradient(black 0 0);
|
||||
mask-composite: exclude;
|
||||
}
|
||||
.rim::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 200%;
|
||||
aspect-ratio: 1;
|
||||
translate: -50% -50%;
|
||||
background: conic-gradient(
|
||||
from 0deg,
|
||||
transparent 0 58%,
|
||||
color-mix(in oklab, var(--accent) 70%, white) 74%,
|
||||
transparent 86%
|
||||
);
|
||||
opacity: 0.55;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.rim::before {
|
||||
animation: rim-spin 11s linear infinite;
|
||||
}
|
||||
}
|
||||
@keyframes rim-spin {
|
||||
to { rotate: 360deg; }
|
||||
}
|
||||
|
||||
/* 가장자리를 도는 빛. 유리판 테두리의 굴절을 흉내낸다 */
|
||||
.glass::after {
|
||||
content: "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue