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

눈 내리는 효과 9종 — 소스 그대로 복붙하는 눈송이부터 쌓이는 눈까지

눈 내리는 효과는 캔버스에 눈송이 입자를 그리고 깊이·바람·쌓임을 계산으로 움직이는 겨울 장면이다. 눈송이는 왜 큰 것이 더 빠르고 선명할까? 01번은 크기를 깊이로 읽어 속도와 흐림을 함께 정하고, 03번은 쌓인 눈이 무너지는 기울기를 지키며, 06번은 가로등 빛 원뿔 안에서만 눈을

눈은 떨어지는 점이 아니라 깊이를 가진 입자다 — 눈 내리는 효과는 크기 하나로 속도·흐림·흔들림을 정하는 데서 시작한다

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

아홉 장면은 같은 눈 입자를 쓰고 그 값을 읽는 방식만 다르다. 01·02 는 눈이 어떻게 떨어지는지, 03·05·07 은 닿은 눈이 어디에 어떻게 남는지, 04·06 은 눈과 빛이 만드는 질감을 다룬다. 08 은 속도를 줄무늬로 바꿔 시야를 닫고, 09 는 앞의 계산을 한 화면에 겹친다.

큰 눈이 빠르고 또렷한 이유

01 은 눈송이 반지름 크기 s 를 S_MIN 1.1 과 S_MAX 5.2 사이에서 0~1 의 깊이 z 로 바꾼다. 그 z 하나가 낙하 속도(0.6~1.4배), 번짐 반지름(최대 3.4px), 가로 흔들림 폭(4.5~19.5px)을 한꺼번에 정하므로 가장 큰 눈은 가장 작은 눈보다 약 2.3배 빨리 떨어진다. 눈마다 값을 따로 고르지 않아도 앞뒤 거리감이 생기는 것이 이 장면의 요점이다.

눈 내리는 효과 소스는 캔버스 2D 하나로 끝난다

아홉 장면은 외부 라이브러리·WebGL·CDN 없이 캔버스 2D 와 requestAnimationFrame 만 쓴다. 난수는 시드를 고정한 생성기라 같은 코드를 같은 값으로 돌리면 같은 눈이 내리고, 한 프레임 시간은 DT_MAX 0.05초로 잘라 쓴다. 파일 한 개를 복사해 붙이면 바로 돌아가므로 눈 내리는 효과 소스를 찾는 사람이 설정 없이 확인하기 좋다.

이웃한 편과 갈리는 지점

점이 떠다니는 장식은 css 파티클 배경 편이 다루고, 비가 땅에 튕기는 장면은 비 효과 편이 다룬다. 이 편은 눈이 닿은 자리에 남는다는 점이 다르다 — 높이 지도에 쌓이고, 유리를 적시고, 우산 위를 미끄러진다. 날씨를 아이콘으로 보여 주는 카드는 날씨 위젯 ui 편, 같은 캔버스 장면 기준선은 캔버스 특수효과 편에 있다.

눈 내리는 효과 9종 — 크기로 깊이를 읽는 눈송이 층·돌풍 장이 휘감는 눈·쌓이며 무너지는 눈더미·창유리에 닿아 녹는 눈·흔들었다 가라앉는 눈 유리구슬·가로등 빛 원뿔 속 눈·우산 위를 미끄러지는 눈·줄무늬로 늘어나는 눈보라·눈 내리는 밤 지붕 히어로 장면

01·02 는 낙하의 계산을 바꾸고 03~05 와 07 은 닿은 뒤의 상태를 저장한다. 06 은 밝기만 바꾸고 눈의 움직임은 그대로 둔다. 08 은 같은 눈을 속도 방향으로 늘이며, 09 는 03 의 쌓임과 06 의 불빛과 01 의 깊이를 함께 올린다.

아홉 장면을 렌더해서 얻은 면적·프레임·강도

장면 누적면적 움직인 프레임 강도 평균
01 깊이 층 눈송이 46.6% 23/23 48.3
02 돌풍 장 41.8% 23/23 59.7
03 쌓이는 눈더미 27.2% 23/23 85.1
04 창유리 녹는 눈 21.6% 23/23 29.4
05 눈 유리구슬 20.5% 23/23 81.0
06 가로등 원뿔 48.4% 23/23 42.0
07 우산 미끄러짐 24.0% 23/23 57.4
08 눈보라 70.2% 23/23 46.8
09 지붕 히어로 27.7% 23/23 46.2

화면 전체에 줄무늬와 흰 막이 깔리는 08 이 70.2% 로 가장 넓고, 구슬 안에서만 눈이 움직이는 05 가 20.5% 로 가장 좁다. 강도 평균은 두 패널에 눈더미가 솟는 03 이 85.1 로 가장 높고 유리 위 물기만 바뀌는 04 가 29.4 로 가장 낮다. 아홉 장면 모두 23/23 프레임에서 움직였다.

01크기로 깊이를 읽는 눈송이 층

겨울 이벤트 랜딩 첫 화면 배경에 깔면 눈 240알이 앞뒤 층으로 나뉘어 떨어지는 모습을 볼 수 있다. speedK 는 깊이 0~1 을 속도 배율 0.6~1.4 로 펴고, blurOf 는 먼 눈일수록 번짐을 키운다. 눈을 크기 순으로 한 번 정렬해 두어 가까운 눈이 위에 그려진다.

크기로 깊이를 읽는 눈송이 층
새 창에서 열기

01 · 크기로 깊이를 읽는 눈송이 층 — 측정 누적면적 46.611% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Z_SPREADS_MAXspeedK
    function depthOf(s) { return (s - S_MIN) / (S_MAX - S_MIN); }
    // 깊이에서 속도 배율 — Z_SPREAD 가 0.5 면 0.6 ~ 1.4 배, 1.0 이면 0.2 ~ 1.8 배
    function speedK(z) { return lerp(1 - 0.8 * Z_SPREAD, 1 + 0.8 * Z_SPREAD, z); }
    function blurOf(z) { return (1 - z) * BLUR * (0.5 + Z_SPREAD); }
    function fallOf(f) { return FALL * speedK(f.z); }
    function reset() {
        rng = seeded(101);
        flakes = [];
        for (let i = 0; i < FLAKES; i += 1) {
            const s = S_MIN + Math.pow(rng(), 1.7) * (S_MAX - S_MIN);
            flakes.push({ bx: rng() * (W + 60) - 30, y: rng() * (H + 20) - 10, s: s, z: depthOf(s), ph: rng() * TAU });
        }
        // 먼 것부터 그린다 — 가까운 눈이 위에 놓이게 크기 순으로 한 번만 정렬
        flakes.sort(function (a, b) { return a.s - b.s; });
    }

02돌풍 장이 휘감는 눈

계절 한정 이벤트 팝업 뒤에 두면 눈이 한 줄로 떨어지다 돌풍 자리에서 휘감겨 올라갔다 가라앉는다. 사인 셋을 합친 값의 기울기를 90도 돌려 속도로 쓰기 때문에 눈이 한 점으로 몰리지도 퍼지기만 하지도 않고 소용돌이로 돈다. 깊이가 얕은 눈은 장 값에 0.3~1.0 몫만 받아 덜 휘감긴다.

돌풍 장이 휘감는 눈
새 창에서 열기

02 · 돌풍 장이 휘감는 눈 — 측정 누적면적 41.808% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GUSTGUST_SPEEDfieldAt
    function fieldAt(x, y, t) {
        let u = 0;
        let v = 0;
        for (const w of WAVES) {
            const c = Math.cos(x * w[0] + y * w[1] + t * w[2] + w[3]) * w[4];
            u += c * w[1];
            v -= c * w[0];
        }
        return [u * 34, v * 34];
    }
    // 돌풍 봉우리 — 시간에 따라 세졌다 가라앉는 몫(0.3 ~ 1)
    function gustAmp(t) { return 0.65 + 0.35 * Math.sin(t * 1.4); }

03쌓이며 무너지는 눈더미

연말 인사 페이지에서 글자 선반 위에 눈이 쌓이게 할 때 쓰면 좋다. 바닥에 닿은 눈은 열마다 둔 높이 지도에 3px 씩 더해지고, 이웃보다 REPOSE 한계를 넘게 높은 열은 넘은 만큼의 절반을 낮은 쪽으로 보낸다. 왼쪽 패널은 기울기 한계를 끈 실패판이고 오른쪽 패널이 한계를 지킨 대책이다.

쌓이며 무너지는 눈더미
새 창에서 열기

03 · 쌓이며 무너지는 눈더미 — 측정 누적면적 27.164% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

REPOSEGRAINrelax
    function relax(p) {
        if (p.limit >= 1e6) {
            return;
        }
        const maxDiff = p.limit * GRAIN;
        for (let pass = 0; pass < 4; pass += 1) {
            for (let c = 0; c < COLS; c += 1) {
                for (let d = -1; d <= 1; d += 2) {
                    const n = c + d;
                    if (n < 0 || n >= COLS) {
                        continue;
                    }
                    const excess = surface(p, c) - surface(p, n) - maxDiff;
                    if (excess > 0 && p.h[c] > 0) {
                        const move = Math.min(p.h[c], excess * 0.5);
                        p.h[c] -= move;
                        p.h[n] += move;
                    }
                }
            }
        }
    }

04창유리에 닿아 녹는 눈

카페 소개 페이지의 창가 배경에 잘 맞는 장면이다. 눈이 유리에 닿으면 4px 칸의 물기를 올리고 방울이 그 아래로 내려가며 칸마다 물기를 남기고 질량을 깎는다. 물기는 EVAP 로 마르므로 자국이 길게 흘러내린 뒤 지워지고, 김서림 층은 물기가 있는 곳만 뚫어 건너편 불빛을 보인다.

창유리에 닿아 녹는 눈
새 창에서 열기

04 · 창유리에 닿아 녹는 눈 — 측정 누적면적 21.645% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EVAPTRAIL_WETLAND_PER_S
        // 중력 방향으로 번짐 — 방울은 질량이 클수록 빠르게 내려가며 지나간 칸을 적시고 질량을 잃는다
        for (const d of drops) {
            if (d.wait > 0) {
                d.wait -= dt;
                continue;
            }
            const v = 14 + 70 * d.m;
            const steps = Math.max(1, Math.round(v * dt / CELL));
            for (let s = 0; s < steps; s += 1) {
                d.y += v * dt / steps;
                d.x += Math.sin(d.y * 0.09 + d.m * 9) * 0.12;
                const c = clamp(Math.floor(d.x / CELL), 0, GC - 1);
                const rr = Math.floor(d.y / CELL);
                if (rr >= 0 && rr < GR) {
                    const i = rr * GC + c;
                    water[i] = Math.min(1, Math.max(water[i], TRAIL_WET));
                    d.m -= TRAIL_COST;
                }
            }
        }

05흔들었다 가라앉는 눈 유리구슬

선물 가게 소개 페이지의 장식 카드로 쓴다. 정해진 시각표(0.25초·3.6초)에 쌓인 눈이 입자로 돌아와 충격을 받고, 공기 저항이 속도를 깎아 입자는 초당 약 49.6px 로 가라앉으며, 바닥 가까이(FLOOR_SLACK 4px 안)에서 속도가 40 아래로 내려온 입자는 바닥 눈더미에 PILE_ADD 1.5px 씩 더해진다. 구슬 벽에서는 바깥으로 향하는 속도 성분만 뒤집고 RESTITUTION 0.45 만큼 남긴다.

흔들었다 가라앉는 눈 유리구슬
새 창에서 열기

05 · 흔들었다 가라앉는 눈 유리구슬 — 측정 누적면적 20.46% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GRAVITYSHAKE_ATRESTITUTION
            // 공기 저항 — 속도에 비례해 깎는다
            const drag = Math.exp(-DRAG * dt);
            g.vx *= drag;
            g.vy *= drag;
            g.x += g.vx * dt;
            g.y += g.vy * dt;
            const dx = g.x - CX;
            const dy = g.y - CY;
            const d = Math.hypot(dx, dy);
            const lim = R - 5 - g.s;
            if (d > lim) {
                // 벽 반사 — 바깥으로 향하는 법선 방향 속도 성분만 뒤집고 RESTITUTION 만큼 남긴다
                const nx = dx / d;
                const ny = dy / d;
                const vn = g.vx * nx + g.vy * ny;
                if (vn > 0) {
                    g.vx -= (1 + RESTITUTION) * vn * nx;
                    g.vy -= (1 + RESTITUTION) * vn * ny;
                }
                g.x = CX + nx * lim;
                g.y = CY + ny * lim;
            }

06가로등 빛 원뿔 속 눈

야간 모드 서비스 소개의 히어로 배경에 어울린다. 눈 한 알의 밝기는 원뿔 각도 몫 × 거리 감쇠 × 앞쪽 산란으로 정하고, 원뿔 밖은 AMBIENT 0.16 만 남아 어둡다. 원뿔 축은 사인으로 좌우로 쓸려서 밝은 눈 무리가 계속 바뀐다.

가로등 빛 원뿔 속 눈
새 창에서 열기

06 · 가로등 빛 원뿔 속 눈 — 측정 누적면적 48.378% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CONE_HALFLAMP_RANGEPHASE_G
    // 눈 한 알의 밝기 — 원뿔 몫 × 거리 감쇠 × (1 + 앞쪽 산란), 원뿔 밖은 AMBIENT
    function brightness(f, axis) {
        const dx = f.x - LAMP_X;
        const dy = f.y - LAMP_Y;
        const d = Math.hypot(dx, dy);
        if (d < 1) {
            return 1;
        }
        let th = Math.abs(Math.atan2(dy, dx) - axis);
        if (th > Math.PI) {
            th = TAU - th;
        }
        const t = th / CONE_HALF;
        const cone = t < 1 ? Math.pow(Math.cos(t * Math.PI / 2), CONE_POWER) : 0;
        const fall = 1 / (1 + (d / LAMP_RANGE) * (d / LAMP_RANGE));
        // 앞쪽 산란 — 빛이 눈을 지나 카메라 쪽으로 가는 정도. 카메라에 가까운 눈(z 큼)일수록, 원뿔 가운데와 바깥에서는 0 이고 반각의 절반 부근에서 가장 크다
        const toward = clamp(f.z * 1.2 - 0.2, 0, 1) * Math.sin(Math.min(1, t) * Math.PI);
        const cosA = clamp(toward, 0, 1);
        const hg = (1 - PHASE_G * PHASE_G) / Math.pow(1 + PHASE_G * PHASE_G - 2 * PHASE_G * cosA, 1.5);
        const scat = SCATTER * hg * 0.14;
        return clamp(AMBIENT + cone * fall * (0.9 + scat), 0, 1.4);
    }

07우산 위를 미끄러지는 눈

날씨 알림 서비스의 눈 오는 날 카드에 놓아 볼 만하다. 타원 우산 면의 법선을 구해 위에서 내려와 면을 지난 눈만 닿게 하고, 접선 속도의 0.6 배만 남겨 가장자리까지 미끄러뜨린다. 옆으로 들어온 눈은 우산 밑을 지나가 그 아래 바닥에는 그늘 타원이 진다.

우산 위를 미끄러지는 눈
새 창에서 열기

07 · 우산 위를 미끄러지는 눈 — 측정 누적면적 23.972% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SLIDE_GFRICTIONnormalAt
                if (Math.abs(f.x - CX) < A) {
                    const sy = surfY(f.x);
                    // 위에서 내려와 면을 지난 눈만 닿는다 — 옆으로 들어온 눈은 우산 밑을 지나간다
                    if (prevY <= sy && f.y >= sy) {
                        const n = normalAt(f.x, sy);
                        f.dir = f.x >= CX ? 1 : -1;
                        // 접선 — 내리막 쪽(아래로 향하는 쪽)으로 잡는다
                        const tx = f.dir * -n[1];
                        const ty = f.dir * n[0];
                        // 법선 성분은 없애고 접선 성분의 0.6 만 속도로 남긴다
                        f.speed = Math.max(0, (f.vx + wind) * tx + f.vy * ty) * 0.6;
                        f.y = sy;
                        f.mode = 1;
                    }
                }

08줄무늬로 늘어나는 눈보라

겨울 스포츠 서비스 소개의 로딩 장면에 쓴다. 눈의 속도 방향으로 속도 × EXPOSURE 길이의 선분을 그어 눈을 줄무늬로 늘이고, 먼 눈은 하늘색에 섞어 시야를 줄인다. 돌풍 정점에서는 흰 막의 투명도가 0.34 까지 올라 화면이 하얗게 막힌다.

줄무늬로 늘어나는 눈보라
새 창에서 열기

08 · 줄무늬로 늘어나는 눈보라 — 측정 누적면적 70.178% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EXPOSUREHIDDEN_SDT_MAX
    function drawStorm(s, ox) {
        ctx.save();
        ctx.beginPath();
        ctx.rect(ox, 0, PANEL_W, H);
        ctx.clip();
        ctx.translate(ox, 0);
        const g = gustAt(sceneT);
        const sky = mix(SKY_TOP, SKY_BOT, 0.5);
        for (const f of s.fl) {
            const vx = (WIND * g + 40) * (0.4 + f.z);
            const vy = FALL * (0.3 + 0.9 * f.z);
            const len = EXPOSURE;
            const c = mix(SNOW, sky, (1 - f.z) * FOG);
            seg(f.x - vx * len, f.y - vy * len, f.x, f.y, 0.7 + f.z * 1.9, c, 0.3 + 0.65 * f.z);
        }
        // 하얗게 막힘 — 돌풍이 셀수록 짙은 막
        ctx.fillStyle = rgba(SNOW, 0.34 * g * g);
        ctx.fillRect(0, 0, PANEL_W, H);
        ctx.restore();
    }

09눈 내리는 밤 지붕 히어로 장면

연말 캠페인 사이트의 히어로로 쓴다. 지붕 모양 높이 지도에 가까운 눈(z 0.45 이상)만 쌓이고, 쌓임은 한 알을 세 열에 0.5·0.25·0.25 로 나눠 더해 지붕 선이 부드럽게 두꺼워진다. 가로등 원뿔 안의 눈만 밝고 먼 눈은 집 뒤를 지나 땅에서 위로 돌아간다.

눈 내리는 밤 지붕 히어로 장면
새 창에서 열기

09 · 눈 내리는 밤 지붕 히어로 장면 — 측정 누적면적 27.677% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ROOF_SLOPEREPOSECONE_HALF
            // 가까운 눈(z ≥ 0.45)만 지붕과 땅에 닿아 쌓인다. 먼 눈은 집 뒤를 지나 바닥에서 위로 돌아간다
            if (f.z >= 0.45) {
                const c = clamp(Math.floor(f.x / GRAIN), 0, COLS - 1);
                if (f.y >= topOf(c)) {
                    h[c] += ADD * 0.5;
                    if (c > 0) {
                        h[c - 1] += ADD * 0.25;
                    }
                    if (c < COLS - 1) {
                        h[c + 1] += ADD * 0.25;
                    }
                    f.y = -4 - rng() * 20;
                    f.x = rng() * (W + 40) - 20;
                }
            }

