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

오로라 배경 9종 — 커튼·세로 광선·호수 반사·산 능선 히어로

웹페이지 첫 화면에 까는 움직이는 오로라 배경은 이미지 파일 없이 캔버스가 매 프레임 빛의 띠를 전부 계산해 그리는 밤하늘이다. 오로라를 그라데이션 한 장으로 흉내 내면 벽지처럼 멈춰 보인다.

어두운 무대에서 직접 흔들어 보는 밤하늘 아홉 장면

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

손을 대지 않아도 장면은 혼자 돌고, 칸마다 있는 멈춤 버튼으로 언제든 세울 수 있다. 05 와 09 는 시드를 고정한 가상 포인터가 움직이는데, 05 는 주기가 다른 사인 두 개를 엮은 길을 돌고 09 는 사인 하나로 좌우를 오간다. 사람이 포인터를 올리면 그 자리로 넘어가고 무대를 벗어나면 다시 가상 경로로 돌아간다. 08 은 2,600ms 한 바퀴 안에서 200ms 에 다이얼을 9 로, 1,300ms 에 2 로 옮기는 시연을 돌리고, 그때 손잡이도 같이 움직인다.

순서는 그리는 방식이 한 겹씩 바뀌도록 짰다. 01 은 버퍼 없이 세로 줄을 벡터로 긋는 커튼이고, 02 는 같은 띠를 작은 픽셀 버퍼에서 칸마다 칠하며, 03 은 그 버퍼에 읽을 좌표부터 비튼 노이즈를 칠한다. 04 는 다 그린 하늘을 거꾸로 한 번 더 쓰는 반사, 05 는 칠하기 대신 가루 900개를 흐름장으로 옮기는 입자, 06 은 오로라 세기를 별 자리에서 다시 읽는 표본이다. 07 은 같은 세기를 문구 상자에서 평균 내 막의 짙기로 바꾸고, 08 은 사람이 고른 값을 스프링으로 받으며, 09 는 01 커튼과 04 반사에 산 능선을 더해 한 장면으로 묶는다.

오로라 배경 9종 — 노이즈 커튼·세로 광선·비튼 하늘·호수 반사·포인터 바람·별빛 가림·문구 뒤 대비 막·세기 다이얼·산 능선 히어로

아홉 장면은 캔버스 2D 와 자바스크립트로만 그린다. CSS 쪽 @keyframes·filter: blur 는 0개이고, 라이브러리·WebGL·CDN·이미지 파일도 없다. 시간은 requestAnimationFrame 이 넘긴 간격을 16ms 고정 틱으로 잘라 한 프레임에 최대 6틱, 100ms 까지만 따라잡고, 노이즈가 읽는 시간 좌표는 그렇게 쌓은 simMs 를 1,000 으로 나눈 초다. 캔버스 화소 배율은 Math.min(window.devicePixelRatio || 1, 2) 로 2 에서 멈춘다.

무대 넓이 대비 바뀐 화소를 23번 비교한 표

2초 동안 찍은 24장을 이웃끼리 견준 23번의 변화로 쟀다. 누적면적은 그동안 바뀐 자리를 모두 겹친 넓이가 무대에서 차지한 비율이고, 움직인 프레임은 23번 가운데 화면이 달라진 횟수다.

장면 누적면적 움직인 프레임 강도 평균
01 노이즈 커튼 36.2% 23/23 22.4
02 세로 광선 26.7% 23/23 17.2
03 비튼 하늘 67.9% 23/23 18.4
04 호수 반사 43.7% 23/23 16.2
05 포인터 바람 71.0% 23/23 53.1
06 별빛 가림 34.8% 23/23 22.4
07 대비 막 69.5% 23/23 27.8
08 세기 다이얼 36.0% 23/23 26.2
09 능선 히어로 41.3% 23/23 18.3

아홉 장면 모두 23/23 이라 견줄 때마다 화면이 달라졌다. 05 의 71.0% 와 강도 53.1 이 가장 큰 것은 가루 900개가 무대 전체로 흩어져 흐르고 자취가 7.3틱 만에 절반으로 옅어져, 화면 거의 어디서나 밝기가 바뀌기 때문으로 보인다. 02 가 26.7% 로 가장 작은 것은 광선이 버퍼 100행 가운데 띠 높이 55행 안에서만 켜지고 그 위 밤하늘은 그대로 남기 때문으로 보인다. 07 의 69.5% 는 띠 중심선이 진폭 84px 의 사인에 노이즈를 더해 44~255px 사이를 크게 오르내리며 문구 상자를 지나가는 탓으로 보인다.

숙소 예약 화면부터 날씨 위젯까지, 붙일 자리와 붙이는 법

첫 화면 전체를 맡길 장면은 01·03·09 다. 01 커튼은 첫 겹 중심선이 무대 아래쪽 213~232px 에 깔려 서비스 첫 화면의 제목을 그 위 어두운 하늘에 올리기 좋고, 03 은 회사 소개 랜딩 히어로처럼 경계 없는 색 얼룩이 천천히 섞여야 하는 곳, 09 는 여행 상품 랜딩 첫 화면을 산·하늘·호수로 통째로 채운다. 04 는 숙소 예약 사이트처럼 물가 풍경이 어울리는 화면에, 06 은 천체 관측 행사 안내처럼 별이 주인공인 하늘에, 02 는 음악 앨범 소개처럼 빛줄기가 리듬을 맡는 페이지에 맞는다.

페이지에 오로라 효과를 깔 때 글자를 캔버스에 그리지 않는 것이 이 편의 원칙이다. 07 의 문구는 <p data-role="headline"> 안의 진짜 글자이고, place() 가 문구 상자를 무대 대비 백분율로 앉히며, 캔버스는 aria-hidden="true" 로 그 뒤에 깔린다. 08 을 날씨 앱 밤하늘 위젯으로 쓴다면 범위 입력 대신 받아 온 값을 levelTarget 에 넣어도 같은 스프링이 받아 줄 것으로 보인다.

오로라 그라데이션 한 장은 왜 멈춘 벽지처럼 보이는가?

그라데이션이 바꿀 수 있는 것은 자리와 색뿐

CSS 로 만든 오로라 그라데이션은 색이 놓일 자리를 처음에 정해 두고, 애니메이션은 그 덩어리 전체를 옮기거나 돌리거나 색을 바꾸는 일만 한다. 덩어리 안의 결은 그대로라 한참 보면 같은 무늬가 미끄러지는 것으로 읽히고, 그것이 벽지처럼 멈춘 느낌의 정체로 보인다. 밤하늘 빛의 띠처럼 보이려면 밑단이 또렷하고, 위로 갈수록 세로 줄로 옅어지며, 주름의 생김새가 시간에 따라 새로 생겨야 한다. 셋 다 가로 위치와 시각마다 다른 값이 있어야 해서, 이 편은 그 값을 노이즈 계산으로 만든다.

앞서 다룬 세 오로라와 갈리는 지점

CSS 배경 애니메이션 의 03 은 radial-gradient 3개에 filter: blur(3px) 와 회전 키프레임을 건 색 덩어리였고, 오로라 메시 그라데이션 랜딩 의 01 은 radial-gradient 5개에 blur(28px) 와 표류 키프레임을 얹은 섹션 장식이다. 캔버스 노이즈 지형 의 05 는 옥타브 합 노이즈로 띠를 칠한 캔버스 오로라였지만 항목 하나였다. 이 편은 키프레임과 blur 를 하나도 쓰지 않고, 같은 노이즈 재료로 커튼·광선·반사·흐름·가림·대비까지 아홉 갈래를 나눈다.

노이즈를 겹치고 비트는 계산 — 옥타브 합·도메인 왜곡·회전 흐름장으로 빛의 띠를 만든다

정수 격자 해시에서 옥타브 합까지

재료는 하나다. hash2 가 정수 격자점마다 0~1 값을 주고, noise2 가 점이 든 칸의 네 모서리 값을 3t²−2t³ 곡선으로 섞어 매끄러운 값 노이즈를 낸다. octaves 는 주파수를 2배로 올리고 세기를 절반으로 줄인 노이즈를 n 겹 더한 뒤 세기 합으로 나눠, 큰 굽이 위에 잔물결이 얹힌 값을 0~1 안에 돌려준다. 01 의 중심선은 이 합 3겹으로 높이를 정하고, 주름 밝기는 결을 2.6배 잘게 읽은 2겹 합을 문턱 0.25 위로 펴 1.8 제곱해 밝은 주름과 어두운 틈을 가른다.

읽을 좌표를 미는 왜곡과 기울기를 돌린 흐름

03 의 도메인 왜곡은 노이즈 값을 바꾸지 않고 노이즈를 읽을 자리를 옮긴다. q 두 개를 읽어 p 를 1.6q 만큼 민 자리에서 r 두 개를 읽고, 다시 p 를 1.6r 만큼 민 자리에서 최종 값을 읽으니 옥타브 합 5번 × 3옥타브로 픽셀 하나에 노이즈 15회, 120×75 버퍼 9,000칸이면 프레임마다 135,000회다. 개발 시점(2026-10-09) 헤드리스 측정에서 paintWarp 한 번이 2.07ms 였다.

05 는 퍼텐셜 ψ 의 가로·세로 편미분을 90도 돌려 속도로 쓴다. 기울기를 그대로 쓰면 가루가 언덕 꼭대기나 골짜기 쪽으로 몰리지만, 돌린 흐름은 등고선을 따라 돌기만 해서 한 자리로 들어오는 양과 나가는 양이 맞먹는다. 수치로 재면 회전 흐름장의 평균 발산은 2.27e-06, 같은 퍼텐셜의 기울기장은 0.013 으로 약 5,700배 차이가 나고, 가루가 흐르면서 한곳에 뭉치거나 텅 비는 자리가 생기지 않는 근거가 이 차이로 보인다.

작은 버퍼에 칠하고 늘려 더하기

