GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

관성 스크롤 9종 — 멈춘 뒤에도 값이 스스로 줄어드는 아홉 가지 방식

관성 스크롤은 손을 뗀 뒤에도 이동·강도 값이 곧바로 0이 되지 않고 이전 값을 이어받아 계산으로 줄어드는 스크롤 인터랙션을 말하는데, 아홉 개 중 하나는 스크롤이 멈춘 뒤에도 이동 값이 속도를 이어받아 서서히 줄어들고, 하나는 끝 지점을 넘어가면 늘어난 값이 손을 놓은 뒤 원래

아홉 개 모두 스크롤 위치·속도 값을 매 프레임 grep 가능한 상수(FRICTION·SPRING_K·GRAVITY 등)로 재계산하는 결정론적 rAF 적분입니다. 난수는 0 — 같은 입력이면 항상 같은 궤적을 그립니다. 화면의 스크롤 영역은 데모 프레임 안에 자체 컨테이너로 갇혀 있어 페이지 자체가 스크롤되지 않습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개의 해부 — 어떤 값이 언제 바뀌는가

아홉 개 전부 실제 사용자 입력(wheel·pointerdown/move/up·keydown·scroll 이벤트)에 반응하고, 입력만으로 자동 재생되는 항목은 하나도 없습니다 — 데모의 자동 시연 타이머는 !reduce 조건에서만 보조로 돌 뿐 리스너 자체를 대체하지 않습니다.

번호 이름 리스너 누적면적% 강도 움직인 프레임/23
01 관성 코스팅 wheel·pointerdown/move/up·resize 20.97 94.7 20
02 오버스크롤 탄성 pointerdown/move/up·wheel 21.33 65.3 23
03 속도 흐림 wheel·pointerdown/move/up·resize 20.22 105.2 18
04 구간 스냅 스프링 pointerdown/move/up·wheel 20.71 88.7 23
05 레이어별 감쇠 패럴랙스 wheel·pointerdown/move/up 9.96 83.6 23
06 낙하 정착 scroll(자기 트리거 가드)·keydown 22.22 47.4 14
07 마찰 감속 마퀴 pointerdown/move/up·wheel 5.44 105.0 23
08 지연 추적 스크롤바 wheel·pointerdown/move/up·keydown 1.78 108.0 9
09 러버 풀 wheel·pointerdown/move/up·keydown 1.80 44.3 9

값이 바뀌는 지점을 계산 방식으로 묶으면 세 갈래로 나뉩니다.

속도가 죽으며 줄어드는 값 — 01·03·05·07

01·03·05·07 은 손을 뗀 순간의 속도 값을 그대로 이어받아 매 프레임 곱셈으로 줄이는 감쇠 계열입니다. 01 은 이동 값 자체를, 03 은 속도에서 파생된 흐림 값을, 05 는 레이어마다 다른 감쇠율로 여러 이동 값을, 07 은 입력이 끊긴 뒤 속도 값을 목표 속도로 되돌리는 방식으로 줄입니다.

경계를 넘거나 바닥에 닿아 튕기는 값 — 02·04·06·09

02 는 스크롤 경계를 넘긴 늘어난 값이 스프링 힘으로 되돌아오고, 04 는 놓인 위치 값이 가장 가까운 구간 값으로 스프링 감쇠하며 정착하고, 06 은 떨어지는 위치 값이 바닥 값에서 반발계수만큼 튀어 오르길 반복하고, 09 는 방향이 바뀐 순간 값이 반대로 당겨졌다가 스프링으로 되돌아옵니다.

실제 스크롤 값을 지연해 뒤쫓는 값 — 08

08 은 앞의 두 갈래와 달리 감쇠도 반발도 없이, 뒤따르는 값이 실제 스크롤 값과의 차이만큼씩 매 프레임 좁혀가며(lerp) 쫓아가는 방식입니다.

패턴별 코드

01스크롤 관성 코스팅 — 스크롤이 멈춘 뒤에도 이동 값이 속도를 이어받아 서서히 줄어드는 방식

손을 뗀 순간의 속도 값을 이어받아 이동 값이 점점 줄어들다 멈춥니다. FRICTION 이 매 프레임 속도 값에 곱해지고, MIN_VELOCITY 아래로 떨어지면 rAF 루프가 멈춥니다.

  var FRICTION = 0.93;
  var MIN_VELOCITY = 0.04;
  var WHEEL_SCALE = 0.55;

  var pos = 0, vel = 0, raf = null, maxScroll = 0;

  function measure() {
    maxScroll = Math.max(0, track.scrollHeight - viewport.clientHeight);
  }
  function clamp(v) { return Math.max(-maxScroll, Math.min(0, v)); }
  function paint() { track.style.transform = 'translateY(' + pos.toFixed(1) + 'px)'; }

  function stopCoast() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  function coastTick() {
    vel *= FRICTION;
    pos = clamp(pos + vel);
    if (pos === 0 || pos === -maxScroll) { vel = 0; }
    paint();
    if (Math.abs(vel) < MIN_VELOCITY) { raf = null; return; }
    raf = requestAnimationFrame(coastTick);
  }
스크롤 관성 코스팅 — 스크롤이 멈춘 뒤에도 이동 값이 속도를 이어받아 서서히 줄어드는 방식
새 창에서 열기

01 · 스크롤 관성 코스팅 — 스크롤이 멈춘 뒤에도 이동 값이 속도를 이어받아 서서히 줄어드는 방식 — 측정 누적면적 20.972% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FRICTIONMIN_VELOCITYWHEEL_SCALE

02오버스크롤 탄성 — 스크롤 끝 지점을 넘어가면 늘어난 값이 되돌아오는 방식

끝 지점을 넘어 당기면 늘어난 값이 손을 놓은 뒤 원래 값으로 튕기듯 돌아옵니다. PULL_RESIST 로 당김 값을 억제하고, springTick 이 SPRING_K·SPRING_C 두 값으로 stretch 값을 1로 되돌립니다.

  var PULL_RESIST = 9;
  var MAX_STRETCH = 1.85;
  var SPRING_K = 210;
  var SPRING_C = 15;
  var HEADER_H = 56;

  var stretch = 1, sVel = 0, raf = null;

  function paint() {
    header.style.transform = 'scaleY(' + stretch.toFixed(3) + ')';
    var extra = HEADER_H * (stretch - 1);
    body.style.transform = 'translateY(' + extra.toFixed(1) + 'px)';
  }
  function stopSpring() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  function springTick() {
    var force = -SPRING_K * (stretch - 1) - SPRING_C * sVel;
    sVel += force * (1 / 60) * 0.06;
    stretch += sVel;
    paint();
    if (Math.abs(stretch - 1) < 0.003 && Math.abs(sVel) < 0.003) {
      stretch = 1; sVel = 0; paint(); raf = null; return;
    }
    raf = requestAnimationFrame(springTick);
  }
오버스크롤 탄성 — 스크롤 끝 지점을 넘어가면 늘어난 값이 되돌아오는 방식
새 창에서 열기

02 · 오버스크롤 탄성 — 스크롤 끝 지점을 넘어가면 늘어난 값이 되돌아오는 방식 — 측정 누적면적 21.332% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PULL_RESISTSPRING_KSPRING_C

03스크롤 속도 흐림 — 스크롤 속도 값이 커질수록 흐림 값이 비례해 커지는 방식

속도 값이 빨라질수록 흐림 값이 커지고, 멈추면 흐림 값이 0으로 되돌아옵니다. VELOCITY_TO_BLUR 가 속도 값을 흐림 값으로 환산하고 BLUR_SMOOTH 로 그 값을 부드럽게 따라갑니다. 배경은 사진이 아니라 CSS 그라디언트로 그려집니다.

  var VELOCITY_TO_BLUR = 0.55;
  var MAX_BLUR = 9;
  var BLUR_SMOOTH = 0.25;
  var COAST_FRICTION = 0.92;
  var MIN_VELOCITY = 0.05;

  var pos = 0, vel = 0, blur = 0, maxScroll = 0, raf = null;

  function measure() { maxScroll = Math.max(0, track.scrollWidth - viewport.clientWidth); }
  function clamp(v) { return Math.max(-maxScroll, Math.min(0, v)); }
  function paint() {
    track.style.transform = 'translateX(' + pos.toFixed(1) + 'px)';
    track.style.filter = blur > 0.05 ? 'blur(' + blur.toFixed(2) + 'px)' : 'none';
  }
  function stopLoop() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  function loopTick() {
    vel *= COAST_FRICTION;
    pos = clamp(pos + vel);
    if (pos === 0 || pos === -maxScroll) { vel = 0; }
    var target = Math.min(MAX_BLUR, Math.abs(vel) * VELOCITY_TO_BLUR);
    blur += (target - blur) * BLUR_SMOOTH;
    paint();
    if (Math.abs(vel) < MIN_VELOCITY && blur < 0.05) { blur = 0; paint(); raf = null; return; }
    raf = requestAnimationFrame(loopTick);
  }
스크롤 속도 흐림 — 스크롤 속도 값이 커질수록 흐림 값이 비례해 커지는 방식
새 창에서 열기

03 · 스크롤 속도 흐림 — 스크롤 속도 값이 커질수록 흐림 값이 비례해 커지는 방식 — 측정 누적면적 20.219% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

VELOCITY_TO_BLURMAX_BLURCOAST_FRICTION

04구간 스냅 스프링 — 놓인 스크롤 위치 값이 가장 가까운 구간 값으로 스프링 감쇠하며 정착하는 방식

놓인 위치 값이 가까운 구간 값을 오가며 잦아들다 멈춥니다. nearestTarget 이 SECTION_H 배수 중 가장 가까운 값을 고르고, springTick(target) 이 이 항목만의 SPRING_K·SPRING_C 로 그 값에 정착시킵니다.

  var SECTION_H = 150;
  var SECTION_COUNT = 3;
  var SPRING_K = 150;
  var SPRING_C = 7.5;

  var pos = 0, vel = 0, raf = null, maxScroll = SECTION_H * (SECTION_COUNT - 1);

  function clampFree(v) { return Math.max(-maxScroll, Math.min(0, v)); }
  function paint() { track.style.transform = 'translateY(' + pos.toFixed(1) + 'px)'; }
  function stopSpring() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  function nearestTarget() { return clampFree(Math.round(pos / SECTION_H) * SECTION_H); }

  function springTick(target) {
    var force = -SPRING_K * (pos - target) - SPRING_C * vel;
    vel += force * (1 / 60) * 0.06;
    pos += vel;
    paint();
    if (Math.abs(pos - target) < 0.4 && Math.abs(vel) < 0.4) {
      pos = target; vel = 0; paint(); raf = null; return;
    }
    raf = requestAnimationFrame(function () { springTick(target); });
  }
구간 스냅 스프링 — 놓인 스크롤 위치 값이 가장 가까운 구간 값으로 스프링 감쇠하며 정착하는 방식
새 창에서 열기

04 · 구간 스냅 스프링 — 놓인 스크롤 위치 값이 가장 가까운 구간 값으로 스프링 감쇠하며 정착하는 방식 — 측정 누적면적 20.706% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SECTION_HSPRING_KSPRING_C

05레이어별 감쇠 패럴랙스 — 스크롤이 멈춘 뒤 레이어마다 다른 감쇠율로 이동 값이 계속 줄어드는 방식

레이어마다 감쇠율 값이 달라 멈춘 뒤에도 이동 값이 저마다 다른 속도로 잦아듭니다. 각 레이어 객체가 자신의 decay 값을 들고 있어, 같은 루프 안에서도 층마다 다른 곡선으로 값이 줄어듭니다.

  var WRAP = 320;
  var raf = null;

  function clampWrap(v) { var m = v % WRAP; if (m > 0) { m -= WRAP; } return m; }
  function paint() {
    for (var i = 0; i < layers.length; i++) {
      layers[i].el.style.transform = 'translateX(' + layers[i].pos.toFixed(1) + 'px)';
    }
  }
  function stopLoop() { if (raf !== null) { cancelAnimationFrame(raf); raf = null; } }
  var inputActive = false;
  function loopTick() {
    var moving = false;
    for (var i = 0; i < layers.length; i++) {
      var L = layers[i];
      if (!inputActive) { L.vel *= L.decay; if (reduce) { L.vel = 0; } }
      L.pos = clampWrap(L.pos + L.vel);
      if (Math.abs(L.vel) > 0.02) { moving = true; }
    }
    paint();
    if (reduce || (!moving && !inputActive)) { raf = null; return; }
    raf = requestAnimationFrame(loopTick);
  }
레이어별 감쇠 패럴랙스 — 스크롤이 멈춘 뒤 레이어마다 다른 감쇠율로 이동 값이 계속 줄어드는 방식
새 창에서 열기

05 · 레이어별 감쇠 패럴랙스 — 스크롤이 멈춘 뒤 레이어마다 다른 감쇠율로 이동 값이 계속 줄어드는 방식 — 측정 누적면적 9.958% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WRAPdecayspeed

06낙하 정착 — 스크롤 진입 시 요소의 위치 값이 중력 값으로 떨어지다 바닥 값에서 튕기며 정착하는 방식

등장한 요소의 위치 값이 아래로 떨어지다 바닥 값에서 튀어 오르길 반복하며 잦아듭니다. GRAVITY 가 매 프레임 속도 값을 키우고, FLOOR_Y 에 닿으면 RESTITUTION 만큼만 속도를 남겨 반발합니다.

  var START_Y = -40;
  var FLOOR_Y = 0;
  var GRAVITY = 0.85;
  var RESTITUTION = 0.42;
  var STOP_VEL = 0.6;

  function dropTick(card, y, vel) {
    vel += GRAVITY;
    y += vel;
    if (y >= FLOOR_Y) {
      y = FLOOR_Y;
      vel = -vel * RESTITUTION;
      if (Math.abs(vel) < STOP_VEL) {
        card.style.transform = 'translateY(0px)';
        card.dataset.done = '1';
        return;
      }
    }
    card.style.transform = 'translateY(' + y.toFixed(1) + 'px)';
    requestAnimationFrame(function () { dropTick(card, y, vel); });
  }
낙하 정착 — 스크롤 진입 시 요소의 위치 값이 중력 값으로 떨어지다 바닥 값에서 튕기며 정착하는 방식
새 창에서 열기

06 · 낙하 정착 — 스크롤 진입 시 요소의 위치 값이 중력 값으로 떨어지다 바닥 값에서 튕기며 정착하는 방식 — 측정 누적면적 22.223% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GRAVITYRESTITUTIONSTART_Y

07마찰 감속 — 입력 값이 사라진 뒤 속도 값이 마찰만큼 서서히 줄어드는 방식

입력이 끊기면 속도 값이 일정한 마찰 값만큼씩 줄어들다 원래 속도 값으로 돌아옵니다. FRICTION_PULL 이 매 프레임 속도 값을 BASE_SPEED 쪽으로 끌어당기는 지수 보간입니다.

  var CHIP_W = 56, GAP = 10, SET_COUNT = 6;
  var WRAP = SET_COUNT * (CHIP_W + GAP);
  var BASE_SPEED = -0.9;
  var FRICTION_PULL = 0.06;
  var pos = 0, vel = BASE_SPEED;
  var inputActive = false;
  var raf = null;

  function clampWrap(v) { var m = v % WRAP; if (m > 0) { m -= WRAP; } return m; }
  function paint() { track.style.transform = 'translateX(' + pos.toFixed(1) + 'px)'; }
  function loopTick() {
    if (reduce) { raf = null; return; }
    if (!inputActive) { vel += (BASE_SPEED - vel) * FRICTION_PULL; }
    pos = clampWrap(pos + vel);
    paint();
    raf = requestAnimationFrame(loopTick);
  }
마찰 감속 — 입력 값이 사라진 뒤 속도 값이 마찰만큼 서서히 줄어드는 방식
새 창에서 열기

07 · 마찰 감속 — 입력 값이 사라진 뒤 속도 값이 마찰만큼 서서히 줄어드는 방식 — 측정 누적면적 5.444% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BASE_SPEEDFRICTION_PULLCHIP_W

08지연 추적 — 뒤따르는 값이 실제 스크롤 값을 곧바로 따라가지 않고 지연 값만큼 늦게 뒤쫓는 방식

뒤따르는 값이 실제 스크롤 값을 지연 값만큼 늦게 쫓다가 서서히 따라잡습니다. LAG 값(소스 기준 0.15)만큼씩 남은 차이를 좁혀가는 lerp 방식이라 감쇠도 반발도 쓰지 않습니다.

  function paintTrack() { track.style.transform = 'translateY(-' + scrollPos.toFixed(1) + 'px)'; }
  function targetThumbY() { return maxScroll <= 0 ? 0 : (scrollPos / maxScroll) * (railH - thumbH); }
  function paintThumb() { thumb.style.transform = 'translateY(' + thumbY.toFixed(1) + 'px)'; }
  function loopTick() {
    var target = targetThumbY();
    if (reduce) { thumbY = target; paintThumb(); raf = null; return; }
    thumbY += (target - thumbY) * LAG;
    if (Math.abs(target - thumbY) < SNAP_EPS) {
      thumbY = target; paintThumb(); raf = null; return;
    }
    paintThumb();
    raf = requestAnimationFrame(loopTick);
  }
지연 추적 — 뒤따르는 값이 실제 스크롤 값을 곧바로 따라가지 않고 지연 값만큼 늦게 뒤쫓는 방식
새 창에서 열기