눈 쌓이는 장면 만들기는 어디에 눈을 얼마나 더해야 자연스러울까?

눈은 어디에나 같은 양으로 쌓이지 않는다. 03 은 눈의 65% 를 세 줄기 열(17·40·64) 근처로 떨어뜨리고 나머지를 화면 전체에 흩뿌려, 줄기 아래는 두껍고 사이는 얇은 눈 쌓이는 장면을 만든다.

글자 선반 위에 쌓을 때

글자 윗면처럼 높이가 다른 선반을 BASE 로 두면 눈은 선반 위 표면(선반 + 쌓인 눈)에서 멈춘다. 03 의 선반은 18px·36px·12px 의 세 덩어리이고, 눈은 각 덩어리 윗면에서 쌓이기 시작한다. 연말 인사 페이지에서는 이 선반을 제목 글자 윗면 높이로 바꿔 쓰면 된다.

지붕에 쌓을 때

09 는 가까운 눈만 쌓이게 하고 먼 눈은 집 뒤를 지나 땅에서 위로 돌려보낸다. 굴뚝처럼 바닥 높이가 6px 넘게 갑자기 달라지는 곳은 벽으로 보고 흘려 보내지 않으므로 눈이 굴뚝 옆에 쌓여 선이 꺾인다. 쌓임 속도를 ADD 4 로 잡았을 때 7초 시연 끝에 지붕 선이 두꺼워진다.

유리·우산처럼 쌓이지 않는 면에서는

04 는 눈이 쌓이는 대신 녹은 물기로 남고, 07 은 우산 면에서 미끄러진 눈이 땅의 ground 열에 쌓이는 식으로 눈의 양이 드러난다. 같은 눈이라도 닿는 면의 성질이 다르면 남는 것이 달라진다는 점이 눈 장면의 설득력을 만든다.

높이 지도로 눈을 쌓고, 기울기를 넘으면 옆으로 흘려보내고, 바람으로 흔든다 — 눈 한 알이 눈더미가 되는 순서

눈 한 알이 눈더미가 되는 길은 네 단계다. 바닥에 닿는 것을 지우지 않고 높이 지도에 더하고, 한계를 넘는 차이를 옆으로 옮기고, 바람으로 흔들고, 먼 것부터 그린다.

단계 하는 일 쓰는 이름 쓰는 장면
1 더하기 닿은 눈을 열 높이에 ADD 만큼 더하고 눈은 위에서 다시 내려보냄 surface·stepPanel 03·07·09
2 흘리기 이웃보다 한계 넘게 높으면 넘은 만큼의 절반을 낮은 쪽으로 relax 03·09
3 흔들기 사인 합의 장 또는 방향키 값을 눈 속도에 더함 fieldAt·WIND_STEP 01·02·05·08
4 그리기 크기 순으로 정렬해 먼 눈부터 그림 flakes.sort 01·02

한계를 넘는 차이만 옮기는 이유

03 은 허용하는 높이 차를 REPOSE × GRAIN 으로 둔다. 1 × 3 이므로 이웃 열보다 3px 넘게 높은 열만 무너지고, 열 폭이 3px 이니 기울기는 45도다. 한계 이하의 차이는 건드리지 않아 눈더미가 완만한 언덕 모양으로 굳는다.

한 번에 옮기지 않고 네 번 되풀이하는 이유

한 열이 무너져 낮은 이웃을 올리면 그 이웃이 다시 한계를 넘을 수 있다. 03 은 한 걸음에 relax 안의 반복을 4번, 09 는 3번 돌려 무너짐이 몇 열 건너까지 번지게 한다. 반복을 늘릴수록 눈더미는 부드러워지지만 열 수만큼 계산이 늘어난다.

먼 눈을 먼저 그리는 이유

