배경을 뷰포트에 고정하고, 게이트 씬을 파이프라인으로 옮긴다
"스크롤할 때 이미지가 고정되어 원경처럼 보이는 게 패럴랙스의 꽃"이라는
지적이 맞았다. 느리게 같이 움직이는 것으로는 그 인상이 안 나온다 —
속도만 다를 뿐 방향이 같기 때문이다.
고정 배경
background-attachment: fixed 는 iOS Safari 가 GPU 메모리 때문에
throttle 해서 스크롤 중 배경이 튄다. 쓰지 않았다.
섹션에 clip-path: inset(0) 을 걸어 position: fixed 의 컨테이닝 블록으로
만들었다. 배경이 화면에 멈춘 채 그 섹션 안에서만 보인다.
실측: 스크롤 1000px 구간에서 viewportTop 이 계속 0.
대가 둘을 받아들였다
조상에 transform/filter/mask 를 못 쓴다 — 페이드 마스크를 포기했다
prefers-reduced-motion 에서는 absolute 로 되돌린다
전부 고정하지는 않았다. 종이는 손에 닿는 거리라 원경으로 두면 거짓말이다
재료·설치·프리플라이트 fixed 원경
사례 drift 두 결과물이 흐르는 자리
조판 drift 종이는 가깝다
게이트 씬을 파이프라인으로
히어로에서는 의미를 판독기가 자막으로 설명해야 했다. 그림이 스스로
말하지 못한다는 뜻이다. 옆에 여섯 단계가 적힌 자리로 옮기니 자막이
필요 없어졌다 — 판을 통과하면 그 단계가 켜진다.
캔버스는 sticky, 진행도는 섹션에서 읽는다.
travel 을 섹션 높이로 잡았더니 캔버스가 사라진 뒤에도 진행이 남아
6단계 중 3단계에서 멈췄다. (섹션 높이 - 뷰포트 높이)로 고쳤다.
캔버스 위 텍스트 대비 14.2:1.
스킬
motion.md 패럴랙스의 원형은 고정 · clip-path 기법과 그 대가
sticky 캔버스의 진행도 계산
three.md 씬의 의미는 옆의 콘텐츠가 반응할 때 성립한다.
자막이 필요하면 배치를 의심해라
This commit is contained in:
parent
1ed3c95542
commit
af3a08397e
12 changed files with 281 additions and 56 deletions
|
|
@ -14,7 +14,15 @@ const { c } = Astro.props;
|
|||
③ 의미 — <ol role="list"> — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다
|
||||
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
|
||||
*/}
|
||||
<section id="pipeline" class="section" aria-labelledby="pipeline-h">
|
||||
<section id="pipeline" class="section has-bg" aria-labelledby="pipeline-h">
|
||||
{/* 히어로에 있던 게이트 씬을 여기로 옮겼다.
|
||||
히어로에서는 그 은유를 판독기가 자막으로 설명해야 했다 — 그림이 스스로
|
||||
말하지 못한다는 뜻이다. 여기서는 옆에 실제 여섯 단계가 적혀 있고,
|
||||
판을 통과할 때 그 단계가 켜진다. 자막이 아니라 콘텐츠가 반응한다. */}
|
||||
<div class="gate-bg" aria-hidden="true">
|
||||
<div class="gate-fallback"></div>
|
||||
<canvas id="pipeline-canvas" class="gate-canvas"></canvas>
|
||||
</div>
|
||||
{/* 제목·리드·브리프·스파인을 한 컨테이너에 담는다.
|
||||
섹션 기본 그리드에 두면 제목은 main 칼럼의 음수 마진으로 x=371,
|
||||
리드는 x=435, 스파인은 x=143 에 서서 정렬선이 셋이 된다. */}
|
||||
|
|
@ -30,8 +38,8 @@ const { c } = Astro.props;
|
|||
|
||||
<ol role="list" class="spine">
|
||||
{
|
||||
c.stages.map((s) => (
|
||||
<li class="stop reveal">
|
||||
c.stages.map((s, i) => (
|
||||
<li class="stop reveal" data-stage={i}>
|
||||
<div class="stop-head">
|
||||
<h3 class="stop-name">{s.name}</h3>
|
||||
<p class="stop-does">{s.does}</p>
|
||||
|
|
@ -54,6 +62,44 @@ const { c } = Astro.props;
|
|||
.brief { max-width: var(--measure); }
|
||||
#pipeline-h { max-width: 24ch; }
|
||||
|
||||
/* ---- 게이트 배경 ------------------------------------------
|
||||
sticky 로 화면에 머물고 음수 마진으로 레이아웃 자리는 차지하지 않는다.
|
||||
섹션이 지나가는 동안 카메라가 판 여섯 장을 통과한다. */
|
||||
.gate-bg {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100dvh;
|
||||
margin-bottom: -100dvh;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
grid-column: full;
|
||||
}
|
||||
.gate-fallback,
|
||||
.gate-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.gate-fallback {
|
||||
background:
|
||||
radial-gradient(46% 54% at 28% 34%,
|
||||
color-mix(in oklab, var(--accent) 20%, transparent), transparent 70%),
|
||||
radial-gradient(42% 50% at 76% 68%,
|
||||
color-mix(in oklab, var(--glow-2) 18%, transparent), transparent 68%);
|
||||
}
|
||||
.gate-canvas { opacity: 0; transition: opacity 1s var(--ease-soft); }
|
||||
.gate-canvas.is-live { opacity: 0.7; }
|
||||
|
||||
/* 통과한 단계가 켜진다. 3D 와 텍스트를 잇는 유일한 연결이다 */
|
||||
.stop { transition: opacity var(--dur-quick) var(--ease-out); }
|
||||
.stop.is-active .stop-name { color: var(--accent); }
|
||||
.stop.is-active::before {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 22%, transparent);
|
||||
}
|
||||
|
||||
/* 스파인은 페이지의 본체다. main 칼럼(561px)에 가두면
|
||||
페이지에서 가장 긴 섹션이 가장 좁아진다.
|
||||
R1(zed.dev) 실측: 컨테이너 1120px 과 전체폭이 교대한다. */
|
||||
|
|
@ -173,3 +219,28 @@ const { c } = Astro.props;
|
|||
이 섹션은 페이지의 본체다. 콘텐츠를 걸고 하는 연출은 하지 않는다. */
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 게이트를 통과할 때 해당 단계를 켠다. 값이 바뀔 때만 DOM 을 쓴다.
|
||||
import { mountHeroScene } from "../lib/hero-scene";
|
||||
|
||||
const canvas = document.getElementById("pipeline-canvas") as HTMLCanvasElement | null;
|
||||
const section = document.getElementById("pipeline");
|
||||
const stops = [...document.querySelectorAll<HTMLElement>("#pipeline .stop")];
|
||||
|
||||
if (canvas && section && stops.length) {
|
||||
let last = -1;
|
||||
mountHeroScene({
|
||||
canvas,
|
||||
accentVar: "--accent",
|
||||
glowVar: "--glow-2",
|
||||
// 캔버스는 sticky 라 rect 가 변하지 않는다. 진행도는 섹션에서 읽는다.
|
||||
progressFrom: section,
|
||||
onGate(index) {
|
||||
if (index === last) return;
|
||||
last = index;
|
||||
stops.forEach((el, i) => el.classList.toggle("is-active", i === index));
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue