스크롤 모션 그래픽은(는) 무엇을 하나요?
스크롤 위치에 의해 구동되는 캔버스 타임라인: 고정된 스테이지(pinned stage), 진행률의 순수 함수인 릴(reels), 그리고 그림이 절대 닿지 않는 캡션. 이 방법은 ImuTechz 홈페이지의 스토리 드럼, R&D 드럼, Founder 경로 필름을 구동합니다. 또한 assets/demo.html에 있는 5릴 데모도 지원합니다.
전체 기술은 한 문장으로 요약됩니다. 스크롤 위치가 유일한 시계입니다. 스크롤 위치를 p ∈ [0, 1]에 매핑하고, 모든 픽셀, 캡션, 회전을 p의 함수로 만드세요.
좋은 예시의 특징
- 비디오처럼 scrub됩니다. 앞으로 스크롤하면 영상이 재생됩니다. 뒤로 스크롤하면 프레임 단위로 되감깁니다. 중간에 멈추면 의미 있는 프레임이 남습니다.
- 페이지가 절대 하이재킹되지 않습니다. 휠, 트랙패드, 터치, 키보드 모두 다른 곳과 동일하게 페이지를 움직입니다. 섹션은 그저 높을 뿐입니다.
- 텍스트는 항상 읽을 수 있습니다. 캡션은 카드 위의 실제 DOM 텍스트입니다. 그림은 캡션이 남긴 공간에 배치되며, 캔버스는 안전망으로 주변을 클리핑합니다.
- 단순한 쇼가 아니라 컨트롤입니다. 스토리 드럼의 면(face)은 링크입니다. 레일, 화살표 버튼, 화살표 키, 마우스 드래그 모두 페이지를 스크롤하여 탐색하므로 스크롤과 드럼이 절대 불일치하지 않습니다.
- 유휴 페이지는 리소스를 소모하지 않습니다. p가 안정되거나 섹션이 뷰포트를 벗어나면 requestAnimationFrame이 실행되지 않습니다.
- Reduced motion을 존중합니다. 스무딩, 중간 드럼 롤, 캡션 진입 애니메이션이 없습니다. 모든 콘텐츠는 그대로 표시됩니다.
- 360px에서 1920px 너비까지, 라이트/다크 모드, DPR 1과 2에서도 문제없이 작동합니다.
엄격한 규칙
- p는 스크롤 위치에서만 오며, 절대 시계에서 오지 않습니다. 릴 내부에서
performance.now(),Date, CSS 애니메이션,setInterval을 사용하지 마십시오. 목표 p를 향한 lerp 스무딩은 최종 프레임으로 수렴하므로 허용됩니다. - 릴은 순수 함수입니다:
draw(ctx, t, env). 릴은 프레임 간에 상태를 유지하지 않으며Math.random()을 절대 호출하지 않습니다. 대신 시드된hash(i)를 사용하십시오. 동일한 (t, env)는 동일한 픽셀을 그려야 합니다. - CSS로 피닝하고, JS로 하지 마십시오.
position: relative; height: N×vh인 긴 트랙을 만들고 그 안에position: sticky; top: 0; height: 100svh인 스테이지를 배치하십시오. 스테이지에position: fixed를 설정하지 말고, body를 잠그지 말며, 트랙의 조상에overflow: hidden|auto|clip을 설정하지 마십시오. - 절대 입력을 가로채지 마십시오. 휠이나 터치에
preventDefault를 사용하지 말고, html이나 body에overflow: hidden을 설정하지 말며, 트랙에 scroll snapping을 사용하지 마십시오. 포커스된 드럼만 화살표 키를 처리할 수 있으며, 그 방식은 페이지를 스크롤하는 것입니다. - 스크롤하여 탐색하십시오. 모든 버튼, 레일 아이템, 키, 앵커는
window.scrollY를 변경합니다. p나 회전을 직접 설정하지 마십시오. 프로그래매틱 점프에는behavior: "instant"를 사용하십시오."auto"는 페이지의 CSSscroll-behavior: smooth를 따르므로 애니메이션될 수 있습니다. - 텍스트는 DOM에 있습니다. 캡션, 제목, 링크는 HTML입니다. 캔버스 텍스트는 연도, 축 틱, 단계 이름과 같은 장식용 레이블로 제한됩니다.
- 매 프레임 캡션을 측정하고 그림을 캡션 밖에 유지하십시오. 각 캡션과 HUD 요소는 keep-out 영역(
data-keep)입니다. 릴은env.free내부에 배치되며, 엔진은 even-odd 클립으로 keep-out 영역을 클리핑합니다. - 절대 두 개의 캡션을 동시에 표시하지 마십시오. 캡션 윈도우는 각 경계에 간격을 두고 릴 내부에 위치하며, 릴은 해당 간격 내에서 크로스페이드됩니다.
- 유휴 상태가 되면 루프를 중지하십시오. 섹션이 뷰포트 근처에 있을 때만(IntersectionObserver) 스크롤 또는 리사이즈 시 rAF를 시작하고, p가 목표와 같아지면 중지하십시오.
- 캔버스는
min(devicePixelRatio, 2)로 스테이지 크기에 맞춰집니다. 매 프레임setTransform(dpr, …)을 호출하고 매 프레임 지웁니다. - 색상은 CSS 커스텀 프로퍼티에서 가져옵니다. 릴에 라이트 모드 팔레트를 하드코딩하지 마십시오.
prefers-color-scheme변경 시 팔레트를 다시 읽으십시오. prefers-reduced-motion: reduce를 존중하십시오. 스무딩을 사용하지 말고, 드럼이 면에서 면으로 스냅되게 하며, 캡션 진입 애니메이션을 끄십시오. 사용자가 구동하므로 프레임은 여전히 스크롤을 따릅니다.
빌드 순서
다음 순서대로 진행하십시오. 각 단계는 다음 단계 전에 테스트 가능해야 합니다.
- 콘텐츠 먼저. 챕터를 작성하십시오: 각 챕터에 대해 kicker, 제목, 한두 문장, 선택적 링크. 릴 개수(3
7개)를 선택하고 각 릴에 약 80100svh의 스크롤을 할당하십시오. - 트랙과 스테이지. 내부에 sticky 100svh 스테이지가 있는 긴 섹션을 추가하고, JS 없이 피닝되는지 확인하십시오(layout-and-captions 참조).
- Scrub 엔진.
ScrollMotion.scrub또는film을 연결하고(engine 참조) p를 로그로 출력하십시오. 핀 시작에서 0, 핀 끝에서 1을 읽고 양방향으로 움직이는지 확인하십시오. - DOM의 캡션과 HUD. 브레이크포인트별로 배치하고
data-keep으로 표시한 다음 엔진이 p에서 불투명도를 구동하게 하십시오. - 릴을 하나씩.
env.free내부에draw(ctx, t, env)를 작성하고(drawing 참조), 다음 릴을 작성하기 전에 앞뒤로 scrub하십시오. - 모션 문법. 진입, 유지, 종료 엔벨로프, stagger, 머뭄(dwell), 크로스페이드를 추가하십시오(motion-grammar 참조).
- 상호작용 (드럼인 경우). 면 링크, 레일, 이전/다음 버튼, 키, 드래그를 추가하고 모두 스크롤로 탐색하게 하십시오.
- 성능 및 접근성 패스. 유휴 루프, DPR, reduced motion, 포커스 순서, 스크린 리더 텍스트, 다크 모드를 다루십시오(performance-and-a11y 참조).
- 검증. 360, 390, 414, 768, 1024, 1280, 1440, 1920에서 헤드리스 스크립트로 확인하십시오(verification 참조). 모든 검사가 통과할 때까지 완료되었다고 하지 마십시오.
결정 체크리스트
캔버스 필름 vs DOM/CSS 변환?
- 많은 작은 움직이는 부품, 선을 그리는 요소, 입자, 차트 → 캔버스 필름 (
ScrollMotion.film). - 소수의 카드, 이미지, 텍스트 패널 →
scrub으로 구동되는 DOM 변환 (드럼이 하나의 예). - 실제 3D 메시나 셰이더 → 이 스킬만으로는 부족합니다. WebGL을 사용하되 규칙 1
5, 912를 지키십시오.
핀 모드 vs 뷰 모드?
- 섹션이 스토리를 재생하는 동안 화면을 고정해야 함 →
mode: "pin"(긴 트랙과 sticky 스테이지). - 효과가 콘텐츠가 지나감에 따라 따라 움직임 (마키나 parallax 등) →
mode: "view", 피닝 없음.
트랙 높이는 얼마나?
- 필름의 경우
n × 80–100svh + 100svh, 드럼의 경우n × 70vh + 100vh를 사용하십시오. 테스트 리더가 "너무 빠르다"고 하면 높이를 추가하십시오. 휠을 가로채서 속도를 늦추지 마십시오.
캡션은 어디에?
- 데스크톱(≥760px): 그림 옆(왼쪽 또는 오른쪽 카드) 또는 넓은 하단 카드. 휴대폰: 항상 하단 시트, 그림은 그 위에.
- 고정된 사이트 헤더가 있는 경우 HUD와 드럼이 헤더를 피할 수 있도록
--sm-nav-h를 설정하십시오.
드럼이 필요한가?
- 챕터가 사용자가 이동하고 싶어할 것들(페이지, 제품, 섹션)일 때 드럼을 사용하십시오. 그렇지 않으면 필름만 사용하십시오.
실패 모드 및 수정
| 증상 | 원인 | 수정 |
|---|---|---|
| 스테이지가 피닝되지 않고 스크롤됨 | 조상에 overflow: hidden/auto/clip이 있거나 스테이지에 top이 없음 | overflow 제거(필요시 body에 overflow-x: clip 사용)하고 top: 0 설정 |
| 애니메이션이 앞으로만 재생되거나, 스크롤 업 시 점프 | 프레임 간에 상태가 누적되었거나, 릴에 시계 또는 Math.random이 있음 | 릴을 t의 순수 함수로 만들고, 난수는 hash(i) 사용 |
| 그림이 캡션 아래로 그림 | 릴이 전체 스테이지에 배치됨 | env.free 안에 배치하고, 캡션에 data-keep 유지, 엔진이 클리핑하도록 함 |
| 페이드 중 캡션 상자가 잘못된 위치에서 측정됨 | getBoundingClientRect 사용(변환 포함) | 레이아웃 상자 측정(offsetLeft/Top/Width/Height, boxIn 참조) |
| 경계에서 두 캡션이 겹침 | 캡션 윈도우가 서로 닿음 | 캡션을 1 − cross에서 페이드 아웃하고 cross에서 페이드 인; 릴은 간격에서 크로스페이드 |
| 유휴 페이지에서 팬이 돌아감 | rAF 루프가 멈추지 않음 | cur === target일 때 중지; IntersectionObserver로 게이트 |
| 휴대폰에서 흐릿한 캔버스 / 4K에서 느림 | 캔버스가 CSS 크기이거나 DPR 제한 없음 | width = W × min(dpr, 2) 및 setTransform(dpr, 0, 0, dpr, 0, 0) 사용 |
| 레일 클릭 후 드럼과 페이지 불일치 | 컨트롤이 회전을 직접 설정함 | 모든 컨트롤은 scrollToProgress 호출; 회전은 항상 p에서만 옴 |
| 드래그 시 버벅임 후 되돌아감 | 페이지에 scroll-behavior: smooth가 있어 scrollTo(x, y)가 애니메이션됨 | 드래그 중에는 scrollTo({ top, behavior: "instant" }) 사용 |
| 드래그 후 면 클릭 시 링크 열림 | 드래그를 끝내는 클릭이 발생함 | 4px 이상 움직인 드래그 후 캡처 단계 클릭 하나를 삼킴 |
| 모바일에서 URL 바가 숨을 때 100vh 점프 | 스테이지에 vh 사용 | 스테이지에 100svh 사용; 스테이지 자체 높이로 p 계산 |
| 다크 모드에서 라이트 모드 그림 표시 | 하드코딩된 색상 | readPalette로 --sm-* 토큰 읽기; 색상 테마 변경 시 다시 읽기 |
| 드럼 뒷면이 비침 | backface-visibility: hidden 누락 | 모든 면에 -webkit- 접두사와 함께 설정 |
| 드럼 면 썸네일이 잘못된 크기로 그려짐 | 3D 변환된 면의 예상 직사각형 사용 | clientWidth/clientHeight로 크기 지정 |
파일
- references/engine.md: p, scrub 루프,
film(), 탐색, React 훅. - references/drawing.md: 릴 계약, 캔버스 설정, 경로, pop-in, 입자, 팔레트.
- references/layout-and-captions.md: 트랙 및 스테이지 CSS, keep-out, 자유 영역, 클립, 드럼 레이아웃.
- references/motion-grammar.md: 엔벨로프, stagger, 머뭄, 크로스페이드, 페이싱, 이징.
- references/performance-and-a11y.md: 유휴 루프, 레이아웃 비용, reduced motion, 키보드, 포커스, 스크린 리더.
- references/verification.md: 8가지 너비, 헤드리스 Brave/Chrome 스크립트, 수동 패스.
- assets/demo.html: 독립 실행형, 제로 종속성, 더블 클릭으로 실행. 5개의 릴과 스토리 드럼.
- assets/scroll-motion.js: 엔진(
scrub,film,drum,scrollToProgress및 헬퍼), UMD. - assets/reels.js: 5개의 예제 릴(orbit, path, stack, wave, close) 및 썸네일용
still(). - assets/scroll-motion.css: 필름 및 드럼 스타일, 라이트 및 다크 토큰.
- assets/ScrollFilm.jsx: 필름용 React 래퍼 및
useScrub훅. - assets/StoryDrum.jsx: React 스토리 드럼 및 자체 회전
DrumPreview. assets/preview-orbit.jpg,assets/preview-path.jpg,assets/preview-close.jpg: 1440 × 900에서 데모 스틸 이미지.
데모에서 시작하십시오: 열고, 소스를 보고, 콘텐츠를 교체하십시오. 파일 하단의 연결 코드는 약 60줄입니다.