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

스크롤 파티클 배경 9종 — 값 하나로 흐름이 달라지는 캔버스 장면

파티클 배경 9종을 값 기준으로 나누면, 아홉 가지 중 하나는 스크롤 속도 값이 빨라질수록 파티클이 생성되는 빈도 값이 늘어나고, 하나는 파티클마다 다른 깊이 값에 따라 같은 스크롤 이동량이 서로 다른 거리로 적용되며, 또 하나는 스크롤 위치 값이 한 지점에 가까워질수록 그 자리

아홉 데모는 전부 스크롤 컨테이너의 scroll·resize 이벤트와 requestAnimationFrame 루프를 함께 써서, 스크롤 위치·속도·방향 값 중 하나를 매 프레임 계산해 파티클의 좌표나 색을 정합니다. 항목마다 그 값이 무엇을 밀어붙이는지 표와 코드로 먼저 짚고, 실제 스크롤 조작이 그 값을 어디서 깨뜨리는지는 뒤에서 따로 다룹니다.

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

아홉 개의 해부 — 어떤 값이 스크롤에 반응하는가

다음 표에는 캔버스를 얼마나 채웠는지·움직임이 얼마나 강했는지·몇 프레임이나 값이 바뀌었는지 세 값이 항목별로 들어 있습니다. 배경 파티클처럼 화면 전체를 채우는 항목 중 누적면적이 가장 넓은 항목은 01 하나(상승 흐름장이 화면 전체를 상시 채우는 만큼 면적을 크게 씁니다)이고, 가장 좁은 항목은 06 하나(평소엔 입자가 거의 없다가 플릭 순간에만 몰아서 터지므로 누적면적 자체가 작습니다)이며, 강도가 가장 센 항목은 09 하나(노이즈 필드가 매 프레임 각도를 다시 구해 순간 변화량이 큽니다)뿐입니다. 움직인프레임도 06만 12/23이고 나머지 여덟 항목은 전부 23/23입니다.

항목 누적면적% 강도_평균 움직인프레임
01 15.71% 88.9 23/23
02 8.29% 85.1 23/23
03 13.02% 101.9 23/23
04 13.94% 85.8 23/23
05 8.34% 106.9 23/23
06 3.72% 69.5 12/23
07 7.91% 124.4 23/23
08 7.17% 104.2 23/23
09 13.37% 125.8 23/23

아홉 항목이 값을 밀어붙이는 계산 방식으로 다시 나누면 네 무리로 갈립니다.

매 프레임 흐름장이 값을 미는 것 — 01·03·09

01은 RISE_SPEED·p.speedVar로 매 프레임 위로 올라가는 값 위에 스크롤 속도×WIND_GAIN 바람과 FIELD_FREQ 사인 흔들림을 더하고, 03은 레인마다 고정된 laneSign에 스크롤 방향 부호 dirSign을 곱해 가로 속도를 정하며, 09는 좌표에 scrollPos×SCROLL_WARP를 섞은 값을 PERM_SIZE 256 순열 기반 valueNoise에 넣어 매 프레임 흐름 각도를 다시 구합니다. 셋 다 목표 값을 따로 두지 않고 그 프레임의 필드 계산만으로 다음 위치가 곧바로 정해집니다.

스프링·완화로 목표를 뒤쫓는 값 — 05·08

05는 scrollVel×DRIVE_GAIN 목표 변위를 STIFFNESS·SETTLE_DAMP 스프링으로 뒤쫓고, 08은 restR×(1-scrollPos×REST_SHRINK) 목표 반지름을 PULL_RATE 완화율로 뒤쫓으며 각속도는 VORTEX_K/r 값으로 반지름이 작을수록 빨리 돕니다. 두 항목 모두 현재 값과 목표 값의 차이를 매 프레임 줄여 나가는 계산입니다.

스크롤 속도가 발생·소멸을 정하는 값 — 02·06

02는 |scrollVel|×SPAWN_GAIN으로 초당 생성률을 정해 파티클을 계속 흘려 내보내고, 06은 FLICK_THRESHOLD를 아래에서 위로 넘는 상승엣지에서만 BURST_COUNT개를 한 번에 터뜨립니다. 두 항목 다 생성된 뒤에는 FRICTION 값으로 지수 감쇠하다 LIFE_MS가 지나면 사라집니다.