01·02 는 눈을 크기 순으로 한 번만 정렬하고 그 순서대로 그린다. 작은 눈이 먼 눈이므로 큰 눈이 위에 놓여 가려지는 모양이 자연스럽고, 프레임마다 정렬하지 않으니 비용도 한 번뿐이다.

눈이 깨지는 순간 — 화면 아래에서 한꺼번에 사라져 뚝 끊기는 눈·쌓인 눈이 솟은 탑처럼 서 버리는 더미·숨은 탭 뒤 한꺼번에 쏟아지는 눈

바닥에서 지우면 눈이 끊긴다

눈이 화면 아래로 나가면 지워 버리는 방식은 눈이 어느 한순간에 한꺼번에 사라진 뒤 위에서 다시 채워지는 틈을 만든다. 03 은 닿은 눈을 높이 지도에 더하고 그 눈을 위에서 다시 내려보내므로 눈 개수가 일정하게 이어진다.

    function stepPanel(p, dt) {
        const wind = userWind() * 30;
        for (const f of p.fl) {
            f.y += f.v * dt;
            f.x += wind * dt;
            const c = clamp(Math.floor(f.x / GRAIN), 0, COLS - 1);
            if (f.y >= GROUND_Y - surface(p, c)) {
                // 바닥에 닿은 눈은 사라지지 않고 높이 지도에 더해진 뒤 위에서 다시 내려온다
                p.h[c] += ADD;
                respawn(p, f);
            }
        }
        relax(p);
    }

눈더미가 탑처럼 서는 판

03 의 왼쪽 패널은 limit 를 1e9 로 두어 relax 가 바로 돌아오는 실패판이다. 눈이 세 줄기 열에만 쌓이므로 그 열이 이웃과 상관없이 솟아 탑이 된다. 오른쪽 패널은 같은 눈을 REPOSE 1 로 흘려 보내 완만한 언덕이 된다.

숨은 탭 뒤에 한꺼번에 쏟아지는 눈

08 의 왼쪽 패널은 시각표(0.3·1.6·2.9·4.2초)마다 숨은 탭 뒤 돌아온 1.3초를 한 걸음에 쓴다. 눈이 생기는 수가 RATE × dt 에 비례하므로 190 × 1.3 으로 약 247알이 위에서 한꺼번에 쏟아지고, DT_MAX 로 0.05초를 상한으로 자른 오른쪽 패널에는 약 10알만 생긴다.

    function update(dt) {
        if (hiddenSeen < HIDDEN_AT.length && sceneT >= HIDDEN_AT[hiddenSeen]) {
            hiddenSeen += 1;
            // 왼쪽은 긴 한 프레임을 그대로 받고, 오른쪽은 DT_MAX 로 자른 만큼만 받는다
            stepStorm(storms[0], HIDDEN_S);
            stepStorm(storms[1], Math.min(HIDDEN_S, DT_MAX));
            return;
        }
        for (const s of storms) {
            stepStorm(s, dt);
        }
    }

실제 탭이 숨으면 아홉 장면이 쓰는 루프는 멈추고, 돌아오면 lastTs 를 −1 로 돌려 숨어 있던 시간이 첫 프레임에 실리지 않게 한다. 그래도 긴 프레임이 들어올 때를 대비해 DT_MAX 를 한 번 더 건다. 압축 파일은 암호 egxx7s4z 로 열고 03 폴더와 08 폴더의 index.html 을 일시정지 버튼으로 세워 두면 깨진 판과 고친 판이 갈라지는 자리를 눈으로 확인할 수 있다.

돌풍을 세게 하면 눈은 얼마나 휘감길까 — 돌풍 세기·깊이 범위·쌓이는 속도를 바꿔 본 차이