칸마다 값을 쓰는 02·03·06·07·08 은 화면 크기와 상관없이 작은 버퍼를 쓴다. makeBuffer 가 화면 밖 캔버스와 createImageData 를 준비하고, blitBuffer 가 putImageData 로 버퍼를 올린 다음 imageSmoothingQuality = 'high' 와 'lighter' 합성으로 480×300 무대에 늘려 더한다(createImageData 설명). 버퍼 크기는 02 가 160×100, 03·07·08 이 120×75, 06 이 96×60 이다. 화면을 다시 읽는 getImageData 는 아홉 장면 어디에도 없어서, 07 의 밝기도 자기가 칠한 값으로 계산한다.

01노이즈로 일렁이는 오로라 커튼 띠

세 겹 커튼마다 3px 간격으로 세로 줄을 긋는데, 줄의 아래 끝은 옥타브 합이 정한 중심선이고 위로 갈수록 exp(-RAY_FALLOFF × d) 로 옅어져 38.5px 마다 밝기가 절반이 된다. 줄 하나가 세로 그라데이션 하나로 칠해지는 벡터 획이라 픽셀 버퍼가 필요 없고, 165px 길이 끝에서는 알파 배율이 0.051 까지 내려가 커튼 윗자락이 밤하늘에 녹아든다. 1초 동안 첫 겹 중심선이 가장 많이 움직인 거리는 20.1px 라, 서비스 첫 화면 배경처럼 오래 띄워 둘 화면에서도 출렁임이 느긋하다.

노이즈로 일렁이는 오로라 커튼 띠
새 창에서 열기

01 · 노이즈로 일렁이는 오로라 커튼 띠 — 측정 누적면적 36.213% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CURTAIN_LAYERSNOISE_SCALERAY_FALLOFF
        for (let layer = 0; layer < CURTAIN_LAYERS; layer += 1) {
            const k = CURTAIN_LAYERS > 1 ? layer / (CURTAIN_LAYERS - 1) : 0;
            const layerAlpha = o.alpha * (1 - 0.35 * k);
            for (let x = -STROKE_STEP; x <= w + STROKE_STEP; x += STROKE_STEP) {
                const fold = curtainFold(o, layer, x, t);
                if (fold < 0.02) {
                    continue;
                }
                const yc = curtainCenter(o, layer, x, t);
                const len = o.len * (0.55 + 0.75 * fold) * (1 - 0.25 * k);
                const top = yc - len;
                const g = c.createLinearGradient(0, yc + 6, 0, top);
                g.addColorStop(0, rgba(o.low, 0));
                // 아래 가장자리 6px — 짧게 밝아져 커튼 밑단이 또렷하다
                g.addColorStop(6 / (len + 6), rgba(o.low, layerAlpha * fold));
                for (let s = 1; s <= 4; s += 1) {
                    const d = len * s / 4;
                    g.addColorStop((6 + d) / (len + 6), rgba(mix(o.low, o.high, s / 4), layerAlpha * fold * Math.exp(-RAY_FALLOFF * d)));
                }
                c.fillStyle = g;
                c.fillRect(x, top, STROKE_STEP + 0.6, len + 6);
            }
        }

02픽셀마다 계산해 깜빡이는 세로 광선

160×100 버퍼 16,000칸마다 그 열의 빛 세기와 높이 그라데이션을 곱해 알파로 쓰고, 버퍼를 3배로 늘려 무대에 더하므로 바늘 같은 광선이 열마다 따로 밝아졌다 꺼진다. 열 세기는 빠른 노이즈와 느린 노이즈를 섞어 문턱을 넘긴 뒤 제곱하고, 깜빡임은 열마다 0.6~1.4배 다른 빠르기의 사인이 밝기를 0.85배까지 누른다. 5.2초 장면에서 160열 가운데 151열이 켜져 있어, 음악 앨범 소개 페이지처럼 띠 전체를 빛줄기로 채울 때 맞는다.

픽셀마다 계산해 깜빡이는 세로 광선
새 창에서 열기

02 · 픽셀마다 계산해 깜빡이는 세로 광선 — 측정 누적면적 26.675% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SHADER_WIDTHRAY_FLICKERBAND_HEIGHT
    function rayColumn(col, t) {
        const fast = noise2(col * RAY_GRAIN, t * 0.7, 211);
        const slow = noise2(col * RAY_GRAIN * 0.12 + t * 0.12, 3.5, 223);
        const lit = Math.pow(smooth((fast * 0.6 + slow * 0.7 - 0.42) / 0.5), 2);
        const flick = 1 - RAY_FLICKER * (0.5 + 0.5 * Math.sin(t * FLICKER_SPEED * colRate[col] + colPhase[col]));
        return lit * flick;
    }
    // 띠 아래 가장자리(버퍼 행) — 열을 따라 느리게 물결친다
    function rayBottom(col, t) {
        return SHADER_HEIGHT * (0.8 + (noise2(col * 0.035 + t * 0.15, 7.5, 229) - 0.5) * 0.22);
    }
    // 높이 그라데이션 — 가장자리 밑으로는 2칸 안에 꺼지고, 위로는 띠 높이에 걸쳐 (1 - u)^1.6 로 옅어진다
    function heightProfile(row, bottom) {
        const band = BAND_HEIGHT * SHADER_HEIGHT;
        const d = bottom - row;
        if (d < 0) {
            return Math.max(0, 1 + d / 2);
        }
        const u = d / band;
        return u >= 1 ? 0 : Math.pow(1 - u, 1.6) * (0.55 + 0.45 * Math.exp(-d / 3));
    }

03노이즈를 노이즈로 비틀어 흐르는 부드러운 하늘

warped 는 노이즈를 읽을 좌표를 q 만큼, 다시 r 만큼 1.6배로 밀어 읽으므로 결이 한 방향으로 흐르지 않고 소용돌이친다. 그 값과 r 의 가로 성분, 시간을 더한 자리를 6색 표에서 골라 칠하는데 시간 몫이 초당 0.05 라 색이 20초에 한 바퀴 돈다. 경계 없는 빛 얼룩이 무대 67.9% 를 덮으며 천천히 섞여, 회사 소개 랜딩 히어로처럼 색이 조용히 바뀌어야 하는 배경에 어울린다.

노이즈를 노이즈로 비틀어 흐르는 부드러운 하늘
새 창에서 열기

03 · 노이즈를 노이즈로 비틀어 흐르는 부드러운 하늘 — 측정 누적면적 67.861% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WARP_STRENGTHWARP_OCTAVESHUE_DRIFT
    function warped(x, y, t, out) {
        const f = t * WARP_FLOW;
        const qx = fbm(x + f, y, 301);
        const qy = fbm(x + 5.2, y + 1.3 - f, 302);
        const rx = fbm(x + WARP_STRENGTH * qx + 1.7 - f * 0.7, y + WARP_STRENGTH * qy + 9.2, 303);
        const ry = fbm(x + WARP_STRENGTH * qx + 8.3, y + WARP_STRENGTH * qy + 2.8 + f * 0.8, 304);
        out[0] = qx;
        out[1] = rx;
        return fbm(x + WARP_STRENGTH * rx, y + WARP_STRENGTH * ry, 305);
    }
    // 색표 조회 — 0..1 위치를 표의 이웃 두 색 사이에서 보간한다. 1 을 넘으면 처음으로 돈다
    function paletteAt(u) {
        const n = PALETTE_03.length;
        const p = (((u % 1) + 1) % 1) * n;
        const i = Math.floor(p);
        return mix(PALETTE_03[i % n], PALETTE_03[(i + 1) % n], smooth(p - i));
    }

04줄마다 다른 물결로 하늘을 되비추는 호수

지평선 176px 아래 물 124px 를 2px 줄 62개로 나누고, 줄마다 거울 자리의 하늘 줄을 두 사인의 합만큼 옆으로 밀어 그린다. 깊을수록 진폭이 0.25배에서 1배로 커져 물가에서는 최대 1.09px, 화면 아래에서는 4.30px 흔들리고, 좌우로 벌린 반투명 사본 두 장이 더해져 깊은 물일수록 반사가 흐려진다. 하늘은 화면 밖 캔버스에 한 번만 그려 위에 쓰고 물에 다시 쓰므로, 숙소 예약 사이트 첫 화면처럼 호숫가를 그릴 때 하늘을 두 번 계산하지 않는다.

줄마다 다른 물결로 하늘을 되비추는 호수
새 창에서 열기

04 · 줄마다 다른 물결로 하늘을 되비추는 호수 — 측정 누적면적 43.742% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RIPPLE_AMPRIPPLE_FREQREFLECT_DIM
        for (let y = 0; y < waterH; y += REFLECT_ROW) {
            const depth = y / waterH;
            // 원근 — 가까운(아래) 물일수록 물결이 크고 성기다
            const amp = RIPPLE_AMP * (0.25 + 0.75 * depth);
            const ph = y * RIPPLE_FREQ * (1 - 0.45 * depth);
            const dx = amp * Math.sin(ph + t * RIPPLE_SPEED) + amp * 0.45 * Math.sin(ph * 2.3 - t * RIPPLE_SPEED * 1.4);
            // 거울 — 물가에서 y 만큼 내려간 줄은 하늘에서 y 만큼 위의 줄을 비춘다
            const sy = clamp(horizon - REFLECT_ROW - y, 0, horizon - REFLECT_ROW);
            c.globalAlpha = 0.88 - 0.3 * depth;
            c.drawImage(src, 0, sy * sd, src.width, REFLECT_ROW * sd, dx, horizon + y, W, REFLECT_ROW);
            // 흐리게 — 깊을수록 옆으로 벌린 사본 두 장을 반투명으로 더 겹친다
            const spread = 1 + 4 * depth;
            c.globalAlpha = 0.22 * depth;
            c.drawImage(src, 0, sy * sd, src.width, REFLECT_ROW * sd, dx - spread, horizon + y, W, REFLECT_ROW);
            c.drawImage(src, 0, sy * sd, src.width, REFLECT_ROW * sd, dx + spread, horizon + y, W, REFLECT_ROW);
        }