위치·색이 스크롤 진행률에 직접 묶이는 값 — 04·07

04는 cameraZ를 scrollPos×DOLLY_RANGE로 정해 FOV/(FOV+depth) 투영 스케일이 스크롤을 따라 바뀌고, 07은 d.bx/W에 scrollPos를 더한 지점을 bandColor 띠에서 읽어 색을 정합니다. 둘 다 스크롤 진행률 값 자체가 계산의 입력입니다.

패턴별 코드

01기본 흐름장 — 스크롤 값이 없을 때도 파티클이 기준 속도 값으로 은은하게 위로 떠가는 바탕 흐름

스크롤 값이 0이어도 파티클은 RISE_SPEED·p.speedVar만큼 매 프레임 위로 올라가고, 스크롤 속도 값이 생기면 WIND_GAIN 배만큼 가로 바람 값이 더해집니다. 바람 위에는 FIELD_FREQ로 높이마다 다른 위상을 갖는 사인 흔들림(SWAY_AMP)이 겹쳐, 같은 높이라도 파티클마다 흐르는 방향이 조금씩 달라집니다.

  function step(dtSec) {
    var wind = scrollVel * WIND_GAIN;
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      p.y -= RISE_SPEED * p.speedVar * dtSec;
      var sway = Math.sin(p.y * FIELD_FREQ + p.phase) * SWAY_AMP;
      p.x += (wind + sway) * dtSec;
      if (p.y < -8) spawnAt(p, false);
      if (p.x < -8) p.x = W + 8; else if (p.x > W + 8) p.x = -8;
    }
  }
기본 흐름장 — 스크롤 값이 없을 때도 파티클이 기준 속도 값으로 은은하게 위로 떠가는 바탕 흐름
새 창에서 열기

01 · 기본 흐름장 — 스크롤 값이 없을 때도 파티클이 기준 속도 값으로 은은하게 위로 떠가는 바탕 흐름 — 측정 누적면적 15.706% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RISE_SPEEDWIND_GAINFIELD_FREQ

02속도 기반 생성 — 스크롤 속도 값이 빨라질수록 새로 생성되는 파티클의 빈도 값이 늘어나는 방식

sampleScroll이 매 scroll 이벤트마다 구한 |scrollVel| 값에 SPAWN_GAIN을 곱해 초당 생성률을 정하고, spawnAcc 누적값이 1을 넘을 때마다 파티클 하나를 스크롤 속도×LAUNCH_GAIN 방향으로 쏩니다. 생성된 파티클은 FRICTION 값으로 지수 감쇠하다 LIFE_MS가 지나면 배열에서 사라집니다.

  function step(now, dtSec) {
    var rate = Math.min(140, Math.abs(scrollVel) * SPAWN_GAIN);
    spawnAcc += rate * dtSec;
    while (spawnAcc >= 1) { spawnOne(now); spawnAcc -= 1; }

    var decay = Math.exp(-FRICTION * dtSec);
    for (var i = particles.length - 1; i >= 0; i--) {
      var p = particles[i];
      p.vx *= decay; p.vy *= decay;
      p.x += p.vx * dtSec;
      p.y += p.vy * dtSec;
      if (now - p.born > LIFE_MS) particles.splice(i, 1);
    }
  }
속도 기반 생성 — 스크롤 속도 값이 빨라질수록 새로 생성되는 파티클의 빈도 값이 늘어나는 방식
새 창에서 열기

02 · 속도 기반 생성 — 스크롤 속도 값이 빨라질수록 새로 생성되는 파티클의 빈도 값이 늘어나는 방식 — 측정 누적면적 8.292% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPAWN_GAINLAUNCH_GAINFRICTION

03방향 반전 — 스크롤 방향 값이 아래에서 위로 바뀌면 파티클 흐름의 방향 값도 즉시 반대로 바뀌는 방식

각 파티클은 레인마다 고정된 부호 laneSign을 갖고, 실제 가로 속도는 이 부호에 스크롤 방향 부호 dirSign을 곱한 값입니다. 스크롤 방향이 아래에서 위로 바뀌는 순간 dirSign이 반전돼 모든 레인의 엇갈림이 통째로 뒤집힙니다.

  function step(dtSec) {
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      p.y -= RISE_SPEED * dtSec;
      p.x += laneSign(p.lane) * dirSign * LANE_SPEED * dtSec;
      var laneMin = p.lane / LANES * W, laneMax = (p.lane + 1) / LANES * W;
      if (p.x < laneMin - 6) p.x = laneMin - 6; else if (p.x > laneMax + 6) p.x = laneMax + 6;
      if (p.y < -8) spawnAt(p, false);
    }
  }
