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

css 비 내리는 효과 9가지 — 창밖 폭풍 캔버스 장면

이 배경 장면에서는 바람 벡터 값 하나가 아홉 층 빗줄기의 기울기와 속도를 동시에 결정한다. 아래 아홉 개는 css 비 내리는 효과를 캔버스 2d 위에서 픽셀 단위로 계산해 그린 장면이며, 파일을 내려받으면 그대로 켜진다.

빗줄기 아홉 겹을 화면에서 바로 확인한다

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

css 비 내리는 효과 9가지 — 층마다 다른 계산이 쌓여 폭풍이 된다

아홉 항목은 순서 없이 나열되지 않는다. 깊이 시차(01)로 층을 나누고, 바람(02)으로 그 층을 흔들고, 창문 물방울(03)과 물보라(04)로 표면 접촉을 계산하고, 세기 순환(05)과 번개(06)로 시간에 따른 강도를 바꾸고, 포인터 바람(07)과 우산(08)으로 입력을 더하고, 마지막 09가 이 계산들을 한 장면에 합친다. 아래에서 바로 켜보는 비 내리는 애니메이션은 각 항목이 무엇을 다시 계산하는지 코드로 직접 확인할 수 있다.

기법별 코드로 보는 빗줄기 계산

아래 아홉 개 코드는 각 데모의 index.html 에서 그대로 가져온 조각이다. 줄 순서를 바꾸거나 들여쓰기를 다시 하지 않고, 실제로 실행되는 순서 그대로 싣는다.

01층마다 굵기와 속도가 다른 빗줄기 시차 배경

streakDepthZ() 가 RAIN_LAYER_COUNT(4)개 층의 인덱스를 0~1 사이 깊이값으로 바꾸고, 그 값으로 층마다 속도를 RAIN_FAR_SPEED_PX_S(40)~RAIN_NEAR_SPEED_PX_S(150) 사이에서, 줄기 개수를 32-z*18 로 정한다. 난수는 seedRand(431011) 로 고정한 값만 써서 매번 같은 배치로 시작한다. 이 장면은 누적면적 63.3%·평균 강도 111.8·23/23 프레임이 움직여 9종 중 가장 넓게 화면을 채운다.

층마다 굵기와 속도가 다른 빗줄기 시차 배경
새 창에서 열기

01 · 층마다 굵기와 속도가 다른 빗줄기 시차 배경 — 측정 누적면적 63.301% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