05포인터가 바람을 일으키는 빛 가루 흐름

빛 가루 900개는 퍼텐셜을 위아래·좌우로 1.5px 씩 벌려 잰 편미분을 90도 돌린 속도로 흐르고, 포인터 둘레에서는 포인터 빠르기에 2.4배와 거리 감쇠를 곱한 바람이 더해진다. 바람은 포인터에서 40px 떨어지면 0.779배, 80px 에서 0.368배, 160px 에서 0.018배로 줄고, 틱마다 검은 막을 알파 0.09 로 덮은 위에 가루를 더해 그려 자취가 띠처럼 남는다. 무대에 초점을 두고 화살표를 누르면 24px 씩 바람을 보내므로, 포트폴리오 인트로 화면처럼 첫 방문자가 손을 대 보게 하고 싶은 자리에 쓴다.

포인터가 바람을 일으키는 빛 가루 흐름
새 창에서 열기

05 · 포인터가 바람을 일으키는 빛 가루 흐름 — 측정 누적면적 71.011% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SWIRL_SCALEPOINTER_WINDDUST_COUNT
    function potential(x, y, t) {
        return octaves(x * SWIRL_SCALE, y * SWIRL_SCALE + t * 0.05, 2, 501);
    }
    // 회전 흐름장 — v = (∂ψ/∂y, -∂ψ/∂x). 퍼텐셜 등고선을 따라 흐르므로 발산이 0 — 가루가 한곳에 뭉치거나 비지 않는다
    function flow(x, y, t, out) {
        const e = CURL_EPS;
        const dPdy = (potential(x, y + e, t) - potential(x, y - e, t)) / (2 * e);
        const dPdx = (potential(x + e, y, t) - potential(x - e, y, t)) / (2 * e);
        out[0] = dPdy / SWIRL_SCALE;
        out[1] = -dPdx / SWIRL_SCALE;
    }

06오로라가 지나가면 숨는 반짝이는 별

별 140개는 저마다 다른 위상과 0.6~1.6배 빠르기의 사인으로 1.78~4.76초마다 반짝이고, 그 자리의 오로라 세기 a 를 다시 계산해 1 - AURORA_OCCLUSION × a 를 곱하므로 띠에 완전히 덮이면 0.15배로 어두워진다. 반지름 1.5px 를 넘는 별 31개는 십자 빛살을 달고, 빛살 길이도 밝기를 따라 줄었다 늘었다 한다. 5초 장면에서 띠 안 별 11개의 평균 밝기 배율이 0.441 이라, 천체 관측 행사 안내 페이지처럼 별이 주인공인 하늘에 빛의 장막을 얹을 때 맞는다.

오로라가 지나가면 숨는 반짝이는 별
새 창에서 열기

06 · 오로라가 지나가면 숨는 반짝이는 별 — 측정 누적면적 34.751% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STAR_COUNTTWINKLE_SPEEDAURORA_OCCLUSION
    function drawStars06(t) {
        for (let i = 0; i < STAR_COUNT; i += 1) {
            const tw = 0.5 + 0.5 * Math.sin(t * TWINKLE_SPEED * starRate[i] + starPhase[i]);
            const a = auroraAt(starX[i], starY[i], t);
            const vis = (0.3 + 0.7 * tw) * (1 - AURORA_OCCLUSION * a);
            if (vis < 0.02) {
                continue;
            }
            ctx.fillStyle = rgba(STAR_RGB, vis);
            ctx.beginPath();
            ctx.arc(starX[i], starY[i], starR[i], 0, TAU);
            ctx.fill();
            // 큰 별은 십자 빛살 — 길이도 밝기를 따라 줄었다 늘었다 한다
            if (starR[i] > 1.5) {
                const L = starR[i] * 4 * vis;
                ctx.fillStyle = rgba(STAR_RGB, vis * 0.5);
                ctx.fillRect(starX[i] - L, starY[i] - 0.4, L * 2, 0.8);
                ctx.fillRect(starX[i] - 0.4, starY[i] - L, 0.8, L * 2);
            }
        }
    }

07문구 뒤 밝기를 재어 스스로 짙어지는 대비 막

버퍼를 칠하면서 문구 상자 340×76px 아래 칸들의 평균색을 내면, 세 틱(48ms)마다 neededAlpha 가 그 색과 막 색을 섞은 휘도가 목표 0.1142 아래로 내려가는 알파를 이분 탐색 14번으로 찾는다. 막의 실제 알파는 매 틱 목표와의 차이의 0.12 만큼만 따라가 8틱(128ms)에 63% 까지 닿고, 밝기는 화면을 다시 읽지 않고 자기가 칠한 버퍼 값과 하늘 식으로 계산한다. 8초 동안 막이 없었다면 3.06:1 까지 떨어졌을 대비를 막이 최저 5.04:1, 평균 7.78:1 로 받쳐, 신제품 출시 랜딩 문구처럼 움직이는 배경 위에 글자를 올릴 때 쓴다.