방향 반전 — 스크롤 방향 값이 아래에서 위로 바뀌면 파티클 흐름의 방향 값도 즉시 반대로 바뀌는 방식
새 창에서 열기

03 · 방향 반전 — 스크롤 방향 값이 아래에서 위로 바뀌면 파티클 흐름의 방향 값도 즉시 반대로 바뀌는 방식 — 측정 누적면적 13.023% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

laneSigndirSignLANE_SPEED

04깊이 원근 레이어 — 파티클마다 다른 깊이 값을 가져, 같은 스크롤 이동량이 깊이 값에 따라 서로 다른 거리로 적용되는 방식

cameraZ 값이 scrollPos×DOLLY_RANGE로 정해지고, 각 파티클은 FOV/(FOV+depth) 스케일로 투영됩니다. paint는 카메라에서 먼 파티클부터 그리는 화가 알고리즘으로 겹침 순서를 맞추고, 카메라를 지나친 파티클은 NEAR 값 기준으로 다시 가장 먼 자리에 배치됩니다.

  function step(dtSec) {
    var camZ = cameraZ();
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      if (p.z - camZ < NEAR) spawnAt(p, false); // 카메라를 지나친 입자는 가장 먼 자리로 재배치한다
    }
  }
깊이 원근 레이어 — 파티클마다 다른 깊이 값을 가져, 같은 스크롤 이동량이 깊이 값에 따라 서로 다른 거리로 적용되는 방식
새 창에서 열기

04 · 깊이 원근 레이어 — 파티클마다 다른 깊이 값을 가져, 같은 스크롤 이동량이 깊이 값에 따라 서로 다른 거리로 적용되는 방식 — 측정 누적면적 13.938% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FOVDOLLY_RANGEcameraZ

05정지 가라앉음 — 스크롤이 멈추는 순간 파티클이 중력 값의 영향을 받아 서서히 아래로 가라앉는 방식

목표 변위 target은 scrollVel×DRIVE_GAIN이고, 각 점은 STIFFNESS·SETTLE_DAMP 스프링으로 그 목표를 매 프레임 뒤쫓습니다. 스크롤이 멈춰 목표 값이 0으로 수렴하면 점들이 가로로 제자리를 찾아가고, 그 사이 idleMs 값이 쌓이는 만큼 잔광 같은 투명도만 옅어집니다.

  function step(dtSec) {
    var target = scrollVel * DRIVE_GAIN;
    for (var i = 0; i < dots.length; i++) {
      var d = dots[i];
      var acc = (target - d.dispX) * d.stiff - d.velX * SETTLE_DAMP;
      d.velX += acc * dtSec;
      d.dispX += d.velX * dtSec;
    }
  }
정지 가라앉음 — 스크롤이 멈추는 순간 파티클이 중력 값의 영향을 받아 서서히 아래로 가라앉는 방식
새 창에서 열기

05 · 정지 가라앉음 — 스크롤이 멈추는 순간 파티클이 중력 값의 영향을 받아 서서히 아래로 가라앉는 방식 — 측정 누적면적 8.342% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STIFFNESSSETTLE_DAMPtriangleHold

06플릭 버스트 — 짧은 시간 안에 스크롤 값이 크게 뛰면 그 지점에서 파티클이 순간적으로 흩뿌려지는 방식

prevAbsVel과 FLICK_THRESHOLD를 매 scroll 이벤트마다 비교해, 속도 절대값이 문턱을 아래에서 위로 넘는 상승엣지에서만 burst()가 한 번 호출됩니다. burst는 BURST_COUNT개의 파티클을 중심에서 사방으로 발사해 FRICTION 값으로 감쇠시키고, LIFE_MS가 지나면 사라집니다.

  function burst(now) {
    burstCount++;
    for (var i = 0; i < BURST_COUNT; i++) {
      var ang = rng() * Math.PI * 2;
      var speed = BASE_SPEED + rng() * SPEED_VAR;
      particles.push({
        x: W / 2, y: H / 2,
        vx: Math.cos(ang) * speed,
        vy: Math.sin(ang) * speed,
        r: 1.2 + rng() * 2.2,
        born: now
      });
    }
    if (particles.length > MAX_PARTICLES) particles.splice(0, particles.length - MAX_PARTICLES);
  }