08 · 지연 추적 — 뒤따르는 값이 실제 스크롤 값을 곧바로 따라가지 않고 지연 값만큼 늦게 뒤쫓는 방식 — 측정 누적면적 1.782% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LAGSNAP_EPSrowH

09러버 풀 — 스크롤 방향이 바뀌는 순간 값이 고무줄처럼 당겨졌다 되돌아오는 방식

방향이 바뀐 순간 값이 반대로 당겨졌다가 제자리 값으로 튕기듯 되돌아옵니다. kickNav 가 KICK 값만큼 navVel 에 충격을 주고, navTick 안의 이 항목 고유 SPRING_K·SPRING_C 값이 navScale 을 1로 되돌립니다.

  function paintTrack() { track.style.transform = 'translateY(-' + scrollPos.toFixed(1) + 'px)'; }
  function paintNav() { nav.style.transform = 'scaleY(' + navScale.toFixed(3) + ')'; }
  function navTick() {
    var force = -SPRING_K * (navScale - 1) - SPRING_C * navVel;
    navVel += force * 0.016;
    navScale += navVel * 0.016;
    if (reduce) { navScale = 1; navVel = 0; paintNav(); navRaf = null; return; }
    if (Math.abs(navScale - 1) < 0.003 && Math.abs(navVel) < 0.02) {
      navScale = 1; navVel = 0; paintNav(); navRaf = null; return;
    }
    paintNav();
    navRaf = requestAnimationFrame(navTick);
  }
  function ensureNavLoop() { if (navRaf === null) { navRaf = requestAnimationFrame(navTick); } }
  function kickNav(sign) {
    if (reduce) { return; }
    navVel += sign * KICK;
    ensureNavLoop();
  }
러버 풀 — 스크롤 방향이 바뀌는 순간 값이 고무줄처럼 당겨졌다 되돌아오는 방식
새 창에서 열기

09 · 러버 풀 — 스크롤 방향이 바뀌는 순간 값이 고무줄처럼 당겨졌다 되돌아오는 방식 — 측정 누적면적 1.796% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MIN_VELKICKSPRING_C

값이 패턴을 바꾸는 곳 — 감쇠·질량·경계 값의 기준

감쇠 값 — FRICTION·COAST_FRICTION·FRICTION_PULL

01 의 FRICTION 0.93 은 손을 뗀 뒤 반 박자 만에 속도 값이 잦아드는 짧은 관성입니다. 03 은 같은 계열의 COAST_FRICTION 0.92 를, 07 은 목표 속도로 끌어당기는 FRICTION_PULL 0.06 을 씁니다. 1에 가까울수록 속도 값이 오래 살아남고, 0에 가까울수록 곧바로 죽습니다.

질량·중력 값 — GRAVITY·RESTITUTION

06 의 GRAVITY 0.85 는 매 프레임 속도 값에 더해지는 낙하 가속이고, RESTITUTION 0.42 는 바닥에 닿을 때마다 남기는 속도 비율입니다. 값이 클수록 튀는 높이가 오래 유지됩니다.

경계·스프링 값 — SPRING_K·SPRING_C(02·04·09 각각 별개)

스프링 상수는 이 편 안에서 세 곳에 따로 있습니다 — 02 의 SPRING_K 210·SPRING_C 15, 04 의 SPRING_K 150·SPRING_C 7.5, 09 의 SPRING_K 260·SPRING_C 14 입니다. 같은 이름이라도 항목이 다르면 서로 다른 값이니, 인용할 때는 반드시 항목 번호를 함께 밝혀야 합니다. K 가 클수록 목표 값으로 되돌아가는 힘이 세지고, C 가 클수록 진동이 빨리 잦아듭니다.

어디서 깨지는가 — 빠른 연속 스크롤·저사양 기기·모션 감소

빠른 연속 스크롤에서는 01·03·05·07 처럼 속도 값을 계속 덮어쓰는 항목이 이전 rAF 틱과 새 입력 값이 겹쳐 순간적으로 튀는 값이 나올 수 있습니다. 저사양 기기에서는 06 의 GRAVITY·RESTITUTION 반복 계산이 프레임을 놓치면 반발 값이 부자연스럽게 커 보입니다. prefers-reduced-motion 이 켜지면 각 index.html 의 reduce 분기가 작동해 01·02·03·04·06·08·09 는 이동·강도 값을 즉시 목표 값으로 스냅하고, 05 는 레이어 속도 값을 0으로 고정하며, 07 은 rAF 루프 자체를 시작하지 않습니다. 데모 zip 을 받아 두면 압축 파일 안에서 각 index.html 을 열어 이 분기를 그대로 확인할 수 있는데, 그 zip 을 여는 비번은 azjysfxp 이고 패키지 안에는 항목 9개의 html·scss·js 가 전부 들어 있습니다.