문구 뒤 밝기를 재어 스스로 짙어지는 대비 막
새 창에서 열기

07 · 문구 뒤 밝기를 재어 스스로 짙어지는 대비 막 — 측정 누적면적 69.529% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SCRIM_TARGET_RATIOSCRIM_ALPHA_MAXSCRIM_SMOOTH
    function neededAlpha(avg) {
        const goal = (luminance(TEXT_RGB) + 0.05) / SCRIM_TARGET_RATIO - 0.05;
        if (luminance(avg) <= goal) {
            return 0;
        }
        let lo = 0;
        let hi = 1;
        for (let k = 0; k < 14; k += 1) {
            const mid = (lo + hi) / 2;
            if (luminance(mix(avg, SCRIM_INK, mid)) > goal) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return Math.min(hi, SCRIM_ALPHA_MAX);
    }
    function itemStep(dt) {
        tickNo += 1;
        if (tickNo % SCRIM_EVERY === 0) {
            scrimTarget = neededAlpha(boxAvg);
        }
        scrimAlpha += (scrimTarget - scrimAlpha) * SCRIM_SMOOTH;
    }

08세기 다이얼을 올리면 스프링으로 번지는 빛 폭풍

<input type="range"> 0~10 값을 10 으로 나눈 목표를 강성 60·감쇠 7 스프링이 따라가고, 그 값으로 띠 높이(0 일 때 30px, 1 일 때 165px)와 흐름 빠르기, 색 이동(최대 0.35)을 함께 바꾼다. 감쇠비가 0.452 라 0.2 에서 0.9 로 올리면 288ms 에 ±2% 띠에 처음 들고, 432ms 에 1.035 까지 19.2% 넘쳤다가, 1,040ms 부터 끝까지 띠 안에 머문다. 빠르기는 flowPhase 에 쌓아 두어 다이얼을 확 돌려도 무늬가 튀지 않으니, 날씨 앱 밤하늘 위젯처럼 값 하나로 하늘의 기세를 보여 줄 때 맞는다.

세기 다이얼을 올리면 스프링으로 번지는 빛 폭풍
새 창에서 열기

08 · 세기 다이얼을 올리면 스프링으로 번지는 빛 폭풍 — 측정 누적면적 36.038% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STORM_LEVEL_DEFAULTLEVEL_STIFFNESSHUE_SHIFT_MAX
    // 스프링 — x 는 지금 세기(0..1, 넘칠 수 있음), v 는 속도
    const lvl = { x: STORM_LEVEL_DEFAULT / LEVEL_MAX, v: 0 };
    let levelTarget = lvl.x;
    // 흐름 위상 — 빠르기 x 시간을 쌓아 둔다(빠르기가 바뀌어도 무늬가 튀지 않는다)
    let flowPhase = 0;
    function springStep(dt) {
        const s = dt / 1000;
        const a = -LEVEL_STIFFNESS * (lvl.x - levelTarget) - LEVEL_DAMPING * lvl.v;
        lvl.v += a * s;
        lvl.x += lvl.v * s;
    }
    function flowSpeed(x) { return lerp(0.25, 2.2, clamp(x, 0, 1.2)); }
    function bandHeight08(x) { return lerp(30, 165, clamp(x, 0, 1.25)); }
    function hueShift08(x) { return HUE_SHIFT_MAX * clamp(x, 0, 1.2); }

09산 능선·오로라·호수 반사를 묶은 시차 히어로

능선 세 겹은 양 끝 높이를 정한 뒤 가운데 점을 이웃 평균에서 흔들고, 흔들림 폭을 0.55배씩 줄이며 6번 나눠 65점으로 만든다. 포인터 위치가 정한 목표로 밀림을 매 틱 8% 씩 따라가 95% 까지 576ms(36틱)가 걸리고, 겹마다 6·14·26px 로 다르게 밀려 가까운 산이 더 크게 움직인다. 01 커튼과 별 70개, 능선을 하늘 캔버스에 그린 뒤 04 의 drawReflection 으로 물에 비추므로 능선까지 호수에 비쳐, 여행 상품 랜딩 첫 화면 하나를 통째로 맡길 수 있다.

산 능선·오로라·호수 반사를 묶은 시차 히어로
새 창에서 열기

09 · 산 능선·오로라·호수 반사를 묶은 시차 히어로 — 측정 누적면적 41.267% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RIDGE_LAYERSRIDGE_ROUGHNESSPARALLAX_LERP
    function midpointRidge(rng, amp) {
        const n = RIDGE_POINTS;
        const a = new Float32Array(n);
        a[0] = (rng() - 0.5) * amp;
        a[n - 1] = (rng() - 0.5) * amp;
        let step = n - 1;
        let spread = amp;
        while (step > 1) {
            const half = step / 2;
            for (let i = half; i < n; i += step) {
                a[i] = (a[i - half] + a[i + half]) / 2 + (rng() - 0.5) * spread;
            }
            step = half;
            spread *= RIDGE_ROUGHNESS;
        }
        return a;
    }
    const ridges = [];
    for (let k = 0; k < RIDGE_LAYERS; k += 1) {
        ridges.push(midpointRidge(rng09, RIDGE_AMP[k]));
    }

큰 화면·밝은 문구·느린 기기 — 픽셀 계산 오로라가 깨지는 곳

목표를 6 으로 세웠는데 최저 5.04 가 나온 까닭

07 의 목표 대비는 SCRIM_TARGET_RATIO 6 인데, 8초(500틱) 동안 잰 실제 최저는 5.04:1 이었다. 막의 목표 알파는 세 틱마다 새로 정해지고 실제 알파는 그 차의 12% 씩만 따라가므로, 밝은 띠가 문구 상자로 빠르게 들어오는 순간에는 막이 한 박자 늦게 짙어진다. 그 8초 동안 최대 알파는 0.348 이었고, 알파가 0.3 을 넘은 틱은 15.6% 였다. 따라가는 비율 SCRIM_SMOOTH 를 키우면 늦음은 줄겠지만 막이 띠를 따라 떨리듯 바뀔 것으로 보여, 이 편은 부드러움 쪽을 골랐다.

평균색 자체도 빈틈이다. 상자 전체의 평균으로 알파를 정하므로 상자 한쪽만 얇게 밝은 띠가 지나가면 평균은 낮게 나오고, 그 줄 위 글자는 목표보다 낮은 대비에 놓일 수 있는 것으로 보인다. 상한 SCRIM_ALPHA_MAX 0.82 도 있다. 지금 색에서는 흰 배경이 와도 필요한 알파가 0.657, 순 민트 배경이 0.602 라 닿지 않지만, 목표 비를 크게 올리거나 글자를 어둡게 바꾸면 상한에 막혀 목표를 못 낼 것으로 보인다.

버퍼를 무대 화소 그대로 키우면 64배

03 버퍼 9,000칸을 무대 실해상도 480×300×배율 2 의 576,000칸으로 키우면 칸 수가 64배가 되고, 개발 시점의 2.07ms 를 그대로 곱하면 한 프레임에 약 130ms 가 든다는 계산이 나온다. 16ms 틱 안에 들 수 없는 수라 02·03·06·07·08 은 버퍼를 작게 고정했고, 그 대가로 큰 화면에서는 늘린 만큼 흐릿해진다. 03 처럼 경계 없는 얼룩은 흐림이 덜 드러나지만, 02 의 바늘 광선은 3배로 늘어난 열 폭이 보일 수 있다.

커튼 줄 긋기·픽셀 버퍼·줄 단위 반사·대비 막을 담은 아홉 장면의 index.html 과 같은 소스에서 만든 리액트 판은 내려받는 압축 파일에 함께 들었고, 그 파일은 85c5k85f 을 넣어야 열린다.

수백 개의 그라데이션과 줄마다 세 번 그리는 반사

01 은 줄 후보 489개 가운데 주름 밝기가 0.02 이상인 줄마다 createLinearGradient 를 새로 만들어, 5.2초 장면에서는 한 프레임에 446개를 만든다. 느린 기기에서는 이 객체들이 메모리 정리 부담이 될 것으로 보이고, STROKE_STEP 3 을 키우면 개수는 줄지만 줄이 듬성해진다. 04 와 09 의 반사는 2px 줄마다 drawImage 를 세 번 불러, 물 높이가 124px 인 04 는 프레임당 186번, 지평선이 196px 로 낮아 물이 104px 인 09 는 156번이다. 표시 크기를 크게 키우면 줄 단위 계단이 보일 수 있다.

자취 얼룩과 화살표가 먹지 않는 순간

05 는 틱마다 검정을 알파 0.09 로 덮어 자취를 지우는데, 8비트 캔버스에서는 어두운 값이 반올림 때문에 0 까지 내려가지 않아 옅은 얼룩이 남을 수 있는 것으로 보인다. 포인터가 무대 밖에서 돌아오는 순간의 큰 이동은 WIND_CAP 12 로 잘라 바람이 튀지 않게 했다. 05·09 의 화살표 키는 무대에 초점이 있을 때만 먹고, 멈춤 버튼에 초점이 있으면 무대가 반응하지 않는다.

노이즈 결을 잘게·광선 깜빡임을 세게·물결을 크게 한 세 가지 변형

이름 값 느낌
노이즈 결 잘게 01 NOISE_SCALE 0.004 → 0.012 노이즈 한 칸이 250px 에서 83px 로 줄어 첫 겹 중심선이 무대 폭에서 5번이 아니라 14번 굽고, 평균 기울기가 0.045 에서 0.246 으로 커져 느긋하던 주름이 잘게 떨리는 불꽃처럼 바뀐다
광선 깜빡임 세게 02 RAY_FLICKER 0.15 → 0.5 깜빡임이 광선을 누르는 최저 배율이 0.85 에서 0.50 으로 내려가고, 켜진 열의 2초 상대 표준편차는 0.450 에서 0.538 로 오른다. 바늘 광선이 열마다 따로 빠르게 깜박인다
호수 물결 크게 04 RIPPLE_AMP 3 → 10 반사 줄의 최대 밀림이 물가 1.09px·화면 아래 4.30px 에서 3.62px·14.32px 로 커져, 또렷하던 커튼 반사가 굵은 물결로 부서진다

깜빡임 세기를 세 배 넘게 올렸는데 상대 표준편차가 약 20% 만 늘어난 것은, 켜진 열의 밝기 변화 대부분을 느린 노이즈가 이미 만들고 있기 때문으로 보인다. 깜빡임은 그 위에 곱해지는 한 겹이라 0.85~1 이던 곱의 범위가 0.50~1 로 넓어져도 전체 흔들림에서 차지하는 몫이 크지 않다. 04 는 진폭에 정비례해 물가와 화면 아래가 똑같이 3.3배 커지므로, 세 손잡이 가운데 결과를 숫자로 미리 가늠하기 가장 쉽다.

움직임 줄이기에서는 멈춘 한 장면으로 — 문구 대비를 지키는 접근성

5.2초에 세운 시계 하나로 함께 멈추는 장면들

prefers-reduced-motion: reduce 가 켜지면 아홉 장면 모두 애니메이션 루프를 시작하지 않고, 시계를 STILL_MS 5,200ms 에 세운 한 장면만 그린다(prefers-reduced-motion 설명). 02 의 깜빡임, 04·09 의 물결, 06 의 반짝임도 같은 시계를 읽으므로 같은 순간에 멈추고, 페이지를 연 채 설정을 바꾸면 change 리스너가 그 순간의 장면으로 세운다. 이때 멈춤 버튼은 숨는다. 헤드리스 점검에서 01·05·08·09 는 300ms 간격으로 찍은 두 장이 같았다.

멈춘 화면에서도 받는 입력

08 은 다이얼을 움직이면 스프링을 건너뛰고 목표 세기를 바로 그리고, 09 는 시차를 목표 자리에 바로 놓으며, 05 는 24틱만 계산한 한 장을 다시 그린다. 01·02·03·04·06·07 은 조작할 컨트롤이 없어 정지 장면만 남는다.

범위 입력·멈춤 버튼·무대 초점

08 의 세기 조절은 캔버스 위 가짜 손잡이가 아니라 「빛 세기」 <label> 이 붙은 <input type="range"> 0~10 이라, 화살표·Home·End 키가 기본 동작대로 값을 바꾼다(range 입력 설명). 멈춤은 aria-pressed 를 가진 <button> 이고, 누르면 이름이 「움직임 다시 켜기」로 바뀐다. 05·09 는 무대가 tabindex="0"·role="group" 으로 초점을 받아 05 는 상하좌우 24px, 09 는 좌우 30px 씩 화살표로 움직인다. 초점 고리는 :focus-visible 에 크림색으로 그리고, 상태 줄은 aria-live="off" 로 있다가 첫 입력에서 polite 로 바뀌며, 그림을 맡은 캔버스는 아홉 장면 모두 aria-hidden="true" 다.

오로라 효과를 사이트 배경에 깔 때 자주 묻는 질문

오로라 애니메이션을 CSS 만으로 만들 수는 없나?

색 덩어리가 천천히 돌거나 흐르는 정도라면 CSS 로 된다. 오로라 메시 그라데이션 랜딩 이 radial-gradient 와 blur, 키프레임만으로 그런 섹션 장식을 만든다. 밑단이 또렷한 커튼, 열마다 따로 깜빡이는 광선, 줄마다 다르게 흔들리는 반사처럼 가로 위치와 시각마다 값이 달라야 하는 결은 계산이 있어야 해서, 이 편은 캔버스로 그렸고 같은 방식의 장면은 캔버스 장면 특수효과 에도 있다.

큰 모니터에서 흐릿한데 버퍼를 키워도 되나?

키울 수는 있지만 칸 수에 비례해 계산이 는다. 03 의 120×75 를 가로세로 2배로 키우면 칸이 4배라, 개발 시점 수치로 한 번 칠하는 데 8ms 를 넘길 것으로 보인다. 01 커튼처럼 버퍼 없이 벡터로 긋는 장면은 화소 배율 2 까지 그대로 그려지므로, 큰 화면의 첫 화면에는 그쪽을 고르는 방법도 있다.

07 의 대비 막만 떼어 다른 배경에 쓸 수 있나?

neededAlpha 는 상자 아래 평균색 하나만 받으므로 함수는 그대로 옮길 수 있다. 다만 07 은 평균색을 화면에서 읽지 않고 자기가 칠한 버퍼와 하늘 식으로 계산하니, 다른 배경에 붙일 때는 그 배경이 상자 아래에 칠한 색의 평균을 따로 구해 넘겨야 한다. 배경을 직접 칠하지 않는 경우라면 화면을 읽어 평균을 내는 단계가 더 필요할 것으로 보인다.

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

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