이름 바꾼 값 느낌
돌풍을 세게 02 GUST 0.6 → 1.5 눈에 더하는 장 속도 계수가 0.6×200=120 에서 1.5×200=300px/초 로 2.5배가 돼 더 큰 원으로 휘감겨 올라가고 한 줄 낙하가 더 빨리 흩어진다
기울기 한계를 가파르게 03 REPOSE 1 → 3 이웃 열과 허용하는 높이 차가 3px 에서 9px 로 커져 기울기가 45도에서 약 72도로 가팔라지고 눈더미가 더 뾰족하고 높게 서며 무너짐이 드물어진다
깊이 범위를 넓게 01 Z_SPREAD 0.5 → 1.0 속도 배율이 0.6~1.4배에서 0.2~1.8배로 벌어지고 가장 먼 눈의 번짐 반지름이 3.4px 에서 5.1px 로 커져 앞뒤 거리감이 깊어진다

값을 바꾸기 전에 볼 것

02 의 GUST 는 속도 계수이지 길이가 아니다. 1.5 로 올리면 장 속도가 300px/초 까지 오르지만, 돌풍 봉우리 몫(0.3~1.0)과 깊이 몫(0.3~1.0)이 그대로 곱해져 실제 휘감김은 그 아래 값이다. REPOSE 를 3 으로 올리면 03 의 오른쪽 패널이 왼쪽 실패판에 가까워지므로 둘을 나란히 두고 비교해야 한다.

방향키로 바람을 바꾸는 눈 장면 — 지금 눈 내리는 상태를 읽어 주는 문장과 움직임 줄이기의 접근성

손 대신 방향키

아홉 장면의 무대 상자는 포커스를 받고, 왼쪽·오른쪽 방향키를 누를 때마다 바람이 WIND_STEP 0.25 씩 −1~1 사이에서 바뀌며 가로 끌기도 같은 바람을 만든다. Enter 와 Space 는 일시정지를 누르고, 사람이 처음 손대면 자동 시연의 바람이 손을 떼고 상태 줄이 aria-live polite 로 바뀐다. 상태 줄은 03 이면 「한계 없음 몇 px · 한계 1 몇 px」 처럼 지금 눈 내리는 상태를 문장으로 읽어 준다.

움직임 줄이기에서는 눈 내린 풍경 한 장

prefers-reduced-motion: reduce 이면 루프를 세우고 REST_STEPS 만큼 계산한 정지 장면 한 장만 그린다. 낙하·돌풍·쌓임은 움직이지 않고 이미 눈이 내려 쌓인 모습만 남으며, 페이지를 연 채 설정을 바꿔도 change 리스너가 다시 읽어 장면을 바꾼다. 눈 내리는 효과 사이트에 붙일 때는 이 설정을 읽는 분기를 지우지 말고 함께 복사하는 편이 좋다. 규격 문서는 MDN 의 prefers-reduced-motion 에 있다.

눈 내리는 효과 FAQ — css 만으로 만들기·휴대폰 부하·배경으로 깔기

눈 내리는 효과 css 만으로도 같은 장면을 만들 수 있나요

01 처럼 크기별 낙하만이라면 css 애니메이션으로 가능하지만, 이 편의 장면은 눈마다 위치를 저장하고 계산해야 한다. 쌓이는 높이 지도, 우산 위 미끄러짐, 가로등 밝기는 프레임마다 값을 읽어 다시 그리는 방식이라 캔버스와 js 가 맞다.

휴대폰에서 눈 개수를 얼마나 둘 수 있나요

장면마다 FLAKES 상수가 눈 개수이고 01 은 240, 06 은 300 이다. 캔버스 내부는 480×300 에 고정하고 devicePixelRatio 도 2 까지만 쓰니 화면이 커져도 계산량은 같다. 그래도 느리면 FLAKES 를 절반으로 줄이는 것부터 시도하면 된다.

페이지 배경으로 깔아도 되나요

01·06·09 처럼 장식인 장면은 히어로 배경에 두기 좋고, 탭이 숨으면 루프가 멈춘다. 글자를 읽어야 하는 구간에는 FLAKES 를 줄이거나 눈 위에 반투명 판을 한 장 두어 글자 대비를 지키는 쪽이 낫다.

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

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