Apple 스타일 스크롤 애니메이션
효과는 타이머가 아닌 스크롤 위치에 따라 제어됩니다. 위로 스크롤하면 역재생됩니다. 라이브러리가 필요 없습니다.
세 가지 핵심 개념
- 고정.
height: 300vh등 큰 높이를 가진 래퍼는position: sticky; top: 0; height: 100vh; overflow: hidden인 자식을 가집니다. 자식은 래퍼가 스크롤되는 동안 화면에 고정되어 있습니다. 래퍼의 추가 높이가 애니메이션 예산이 됩니다. - 진행률. 하나의 함수가 스크롤 위치를 0..1 사이의 숫자로 변환합니다:
- 고정 섹션:
clamp(-rect.top / (rect.height - innerHeight)) - 요소가 뷰포트에 진입할 때:
clamp((innerHeight*a - rect.top) / (innerHeight*(a-b)))
- 고정 섹션:
- 숫자로 시각 효과 제어.
translate,rotate,scale,opacity(및borderRadius)를 각 프레임마다 설정합니다.requestAnimationFrame으로 조절된 하나의scroll리스너가 모든 것을 다시 계산합니다.
바로 사용 가능한 엔진: references/scroll-engine.js
잘 작동하는 효과 (모두가 아닌 3-5개 선택)
| 효과 | 방법 |
|---|---|
| 조립 | 아이템이 뷰포트 가장자리에 흩어진 상태(무작위 각도, 회전, 배율 .35, 투명도 .2)로 시작하여 레이아웃 위치로 부드럽게 이동합니다. 각 아이템의 자연스러운 위치로부터의 오프셋을 (로드/리사이즈 시) 한 번 계산한 후 (1 - ease(p))를 곱합니다. 인덱스에 따라 지연을 추가합니다. |
| 단어 강조 | 각 단어를 span으로 감쌉니다. clamp(p * 1.25 * N - k)에 따라 투명도가 .18에서 1로 변경됩니다. |
| 고정 스테퍼 | step = floor(p * N); 분수 p*N - step은 활성 단계 내 진행률 표시줄을 채웁니다. 단계를 클릭하면 해당 단계 위치로 scrollTo됩니다. |
| 기울임-평면 | 전화기/기기 목업의 경우 perspective(1400px) rotateX((1-e)*22deg) translateY((1-e)*60px) scale(.84 + .16*e)를 사용합니다. |
| 카드 등장 | translate: 0 (1-e)*80px; scale: .9+.1e; opacity: e, 열(i % cols)에 따라 지연됩니다. |
| 확장 | 비디오/이미지 scale .78 → 1 및 borderRadius 44px → 28px를 사용합니다. |
진행률에 적용하기 전에 ease = t => 1 - (1-t)**3을 사용합니다.
규칙 (어렵게 배운 교훈)
- 개별 CSS 속성
translate,rotate,scale을 제어에 사용하고,transform은 호버용으로 남겨둡니다. 그렇지 않으면 호버와 스크롤이 동일한 속성을 놓고 경쟁합니다. - 같은 요소에
.reveal페이드 클래스와 제어되는transform을 결합하지 마세요. 하나만 선택하세요. - 섹션 제목을 내용과 함께 고정 패널 안에 넣으세요. 패널이 내용만 담고 중앙에 위치하면 큰 모니터에서 빈 공간이 많이 생깁니다.
- 스크롤 거리를 적절히 유지하세요. N단계 스테퍼의 경우 약
N * 55vh + 100vh, 단일 효과의 경우240-300vh입니다. 사용자는 700vh 이상을 피곤하게 생각합니다. - 고정되는 것은 100vh에 맞게 제한하세요. 짧은 화면(
max-height: 820px)과 폰에서는 자막과 추가 요소를 숨기고 스테퍼에서는 활성 단계만 표시합니다. - 동작 감소:
prefers-reduced-motion: reduce가 설정된 경우 모든 곳에서 진행률1을 반환합니다(최종 상태, 움직임 없음). 요소를 숨긴 상태로 두지 마세요. - 고정(sticky)은 상위 요소에
overflow가 없어야 합니다. 단,body에overflow-x: hidden은 괜찮습니다. 고정 요소와 페이지 사이에 있는 어떤 래퍼에도overflow: hidden을 피하세요. - 리사이즈 시 다시 계산 (조립 오프셋은 레이아웃에 의존합니다).
translate/rotate/scale/opacity/border-radius만 애니메이션하고 레이아웃 속성은 피하세요.- 비디오의 첫 번째/마지막 짧은 부분을 피하세요:
<video>재생을 제어할 때 짧은 클립은 빈 화면을 만들 수 있습니다.
일반적인 페이지 순서
히어로 (고정, 조립) → 일반 문장 (효과 없음) → 증거/영상 (확장) → 고정 스테퍼 → 기기 목업 (기울임-평면) → 카드 그리드 (등장) → CTA. Apple 느낌을 위해 검은색과 밝은 회색 섹션을 번갈아 사용하고 악센트 색상 하나를 사용합니다.
테스트
헤드리스 Chrome의 --screenshot은 스크롤할 수 없습니다. 대신 DevTools 프로토콜을 통해 실제 Chrome을 제어하세요:
chrome --headless=new --remote-debugging-port=9333 --user-data-dir=/tmp/prof about:blank &- Node (v22+에는 전역
WebSocket이 있음)에서:http://localhost:9333/json을 가져오고, 페이지의webSocketDebuggerUrl을 열고,Emulation.setDeviceMetricsOverride,Page.navigate을 보낸 다음, 각 위치에 대해Runtime.evaluate("scrollTo(0,Y)")를 보내고 ~700ms 기다린 후Page.captureScreenshot을 보냅니다. - 각 고정 섹션의
offsetTop과 높이(예: 0%, 60%, 100%)에서 Y를 선택합니다. - 데스크톱(1440x900), 큰 화면(2000x1300), 폰(390x800)에서 확인하고
document.documentElement.scrollWidth === innerWidth인지 확인합니다(수평 오버플로우 없음).
전달 체크리스트
- 모든 효과는 스크롤 진행률에 의해 제어되며, 역재생 가능
- 동작 감소 대체 처리
- 폰 레이아웃 확인, 수평 스크롤 없음
- 고정된 스크롤 길이가 적절하며, 큰 화면에서 빈 공간 없음
- 각 고정 섹션의 시작/중간/끝에서 스크린샷 촬영