플릭 버스트 — 짧은 시간 안에 스크롤 값이 크게 뛰면 그 지점에서 파티클이 순간적으로 흩뿌려지는 방식
새 창에서 열기

06 · 플릭 버스트 — 짧은 시간 안에 스크롤 값이 크게 뛰면 그 지점에서 파티클이 순간적으로 흩뿌려지는 방식 — 측정 누적면적 3.719% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FLICK_THRESHOLDBURST_COUNTflickWave

07구간별 색상 전환 — 스크롤 진행률 값이 구간을 넘어설 때마다 파티클의 색상 값이 다음 구간 색으로 바뀌는 방식

각 점의 색은 bandColor 함수가 BAND_A·BAND_B·BAND_C 세 앵커 색을 자체 lerpRGB로 이어 만든 띠에서, d.bx/W 값에 scrollPos를 더한 지점을 읽어 정합니다. 위치는 BOB_FREQ·BOB_AMP로 매 프레임 사인 흔들림만 더해질 뿐, 색을 정하는 값은 스크롤 진행률 하나입니다.

  function lerpRGB(c1, c2, t) {
    return { r: c1.r + (c2.r - c1.r) * t, g: c1.g + (c2.g - c1.g) * t, b: c1.b + (c2.b - c1.b) * t };
  }
  function bandColor(t) {
    t = ((t % 1) + 1) % 1;
    var c = t < 0.5 ? lerpRGB(BAND_A, BAND_B, t * 2) : lerpRGB(BAND_B, BAND_C, (t - 0.5) * 2);
    return 'rgb(' + (c.r | 0) + ',' + (c.g | 0) + ',' + (c.b | 0) + ')';
  }
구간별 색상 전환 — 스크롤 진행률 값이 구간을 넘어설 때마다 파티클의 색상 값이 다음 구간 색으로 바뀌는 방식
새 창에서 열기

07 · 구간별 색상 전환 — 스크롤 진행률 값이 구간을 넘어설 때마다 파티클의 색상 값이 다음 구간 색으로 바뀌는 방식 — 측정 누적면적 7.906% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

bandColorBOB_FREQBOB_AMP

08소용돌이 흡인 — 스크롤 위치 값이 특정 지점에 가까워질수록 그 지점 소용돌이의 흡인력 값이 커지는 방식

목표 반지름은 restR×(1-scrollPos×REST_SHRINK)로 정해지고, 각 파티클의 반지름 p.r은 PULL_RATE 완화율로 그 목표를 뒤쫓습니다. 각속도는 VORTEX_K/r 값(반지름이 작을수록 빨리 돎)으로 계산돼, spinDir 값에 따라 스크롤 방향으로 회전합니다.

  function step(dtSec) {
    var target = restR * (1 - scrollPos * REST_SHRINK);
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      p.r += (target - p.r) * PULL_RATE * dtSec;
      var angVel = VORTEX_K / Math.max(p.r, 8);
      p.theta += angVel * dtSec * 0.01 * spinDir;
    }
  }
소용돌이 흡인 — 스크롤 위치 값이 특정 지점에 가까워질수록 그 지점 소용돌이의 흡인력 값이 커지는 방식
새 창에서 열기

08 · 소용돌이 흡인 — 스크롤 위치 값이 특정 지점에 가까워질수록 그 지점 소용돌이의 흡인력 값이 커지는 방식 — 측정 누적면적 7.172% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

VORTEX_KPULL_RATEREST_SHRINK

09노이즈 흐름 왜곡 — 스크롤 값이 2D 노이즈 필드의 오프셋 값을 밀어, 파티클 경로가 매끈하게 휘는 흐름으로 바뀌는 방식