streakDepthZRAIN_NEAR_SPEED_PX_SRAIN_LAYER_COUNT
  var RAIN_LAYER_COUNT = 4;
  var RAIN_NEAR_SPEED_PX_S = 150;
  var RAIN_FAR_SPEED_PX_S = 40;

  function seedRand(seed) {
    var s = seed >>> 0;
    return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
  }
  var rRand = seedRand(431011);

  function streakDepthZ(layerIndex) {
    return RAIN_LAYER_COUNT <= 1 ? 1 : layerIndex / (RAIN_LAYER_COUNT - 1);
  }

  var streaks = [];
  for (var li = 0; li < RAIN_LAYER_COUNT; li += 1) {
    var z = streakDepthZ(li);
    var count = Math.round(32 - z * 18);
    var speed = RAIN_FAR_SPEED_PX_S + (RAIN_NEAR_SPEED_PX_S - RAIN_FAR_SPEED_PX_S) * z;

02시간에 따라 방향이 바뀌는 바람 빗줄기 배경

windGustAngle() 은 RAIN_WIND_CYCLE_MS(5200) 주기 사인파와 그보다 1.73배 느린 두 번째 사인파를 더해 바람 각도를 매 프레임 다시 계산한다. RAIN_STREAK_COUNT(70)개 줄기가 RAIN_FALL_SPEED_PX_S(130) 로 떨어지면서 이 각도만큼 함께 기운다. 이 장면은 누적면적 57.8%·평균 강도 104.1·23/23 프레임이 움직인다.

시간에 따라 방향이 바뀌는 바람 빗줄기 배경
새 창에서 열기

02 · 시간에 따라 방향이 바뀌는 바람 빗줄기 배경 — 측정 누적면적 57.836% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

windGustAngleWIND_GUST_MAX_DEGRAIN_WIND_CYCLE_MS
  var WIND_GUST_MAX_DEG = 8;
  var RAIN_WIND_CYCLE_MS = 5200;
  var RAIN_STREAK_COUNT = 70;
  var RAIN_FALL_SPEED_PX_S = 130;

  function seedRand(seed) {
    var s = seed >>> 0;
    return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
  }
  var rRand = seedRand(431021);

  function windGustAngle(nowMs) {
    var a = Math.sin(nowMs / RAIN_WIND_CYCLE_MS * Math.PI * 2) * 0.62;
    var b = Math.sin(nowMs / (RAIN_WIND_CYCLE_MS * 1.73) * Math.PI * 2 + 1.3) * 0.38;
    return (a + b) * WIND_GUST_MAX_DEG;

03흘러내리며 서로 합쳐지는 창문 물방울

stepSim() 은 매 프레임 목표 속도(DROPLET_GRAVITY_PX_S 와 질량의 제곱근을 DROPLET_DRAG_COEF 로 나눈 값)를 향해 속도를 보간하고, 좌우로는 사인파를 더해 흘러내리는 경로를 흔든다. 두 물방울 사이 거리가 DROPLET_MERGE_DIST_PX 보다 가까우면 dropletMassMerge() 가 둘을 하나로 합치고 배열에서 하나를 지운다. 이 장면은 누적면적 9.0%·평균 강도 80.9·23/23 프레임으로 9종 중 가장 좁은 면적만 바꾼다 — 물방울 수십 개가 유리창 일부에만 맺히는 장면이라 원래 면적이 작다.

흘러내리며 서로 합쳐지는 창문 물방울
새 창에서 열기

03 · 흘러내리며 서로 합쳐지는 창문 물방울 — 측정 누적면적 8.967% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

dropletMassMergeDROPLET_GRAVITY_PX_SDROPLET_MERGE_DIST_PX
  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    spawnAcc += dtMs * DROPLET_SPAWN_PER_S / 1000;
    while (spawnAcc >= 1 && drops.length < 46) { spawnDroplet(); spawnAcc -= 1; }
    for (var i = 0; i < drops.length; i += 1) {
      var d = drops[i];
      var targetVy = (DROPLET_GRAVITY_PX_S * Math.sqrt(d.mass)) / DROPLET_DRAG_COEF;
      d.vy += (targetVy - d.vy) * Math.min(1, 5 * dtS);
      d.y += d.vy * dtS;
      d.x += Math.sin(nowMs * 0.0012 + d.seed) * 1.6 * dtS;
    }
    // 병합 — 가까운 두 물방울이 겹치면 하나로
    for (var a = 0; a < drops.length; a += 1) {
      for (var b = a + 1; b < drops.length; b += 1) {
        var p1 = drops[a], p2 = drops[b];
        if (!p1 || !p2) continue;
        var dx = p1.x - p2.x, dy = p1.y - p2.y;
        var dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < DROPLET_MERGE_DIST_PX + (p1.r + p2.r) * 0.55) {
          drops[a] = dropletMassMerge(p1, p2);
          drops.splice(b, 1);
          b -= 1;

04떨어진 자리마다 튀는 빗방울 물보라

splashBurst() 는 줄기가 GROUND_Y 에 닿는 순간 SPLASH_PARTICLE_COUNT 개 입자를 위쪽 반원 각도(Math.PI~2×Math.PI)로 흩뿌려 만든다. 입자마다 SPLASH_GRAVITY_PX_S2 값을 매 프레임 다시 더해 포물선을 그리며 떨어지게 한다. 이 장면은 누적면적 48.2%·평균 강도 78.5·23/23 프레임이 움직인다.

떨어진 자리마다 튀는 빗방울 물보라
새 창에서 열기

04 · 떨어진 자리마다 튀는 빗방울 물보라 — 측정 누적면적 48.23% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPLASH_PARTICLE_COUNTsplashBurstSPLASH_GRAVITY_PX_S2
  function splashBurst(x, nowMs) {
    for (var i = 0; i < SPLASH_PARTICLE_COUNT; i += 1) {
      var ang = Math.PI + sRand() * Math.PI; // 위쪽 반원으로 퍼진다
      var speed = 16 + sRand() * 26;
      splashes.push({
        x: x, y: GROUND_Y,
        vx: Math.cos(ang) * speed,
        vy: Math.sin(ang) * speed,
        born: nowMs
      });
    }
  }

  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    for (var i = 0; i < streaks.length; i += 1) {
      var s = streaks[i];
      s.y += s.speed * dtS;
      if (s.y >= GROUND_Y) {
        splashBurst(s.x, nowMs);

05이슬비에서 폭우까지 세기가 오가는 장면

stormIntensity(nowMs) 가 만든 세기값보다 줄기별 문턱(s.thresh)이 낮을 때만 그 줄기를 보이게 해, 화면에 뜬 줄기 수 자체가 세기를 따라 바뀐다. 같은 세기값으로 속도 배율과 바람(wind)도 함께 다시 계산해 화면 전체가 한 번에 약해지고 세진다. 이 장면은 누적면적 60.4%·평균 강도 122.6·23/23 프레임으로 9종 중 평균 강도가 가장 높다.

이슬비에서 폭우까지 세기가 오가는 장면
새 창에서 열기

05 · 이슬비에서 폭우까지 세기가 오가는 장면 — 측정 누적면적 60.446% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stormIntensitySTORM_CYCLE_MSRAIN_SPAWN_RATE_BASE
  function stepSim(dtS, nowMs) {
    var inten = stormIntensity(nowMs);
    var wind = (inten - 0.5) * 20;
    for (var i = 0; i < streaks.length; i += 1) {
      var s = streaks[i];
      var visible = inten >= s.thresh * (1 - RAIN_SPAWN_RATE_BASE / 100);
      if (!visible) continue;
      s.y += s.speed * (0.6 + inten * 0.7) * dtS;
      s.x += wind * dtS;
      if (s.y > CALC_H + 6) { s.y = -6 - rRand() * 8; s.x = rRand() * CALC_W; }
      if (s.x < -6) s.x = CALC_W + 6;
      if (s.x > CALC_W + 6) s.x = -6;
    }
  }

  function draw(nowMs) {

06가지치며 뻗어나가는 번개와 화면 플래시

branchLightning() 은 depth 값이 남은 동안 재귀로 자신을 호출하는 함수로, 가지 하나가 3~4 스텝으로 이뤄지고 스텝마다 각도를 어긋나게 꺾어 경로를 그린다. 30% 확률로 옆가지를 새로 뻗되 depth-1·길이 절반으로 줄여 호출해, 가지가 갈수록 짧고 옅어지게 만든다. 이 장면은 누적면적 73.4%·평균 강도 51.7·23/23 프레임으로 9종 중 평균 강도가 가장 낮다.

가지치며 뻗어나가는 번개와 화면 플래시
새 창에서 열기

06 · 가지치며 뻗어나가는 번개와 화면 플래시 — 측정 누적면적 73.354% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LIGHTNING_FLASH_DECAY_MSbranchLightningLIGHTNING_BRANCH_DEPTH
  function branchLightning(segs, x, y, ang, len, depth) {
    if (depth <= 0 || len < 3) return;
    var steps = 3 + Math.floor(rRand() * 2);
    var cx = x, cy = y, cang = ang;
    for (var i = 0; i < steps; i += 1) {
      cang += (rRand() - 0.5) * 0.9;
      var stepLen = len / steps;
      var nx = cx + Math.sin(cang) * stepLen;
      var ny = cy + Math.cos(cang) * stepLen;
      segs.push({ x0: cx, y0: cy, x1: nx, y1: ny, depth: depth });
      if (depth > 1 && rRand() < 0.3) {
        var side = rRand() < 0.5 ? -1 : 1;
        branchLightning(segs, nx, ny, cang + side * (0.6 + rRand() * 0.5), len * 0.5, depth - 1);
      }
      cx = nx; cy = ny;
    }
  }

07포인터 위치로 바람이 기우는 배경

포인터를 올리면 manual 이 true 로 바뀌며 pointerNormX 값을 windLeanTarget() 에 넘겨 목표각을 구하고, 손을 떼면 virtualPointerSweep() 이 사인파로 자동 스윕한다. leanDeg 는 목표각을 향해 POINTER_WIND_LERP 비율로 매 프레임 보간되며, 9개 항목 중 이 장면만 keydown 코드가 있어 키보드로도 기울기를 조작할 수 있다. 이 장면은 누적면적 16.4%·평균 강도 62.6·23/23 프레임이 움직인다.

포인터 위치로 바람이 기우는 배경
새 창에서 열기

07 · 포인터 위치로 바람이 기우는 배경 — 측정 누적면적 16.376% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

windLeanTargetPOINTER_WIND_LERPvirtualPointerSweep
  var manual = false;
  var pointerNormX = 0;
  var manualReleaseTimer = null;

  function windLeanTarget(normX) {
    return normX * WIND_LEAN_MAX_DEG;
  }

  function virtualPointerSweep(nowMs) {
    return Math.sin((nowMs / SWEEP_PERIOD_MS) * Math.PI * 2);
  }

  var leanDeg = 0;
  function stepSim(dtS, nowMs) {
    var normX = manual ? pointerNormX : virtualPointerSweep(nowMs);
    var target = windLeanTarget(normX);
    leanDeg += (target - leanDeg) * Math.min(1, POINTER_WIND_LERP * (dtS * 60));

08우산 뒤로는 가려지는 빗줄기

umbrellaOcclusion() 은 타원 돔 방정식으로 점이 우산 안쪽인지 매 프레임 판정하고, 안에 들어온 줄기는 지우는 대신 umbrellaBounce() 가 UMBRELLA_BOUNCE_COUNT(5)개의 튐 입자를 새로 만든다. 우산 밖을 지나는 줄기는 판정 없이 그대로 통과해 바닥까지 떨어진다. 이 장면은 누적면적 40.3%·평균 강도 52.7·23/23 프레임이 움직인다.

우산 뒤로는 가려지는 빗줄기
새 창에서 열기

08 · 우산 뒤로는 가려지는 빗줄기 — 측정 누적면적 40.252% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

umbrellaOcclusionUMBRELLA_BOUNCE_COUNTumbrellaBounce
  function umbrellaOcclusion(x, y, umbX) {
    var dx = (x - umbX) / UMBRELLA_RADIUS_PX;
    if (Math.abs(dx) > 1) return false;
    var edgeY = umbY - UMBRELLA_HEIGHT_PX * Math.sqrt(Math.max(0, 1 - dx * dx));
    return y >= edgeY && y <= umbY;
  }

  function umbrellaBounce(x, y, umbX) {
    var dx = (x - umbX) / UMBRELLA_RADIUS_PX;
    var nx = dx;
    for (var i = 0; i < UMBRELLA_BOUNCE_COUNT; i += 1) {
      var spread = (rRand() - 0.5) * 40;
      bounces.push({
        x: x,
        y: y,
        vx: nx * (40 + rRand() * 30) + spread,
        vy: -(30 + rRand() * 40),
        born: 0,
        life: 380 + rRand() * 200
      });
    }

09층·바람·물방울·번개를 한 화면에 합친 폭풍 히어로 장면

HERO_ 접두가 붙은 상수만 따로 선언해 01의 층 시차(HERO_RAIN_LAYER_COUNT)·02의 바람 돌풍(HERO_WIND_GUST_MAX_DEG)·03의 물방울(HERO_DROPLET_*)·06의 번개(HERO_LIGHTNING_*)를 같은 화면에서 동시에 계산한다. 이 상수는 각 단일 항목의 상수와 독립이고, 포인터·키보드 코드는 한 줄도 없어 07의 입력 계산만 빠진 조합이다. 이 장면은 누적면적 73.9%·평균 강도 83.5·23/23 프레임으로 9종 중 누적면적이 가장 넓다.

층·바람·물방울·번개를 한 화면에 합친 폭풍 히어로 장면
새 창에서 열기

09 · 층·바람·물방울·번개를 한 화면에 합친 폭풍 히어로 장면 — 측정 누적면적 73.926% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HERO_WIND_GUST_MAX_DEGHERO_LIGHTNING_FLASH_DECAY_MSHERO_GLASS_TOP
  // 상수는 전부 HERO_ 접두로 이 장면 전용값이며 각 단일 항목의 상수와는 독립이다.
  var root = document.getElementById('rnRoot');
  var canvas = document.getElementById('rnCanvas');
  var ctx = canvas.getContext('2d');
  var wrap = canvas.parentElement;

  var CALC_W = 160, CALC_H = 100;
  var HERO_RAIN_LAYER_COUNT = 4;
  var HERO_RAIN_NEAR_SPEED_PX_S = 150;
  var HERO_RAIN_FAR_SPEED_PX_S = 40;
  var HERO_STREAKS_PER_LAYER = 26;
  var HERO_WIND_GUST_MAX_DEG = 10;
  var HERO_WIND_CYCLE_MS = 5200;
  var HERO_DROPLET_SPAWN_PER_S = 2.2;
  var HERO_DROPLET_GRAVITY_PX_S = 54;
  var HERO_DROPLET_DRAG_COEF = 3;
  var HERO_DROPLET_MERGE_DIST_PX = 2.2;
  var HERO_GLASS_TOP = CALC_H * 0.76;
  var HERO_LIGHTNING_STRIKE_PERIOD_MS = 2600;
  var HERO_LIGHTNING_FLASH_DECAY_MS = 240;
  var HERO_LIGHTNING_BOLT_FADE_MS = 850;

빗줄기 아홉 항목을 실측하면 이런 값이 나온다

항목 누적면적 평균 강도 움직인 프레임
01 층 시차 63.3% 111.8 23/23
02 바람 돌풍 57.8% 104.1 23/23
03 창문 물방울 9.0% 80.9 23/23
04 물보라 48.2% 78.5 23/23
05 세기 순환 60.4% 122.6 23/23
06 번개 플래시 73.4% 51.7 23/23
07 포인터 바람 16.4% 62.6 23/23
08 우산 오클루전 40.3% 52.7 23/23
09 히어로 장면 73.9% 83.5 23/23

값이 폭풍의 세기를 바꾸는 곳 — 바람·간격·번개·튐

비 내리는 배경화면에서 눈에 띄게 다른 느낌을 만드는 값은 네 개로 좁혀진다. 02의 WIND_GUST_MAX_DEG 는 줄기가 기우는 각도 자체를 정해 바람의 세기를 결정하고, 01의 RAIN_LAYER_COUNT 는 층 개수를 늘리거나 줄여 화면 깊이감의 간격을 바꾼다. 06의 LIGHTNING_FLASH_DECAY_MS 는 번개가 번쩍인 뒤 잦아드는 시간을, 04의 SPLASH_PARTICLE_COUNT 는 바닥에 튀는 입자 수를 정해 폭우처럼 보이는 정도를 바꾼다.

어디서 깨지는가 — 동작 줄임 설정·좁은 화면·층이 많을 때

아홉 장면은 전부 matchMedia('(prefers-reduced-motion: reduce)') 로 동작 줄임 설정을 읽어, 켜져 있으면 stepSim(0) 을 수십 번만 미리 돌려 정지 화면 한 장만 그리고 requestAnimationFrame 루프 자체를 시작하지 않는다. 07 포인터 바람 장면만 포인터·키보드 입력을 받고 나머지 여덟 장면은 입력 없이 시간에 따라서만 흘러가며, 좁은 화면에서는 @media (max-width: 340px) 로 캡션 글자 크기만 줄고 캔버스 내부 계산 해상도(CALC_W 160 · CALC_H 100)는 그대로 유지된다. 09 히어로 장면은 층·바람·물방울·번개 네 계산을 한 화면에서 동시에 처리해 층이 많아질수록 프레임마다 계산량도 함께 늘어난다. 아홉 장면의 원본 소스는 zip 압축 파일로 묶여 있고, 여는 비밀번호는 qms3kcmq 이다.

변형 세 가지와 접근성

이름 값 느낌
최대 바람 기울기 각도 조정 02 WIND_GUST_MAX_DEG 8 → 20 값을 줄이면 바람이 세게 불어도 빗줄기가 거의 수직으로 떨어지고, 값을 늘리면 바람이 조금만 불어도 빗줄기가 크게 기울어 떨어진다.
물보라 입자 개수 조정 04 SPLASH_PARTICLE_COUNT 6 → 14 값을 줄이면 물방울이 닿아도 물보라가 거의 안 보이고, 값을 늘리면 바닥 전체가 튀는 입자로 붐벼 폭우처럼 보인다.
번개 플래시 감쇠 시간 조정 06 LIGHTNING_FLASH_DECAY_MS 220 → 500 값을 줄이면 번개가 순간적으로만 번쩍이고 바로 어두워지고, 값을 늘리면 번쩍인 빛이 한동안 화면에 남아 잔광처럼 보인다.

포인터·키보드 입력이 있는 항목은 07 하나뿐이라 방향키나 클릭 없이도 나머지 여덟 장면은 그대로 재생된다. prefers-reduced-motion 값이 켜지면 아홉 장면 모두 정지 화면 한 장만 남기고 루프를 멈춘다.

비 내리는 배경화면 예제는 창문 장면에서 무엇을 계산하는가?

창문에 맺히는 물방울만 놓고 보면 픽셀을 직접 지우고 다시 그리는 계산이 낯설게 느껴질 수 있다. 실제로는 html5 canvas 애니메이션 9종 — 물방울·블랙홀 특수효과 가 먼저 다룬 굴절 계산과 같은 축에 있는 기법으로, 이 장면은 좌표와 병합 판정만 다시 계산하고 픽셀 자체를 왜곡하지는 않는다. 표면 위에서 물체가 흔들리는 계산을 더 보고 싶다면 이미지 물결 효과 9가지 — 캔버스로 계산하는 픽셀 파동 도 같은 캔버스 2d 좌표 계산을 다른 장면에 적용한 사례다.

비 내리는 css 장면을 쓰기 전에 자주 묻는 질문

css 속성만으로 비 내리는 배경을 만들 수 있나?

css 속성만으로는 층마다 다른 속도·병합·굴절 같은 계산을 표현하기 어렵다. 이 아홉 장면은 css 대신 캔버스 2d 위에서 좌표를 매 프레임 다시 계산해 그린다.

빗소리 창문 연출도 포함되어 있나?

아니다. 이 장면들은 소리 없이 창문에 물방울이 맺히고 흘러내리는 시각 계산만 다루며, 오디오 재생 코드는 아홉 항목 어디에도 없다.

화면이 무거워지면 어떻게 줄이나?

동작 줄임 설정을 켜면 아홉 장면 모두 정지 화면 한 장만 남기고 requestAnimationFrame 루프를 멈춘다. 09 히어로 장면처럼 여러 계산을 합친 항목일수록 층 수(HERO_RAIN_LAYER_COUNT 같은 값)를 줄이는 쪽이 직접적인 효과가 있다.

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

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