변형 세 가지와 접근성

이름 값 느낌
관성 코스팅 지속 01 FRICTION 0.93 → 0.99 FRICTION 이 0.93이면 손을 뗀 뒤 반 박자 만에 속도 값이 잦아들어 짧게 미끄러지는데, 0.99 로 올리면 같은 속도로 놓아도 몇 초씩 밀려나가며 훨씬 긴 관성 활주감이 된다
낙하 정착 탄성 06 RESTITUTION 0.42 → 0.75 RESTITUTION 이 0.42면 바닥에서 한두 번 낮게 튀고 바로 멈추는데, 0.75 로 올리면 같은 높이에서 떨어져도 여러 번 높게 되튀며 한참을 튕기다 정착하는 낙하감이 된다
방향전환 스프링 반동 09 SPRING_K 260 → 90 SPRING_K 가 260이면 방향을 바꿀 때 네비 바가 짧고 뻣뻣하게 한 번 튕기고 마는데, 90 으로 낮추면 같은 충격에도 스프링이 물러져 부드럽게 여러 번 출렁이며 되돌아오는 반동감이 된다

접근성은 소스에 실재하는 범위로만 밝힙니다. 아홉 항목의 스크롤 컨테이너는 전부 role="application"과 aria-label을 달고 있고, 06·07·08·09 네 항목에는 tabindex="0"도 있어 키보드 포커스가 들어갑니다. 06·08·09 는 keydown 리스너까지 등록돼 있어 방향키로도 값을 움직일 수 있습니다. prefers-reduced-motion은 위에서 밝힌 대로 항목별 reduce 분기가 이동·강도 값을 즉시 스냅하거나 루프를 멈추는 식으로 처리합니다.

FAQ

관성 스크롤 값과 오버스크롤 바운스 값은 같은 계산인가요?

아니요. 01·03·05·07 의 관성 값은 손을 뗀 순간의 속도 값을 그대로 이어받아 감쇠시키는 계산이고, 02·09 처럼 경계를 넘거나 방향이 바뀔 때 나오는 오버스크롤 바운스 값은 스프링 힘 방정식(-K*x - C*v)으로 목표 값을 되찾는 계산입니다. 두 계산 방식의 차이는 MDN overscroll-behavior 문서의 정의와도 맞습니다.

왜 CSS scroll-behavior: smooth 만으로는 이 아홉 가지를 구현할 수 없나요?

scroll-behavior는 정해진 목표 지점까지 브라우저가 정한 곡선으로 한 번 이동시켜 줄 뿐, 01의 감쇠나 06의 반발처럼 매 프레임 값을 계산해 바꾸는 물리 시뮬레이션은 만들지 않기 때문입니다. 이 아홉 개는 전부 requestAnimationFrame 루프 안에서 직접 값을 적분합니다.

스크롤 애니메이션 사이트를 만들 때 아홉 개를 한 페이지에 다 넣어도 되나요?

기술적으로는 가능하지만 권장하지 않습니다. 각 항목이 자체 rAF 루프를 도는 구조라 스크롤 인터랙션이 몰리는 페이지에서는 필요한 항목 한두 개만 골라 쓰는 편이 프레임 유지에 유리합니다. 데모처럼 프레임 컨테이너로 영역을 나누면 스크롤 이펙트가 서로의 값에 간섭하지 않습니다.

더 읽을거리

CSS 스크롤 애니메이션 9종은 스크롤 위치 값에 CSS animation-timeline을 그대로 묶어 재생하는 방식이라, 이 편처럼 속도·감쇠·스프링 값을 매 프레임 계산하는 관성 스크롤과는 계산 축이 다릅니다. 당겨서 새로고침 9종은 당겼다 놓는 순간의 거리 값이 임계값을 넘었는지만 판정하는 반면, 이 편은 그 이후 값이 시간에 따라 어떻게 줄거나 튕기는지를 다룹니다. 버튼 눌림 효과 9종은 버튼 자체의 질량·힘 값이 눌림·놓임 순간에만 반응하는데, 358 은 스크롤 속도·위치에서 파생되는 관성·감쇠·스프링 값을 다룬다는 점에서 입력 축이 다릅니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.