fieldAngle은 좌표에 NOISE_SCALE·시간·scrollPos×SCROLL_WARP를 섞은 값을 PERM_SIZE 256 순열 기반 valueNoise(hash2·smooth 보간)에 넣어 각도로 바꿉니다. 각 점은 그 각도로 BASE_SPEED×FLOW_SPEED×speedGain만큼 이동해, 스크롤 속도가 빠를수록 흐름이 커지되 경로 자체는 노이즈 결을 따라 매끈하게 휩니다.

  function fieldAngle(x, y, now) {
    var nx = x * NOISE_SCALE + now * TIME_GAIN + scrollPos * SCROLL_WARP;
    var ny = y * NOISE_SCALE;
    return valueNoise(nx, ny) * Math.PI * 4;
  }

  function step(now, dtSec) {
    var speedGain = 1 + Math.min(2.2, Math.abs(scrollVel) / 180);
    for (var i = 0; i < dots.length; i++) {
      var d = dots[i];
      var ang = fieldAngle(d.x, d.y, now);
      var speed = BASE_SPEED * FLOW_SPEED * speedGain;
      d.x += Math.cos(ang) * speed * dtSec;
      d.y += Math.sin(ang) * speed * dtSec;
      if (d.x < 0) d.x += W; else if (d.x > W) d.x -= W;
      if (d.y < 0) d.y += H; else if (d.y > H) d.y -= H;
    }
  }
노이즈 흐름 왜곡 — 스크롤 값이 2D 노이즈 필드의 오프셋 값을 밀어, 파티클 경로가 매끈하게 휘는 흐름으로 바뀌는 방식
새 창에서 열기

09 · 노이즈 흐름 왜곡 — 스크롤 값이 2D 노이즈 필드의 오프셋 값을 밀어, 파티클 경로가 매끈하게 휘는 흐름으로 바뀌는 방식 — 측정 누적면적 13.367% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

valueNoiseSCROLL_WARPNOISE_SCALE

값이 위치와 색을 바꾸는 곳 — 속도 값·깊이 값·위치 값 기준

아홉 항목을 다시 세 축으로 겹쳐 보면 각자 다른 값이 위치와 색을 정합니다. 속도 값이 기준인 쪽은 02(생성 빈도)·06(버스트 발생)처럼 |scrollVel|이 얼마나 큰지가 그 프레임에 파티클이 새로 생기는지를 정하고, 깊이 값이 기준인 쪽은 04 하나로 파티클마다 다른 z 값이 같은 스크롤 이동량을 서로 다른 화면 거리로 바꿉니다. 위치 값이 기준인 쪽은 07(색상 띠 좌표)·08(소용돌이 흡인력)처럼 scrollPos 자체가 그 프레임의 색이나 힘을 직접 정합니다.

나머지 01·03·05·09는 이 세 기준과 무관하게 자기만의 값을 씁니다. 01은 상승 흐름장의 바람 값, 03은 레인 방향 부호 값, 05는 스프링이 뒤쫓는 목표 변위 값, 09는 노이즈 필드의 오프셋 값이 각자 위치를 정하는 기준입니다.

어디서 깨지는가 — 빠른 스크롤, 탭 전환, reduced-motion

06은 스크롤 속도가 FLICK_THRESHOLD 근처에서 빠르게 오르내리면 상승엣지 판정이 짧은 간격으로 계속 참이 돼 burst()가 연달아 불리고, 그때마다 BURST_COUNT개가 쌓여 MAX_PARTICLES 220을 넘으면 splice가 가장 오래된 파티클부터 한꺼번에 잘라내 화면이 순간적으로 듬성해집니다.

02와 06은 파티클의 수명 계산이 born 시각과 현재 시각(now)의 실제 차이를 그대로 씁니다. 탭을 전환했다 돌아오면 requestAnimationFrame은 멈췄다 재개되지만 born 값은 실제 시간으로 남아 있어, 복귀한 첫 프레임에 now - p.born이 LIFE_MS를 한꺼번에 넘어 살아 있던 파티클 대부분이 그 프레임에 동시에 사라집니다.

파일을 열어 값을 직접 바꿔 보고 싶다면, 압축 비번 x3fyh6j2으로 아홉 항목의 html·scss·js 전체를 내려받을 수 있습니다.

reduced-motion에서는 아홉 항목 전부 paintStatic()으로 scrollPos 기반 정적 배치만 그립니다. 05는 스프링 계산 대신 scrollPos×6×scrollDir로 구한 고정 변위 하나로 점을 옮기고, 08은 반지름을 목표 반지름 쪽으로 고정 비율 0.3만큼 한 번 당겨 위치를 고정하며, 09는 fieldAngle을 시각 0으로 얼려 구한 각도로 점마다 6px씩만 옮긴 뒤 더는 움직이지 않습니다.

같은 스크롤 계산을 스크롤 트리거 애니메이션 9종 — GSAP 없이 바닐라 JS 엔진으로의 판정과 겹쳐 보면, 그 편은 임계값을 넘었는지 아닌지 온/오프 트리거 자체를 계산하고 이 편은 트리거 이후 캔버스 파티클의 좌표·색 값을 계산한다는 차이가 있습니다. 관성 스크롤 9종 — 멈춘 뒤에도 값이 스스로 줄어드는 아홉 가지 방식과 비교하면 그 편은 스크롤이 멈춘 뒤의 감쇠 자체가 주제이고, 이 편은 감쇠(02·06의 FRICTION)를 아홉 계산 축 중 하나로만 씁니다. 스크롤 스토리텔링 섹션 9종 — 복붙하는 CSS 장면 구성은 섹션 전체 레이아웃이 스크롤에 맞춰 바뀌는 쪽이라, 캔버스 안 점 하나하나의 값을 계산하는 이 편의 아홉 항목과는 계산 단위가 다릅니다.

변형 세 가지와 접근성

변형 값 느낌
노이즈 흐름 속도 늦추기 09 FLOW_SPEED 0.6 → 0.25 흐름 속도를 0.6→0.25로 낮추면 노이즈 필드의 오프셋 값이 천천히 밀려, 파티클 경로가 급하게 휘지 않고 완만하게 흐른다.
정지 가라앉음 감쇠 낮추기 05 SETTLE_DAMP 22 → 10 감쇠를 22→10으로 낮추면 파티클이 목표 값에 바로 멈추지 않고 살짝 지나쳤다가 되돌아오는 오버슛이 남아, 가라앉음이 더 튕기듯 보인다.
버스트 파티클 개수 늘리기 06 BURST_COUNT 40 → 90 개수를 40→90으로 늘리면 플릭 시 흩뿌려지는 파티클 값이 훨씬 촘촘해져 화면이 순간적으로 더 밀도 높게 보인다.

canvas는 아홉 항목 모두 aria-hidden이라 스크린 리더가 그림을 직접 읽지 않고, 대신 뷰포트마다 role="region" aria-label로 무엇을 스크롤하는 영역인지 밝히고 그 옆 힌트 문장이 조작 방법을 글로 설명합니다. Canvas API는 픽셀을 그릴 뿐 접근성 트리에 아무 정보도 남기지 않으므로, 이런 텍스트 대체가 없으면 아홉 장면 모두 스크린 리더에는 빈 화면입니다. scroll 이벤트와 requestAnimationFrame은 reduced-motion 값이 켜지면 아홉 항목 전부 paintStatic()으로 대체돼, 파티클 연출 자체는 사라지지 않고 scrollPos 값만 반영한 정지 그림 하나로 남습니다.

FAQ

스크롤을 멈추면 파티클도 완전히 멈추나요?

아닙니다. 01은 스크롤 값이 0이어도 RISE_SPEED로 계속 위로 흐르고, 05는 목표 변위가 0에 가까워질 뿐 스프링 계산 자체는 계속 돌며, 07·09도 매 프레임 자기 필드 값을 다시 구합니다. 완전히 값이 고정되는 건 reduced-motion을 켰을 때뿐입니다.

zip 안 코드를 그대로 붙이면 css 파티클 배경이 바로 동작하나요?

네, 아홉 항목 모두 외부 라이브러리 없이 canvas·js 값 계산만으로 동작하도록 만들어졌습니다. zip 안 코드를 그대로 붙이면 각 항목의 step·paint 함수까지 포함해 그대로 동작합니다.

아홉 항목 중 계산이 가장 무거운 건 어떤 항목인가요?

04는 매 프레임 파티클 배열을 z 값 기준으로 다시 정렬하는 화가 알고리즘을 쓰고, 09는 파티클마다 valueNoise를 한 번(hash2 네 번) 계산합니다. 나머지 일곱 항목은 파티클마다 사칙연산 몇 줄만으로 다음 위치를 구합니다.

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

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