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

css starfield 9가지 — 별이 깊이를 따라 날아오르는 우주 배경

css starfield 를 별 점 몇 개의 CSS 애니메이션으로만 만들면 깊이가 없고 속도가 일정해 평평하다 — 01번·02번·05번이 원근 투영·층별 이동량·지수 곡선으로 푼다. 아홉 장면 모두 캔버스에 직접 그린 별이라 압축 파일 하나만 열어도 돈다.

아홉 가지 별 배경을 화면에서 바로 날려 본다

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

css starfield 9가지 — 깊이·회전·굴절을 계산해야 별이 살아난다

먼저 밝혀 둘 것이 있다. 이 편의 아홉 장면은 CSS 키프레임이 아니라 캔버스 2D 와 자바스크립트로 그린 별이고, CSS 는 480×300 캔버스 틀과 캡션·버튼 자리, 포커스 외곽선, touch-action: none 을 맡는다. 별 수백 개가 각자 자기 거리로 나눈 위치에 찍혀야 깊이가 생기는데, 그 나눗셈은 키프레임에 적어 두기보다 프레임마다 코드로 계산하는 쪽이 맞다.

아홉 파일은 start() 가 한 글자도 다르지 않다(60~197줄을 해시로 견주면 같다). 16밀리초(FIXED_DT) 단위로 scene.step() 을 부르고, 포인터가 2.4초(IDLE_TICKS 150) 동안 없으면 장면마다 정해 둔 자동 곡선으로 돌아가며, 난수는 Math.random 대신 시드가 있는 lcg 라 새로 고쳐도 별이 같은 자리에 놓인다. 시드는 장면 번호를 따라 450101 부터 450909 까지다.

장면 받는 입력 핵심 계산
01 워프 줄기 포인터 = 소실점, 탭 = 속도 폭발 깊이 z 로 나눈 투영
02 시차 세 겹 포인터 = 시차 방향, 탭 = 흐름 뒤집기 층 깊이로 나눈 이동량
03 노이즈 밤하늘 포인터 가로 = 노이즈 원점, 탭 = 시드 +1 값 노이즈 두 겹
04 색온도 포인터 = 소실점, 탭 = 회전 방향 뒤집기 시선각 코사인
05 점프 탭 = 점프 처음부터 2 의 거듭제곱 가속 곡선
06 하늘 돔 포인터 = 고개, 탭 = 60° 돌리기 yaw·pitch 회전 행렬
07 별자리 포인터 = 별 하나, 탭 = 속도 다시 뽑기 격자 칸 이웃 탐색
08 별똥별 탭 = 포인터 자리에서 한 발 위치 기록과 중력
09 중력 렌즈 포인터 = 렌즈 중심, 탭 = 질량 1.9배 점질량 렌즈 방정식

05 는 포인터 움직임을 읽지 않고 08 은 탭할 때의 포인터 자리만 쓴다. 금지 목록에 있는 Math.random·Date·fetch 같은 호출은 소스를 grep 하면 0건이고 라이브러리·CDN·WebGL 도 없어서 별은 외부 스크립트 없이 그려진다.

css starfield background 를 기법별 코드로 본다

코드 조각은 각 장면 index.html 의 연속한 줄이고 한 조각이 25줄을 넘지 않는다. 처음부터 끝까지 이어진 전체 파일은 뒤에 나오는 압축 파일에 있다.

01가까워질수록 길게 늘어나는 워프 별줄기

별마다 (x, y, z) 를 두고 화면 위치를 소실점 vx·vy 에 x·FOV÷z 를 더해 구하며, 줄기는 지금 z 와 speed·STREAK_LEN 만큼 먼 z 의 두 투영점을 이은 선이라 속도가 곧 길이가 된다. z 가 Z_NEAR 24 아래로 내려오거나 화면 밖 80px 을 넘으면 Z_FAR 1000 에서 다시 놓는다. 측정은 누적면적 57.2%, 움직인 프레임 23/23 이다.

가까워질수록 길게 늘어나는 워프 별줄기
새 창에서 열기

01 · 가까워질수록 길게 늘어나는 워프 별줄기 — 측정 누적면적 57.23% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FOVSPEEDSTREAK_LENSTAR_COUNT
                for (let i = 0; i < stars.length; i += 1) {
                    const s = stars[i];
                    s.z -= speed;
                    const sx = vx + s.x * FOV / s.z;
                    const sy = vy + s.y * FOV / s.z;
                    if (s.z < Z_NEAR || sx < -80 || sx > W + 80 || sy < -80 || sy > H + 80) {
                        place(s, true);
                    }
                }
                for (let i = 0; i < stars.length; i += 1) {
                    const s = stars[i];
                    const k = 1 - s.z / Z_FAR;
                    const zb = Math.min(s.z + speed * STREAK_LEN, Z_FAR * 1.3);
                    ctx.strokeStyle = rgba(s.c, 0.25 + 0.75 * k);
                    ctx.lineWidth = 0.6 + 2.6 * k * k;
                    ctx.beginPath();
                    ctx.moveTo(vx + s.x * FOV / zb, vy + s.y * FOV / zb);
                    ctx.lineTo(vx + s.x * FOV / s.z, vy + s.y * FOV / s.z);
                    ctx.stroke();
                }

02깊이 층마다 다른 속도로 흘러가는 별 세 겹

층의 이동량은 px·PARALLAX÷깊이 + drift÷깊이 이고 깊이가 4·2·1 인 세 층이라, 포인터를 끝까지 밀면 앞 층은 70px 가장 먼 층은 17.5px 어긋난다. 반지름은 DEPTH_SCALE÷깊이^0.8, 밝기는 0.35 + 0.65÷√깊이 로 줄어 먼 층일수록 작고 어두우며 후광은 깊이 1 층에만 붙는다. 측정은 누적면적 52.6%, 움직인 프레임 23/23 이다.

깊이 층마다 다른 속도로 흘러가는 별 세 겹
새 창에서 열기

02 · 깊이 층마다 다른 속도로 흘러가는 별 세 겹 — 측정 누적면적 52.597% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LAYER_COUNTPARALLAXDRIFTDEPTH_SCALE
                for (let li = 0; li < LAYER_COUNT; li += 1) {
                    const d = LAYER_DEPTH[li];
                    const ox = px * PARALLAX / d + drift / d;
                    const oy = py * PARALLAX * 0.6 / d;
                    const ms = mists[li];
                    for (let i = 0; i < ms.length; i += 1) {
                        glow(ctx, wrap(ms[i].x - ox, W + 160) - 80, wrap(ms[i].y - oy, H + 120) - 60, ms[i].r, ms[i].c, 0.07 + 0.05 / d);
                    }
                    const dots = layers[li];
                    const alpha = 0.35 + 0.65 / Math.sqrt(d);
                    for (let i = 0; i < dots.length; i += 1) {
                        const s = dots[i];
                        const sx = wrap(s.x - ox, W);
                        const sy = wrap(s.y - oy, H);
                        if (d === 1) {
                            glow(ctx, sx, sy, s.r * 4, s.c, 0.3);
                        }
                        ctx.fillStyle = rgba(s.c, alpha);
                        ctx.beginPath();
                        ctx.arc(sx, sy, s.r, 0, PI2);
                        ctx.fill();
                    }
                }

03노이즈 장이 별마다 다르게 반짝이게 하는 밤하늘

별 360개의 밝기는 느린 값 노이즈(좌표 × NOISE_SCALE 0.011)와 빠른 값 노이즈(좌표 × 0.5)를 0.65 대 0.35 로 섞어 CONTRAST 2.6 제곱한 값이라, 가까운 별은 함께 번지고 먼 별은 따로 깜박인다. 뒤의 안개는 60×38 픽셀 이미지를 같은 노이즈로 채워 480×300 으로 늘려 그리며 이 장면만 createImageData 를 쓴다. 측정은 누적면적 69.4%, 움직인 프레임 23/23 이다.

노이즈 장이 별마다 다르게 반짝이게 하는 밤하늘
새 창에서 열기

03 · 노이즈 장이 별마다 다르게 반짝이게 하는 밤하늘 — 측정 누적면적 69.392% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

NOISE_SCALETWINKLE_SPEEDCONTRASTSEED
    // 격자 점의 값 — 정수 두 개와 시드를 섞어 0 이상 1 미만으로
    function hash2(ix, iy, seed) {
        let h = Math.imul(ix | 0, 374761393) + Math.imul(iy | 0, 668265263) + Math.imul(seed | 0, 1442695041);
        h = Math.imul(h ^ (h >>> 13), 1274126177);
        return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
    }
    // 2차원 값 노이즈 — 네 격자 점을 부드럽게 보간
    function vnoise(x, y, seed) {
        const ix = Math.floor(x);
        const iy = Math.floor(y);
        const fx = smooth(x - ix);
        const fy = smooth(y - iy);
        return lerp(lerp(hash2(ix, iy, seed), hash2(ix + 1, iy, seed), fx), lerp(hash2(ix, iy + 1, seed), hash2(ix + 1, iy + 1, seed), fx), fy);
    }
                    // 느린 장(이웃이 닮는다)과 빠른 장(별마다 다르다)을 섞어 밝기를 정한다
                    const slow = vnoise(s.x * NOISE_SCALE + ox * 0.05, s.y * NOISE_SCALE + phase, seed);
                    const fast = vnoise(s.x * 0.5 + phase * 7, s.y * 0.5, seed + 1);
                    const v = slow * 0.65 + fast * 0.35;
                    const b = clamp(Math.pow(v * 1.6, CONTRAST), 0, 1);

04속도에 따라 푸르게 쏠리고 붉게 늘어지는 별빛

소실점에서 화면 거리 r 인 별의 시선각 코사인 FOV÷√(r²+FOV²) 를 색온도 k 에 넣어, 가운데는 청백이고 멀어질수록 주황 쪽으로 기운다. k 는 COLOR_TEMP 0.5 에서 SHIFT_GAIN·(cosA − CONE_COS)·(0.5 + 속도비) 만큼 움직이고 속도비가 사인 곡선으로 오르내려 쏠림도 같이 숨 쉰다. 측정은 누적면적 62.4%, 움직인 프레임 23/23 이다.

속도에 따라 푸르게 쏠리고 붉게 늘어지는 별빛
새 창에서 열기

04 · 속도에 따라 푸르게 쏠리고 붉게 늘어지는 별빛 — 측정 누적면적 62.376% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

COLOR_TEMPSHIFT_GAINHEADINGCONE_COS
    // 0 은 주황, 0.5 는 흰빛, 1 은 청백
    function temperature(k) {
        return k < 0.5 ? mix(WARM, WHITE, k * 2) : mix(WHITE, COOL, (k - 0.5) * 2);
    }
                    const sx = vx + s.x * FOV / s.z;
                    const sy = vy + s.y * FOV / s.z;
                    const r = Math.hypot(sx - vx, sy - vy);
                    // 시선과 별 방향 사이 각도의 코사인 — 소실점에서 멀수록 작다
                    const cosA = FOV / Math.hypot(r, FOV);
                    const k = clamp(COLOR_TEMP + SHIFT_GAIN * (cosA - CONE_COS) * (0.5 + ratio), 0, 1);
                    const c = temperature(k);

05멈춰 있다가 빛의 속도로 뛰어드는 점프 곡선

한 주기는 대기 18틱·가속 72틱·섬광 24틱·도착 56틱으로 170틱(2.72초)이고, 가속 구간의 속도는 BASE_SPEED 에서 MAX_SPEED 까지 (2^(JUMP_EASE·p) − 1)÷(2^JUMP_EASE − 1) 만큼 올라간다. 줄기 길이가 같은 speed 에서 나오고 섬광 알파도 FLASH·(1 − q)² 한 식으로 묶인다. 측정은 누적면적 75.4%, 움직인 프레임 23/23 이다.

멈춰 있다가 빛의 속도로 뛰어드는 점프 곡선
새 창에서 열기

05 · 멈춰 있다가 빛의 속도로 뛰어드는 점프 곡선 — 측정 누적면적 75.433% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

JUMP_EASEMAX_SPEEDFLASHHOLD
    // t 틱 시점의 속도. 올라갈 때만 2 의 거듭제곱 곡선이고 나머지는 부드러운 복귀
    function speedAt(t) {
        if (t < HOLD) {
            return BASE_SPEED;
        }
        if (t < HOLD + RAMP) {
            const p = (t - HOLD) / RAMP;
            return BASE_SPEED + (MAX_SPEED - BASE_SPEED) * (Math.pow(2, JUMP_EASE * p) - 1) / (Math.pow(2, JUMP_EASE) - 1);
        }
        if (t < HOLD + RAMP + FLASH_TICKS) {
            const q = (t - HOLD - RAMP) / FLASH_TICKS;
            return lerp(MAX_SPEED, BASE_SPEED, smooth(q));
        }
        return BASE_SPEED;
    }
    function flashAt(t) {
        const q = (t - HOLD - RAMP) / FLASH_TICKS;
        if (q < 0 || q > 1) {
            return 0;
        }
        return FLASH * (1 - q) * (1 - q);
    }

06구면 위의 별을 회전 행렬로 돌려 보는 하늘 돔

별 900개를 구 위에 고르게, 하늘을 가로지르는 띠의 별 520개를 기울어진 큰 원 둘레에 모아 두고 project 가 yaw 행렬과 pitch 행렬을 차례로 곱한 뒤 W/2 + FOV·x÷z 로 찍는다. z 가 NEAR_CUT 0.08 이하인 점은 시야 뒤라 그리지 않고, 위도선 5개와 경선 10개는 점으로만 그려 도는 방향이 보인다. 측정은 누적면적 56.9%, 움직인 프레임 23/23 이다.

구면 위의 별을 회전 행렬로 돌려 보는 하늘 돔
새 창에서 열기

06 · 구면 위의 별을 회전 행렬로 돌려 보는 하늘 돔 — 측정 누적면적 56.859% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

YAW_RATEPITCHFOVSTAR_COUNT
        function project(x, y, z, cy, sy, cp, sp) {
            const x1 = x * cy + z * sy;
            const z1 = -x * sy + z * cy;
            const y2 = y * cp - z1 * sp;
            const z2 = y * sp + z1 * cp;
            return [W / 2 + FOV * x1 / z2, H / 2 - FOV * y2 / z2, z2];
        }
                for (let i = 0; i < stars.length; i += 1) {
                    const s = stars[i];
                    const p = project(s.x, s.y, s.z, cyw, syw, cp, sp);
                    if (p[2] <= NEAR_CUT || p[0] < -6 || p[0] > W + 6 || p[1] < -6 || p[1] > H + 6) {
                        continue;
                    }
                    const r = 0.5 + 1.9 * s.m * s.m * s.m + (i < STAR_COUNT ? 0.3 : 0);
                    const a = 0.35 + 0.65 * clamp(p[2] * 1.2, 0, 1);
                    if (r > 1.5) {
                        glow(ctx, p[0], p[1], r * 5, s.c, 0.35 * a);
                    }
                    ctx.fillStyle = rgba(s.c, a);
                    ctx.beginPath();
                    ctx.arc(p[0], p[1], r, 0, PI2);
                    ctx.fill();
                }

07가까운 별끼리 선으로 이어지는 별자리

별 110개와 포인터가 되는 별 하나를 GRID_CELL(LINK_RADIUS 와 같은 72px) 칸에 번호로 모아 두고, 각 별은 자기 칸을 둘러싼 3×3 칸만 훑어 거리가 72 미만인 짝에 선을 긋는다. 선 투명도는 LINE_ALPHA·(1 − d÷LINK_RADIUS)^1.3 이고 한 별에 붙는 선은 3개(포인터는 6개)까지다. 측정은 누적면적 56.2%, 움직인 프레임 23/23 이다.

가까운 별끼리 선으로 이어지는 별자리
새 창에서 열기

07 · 가까운 별끼리 선으로 이어지는 별자리 — 측정 누적면적 56.17% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LINK_RADIUSMAX_LINKSGRID_CELLLINE_ALPHA
                for (let i = 0; i < nodes.length; i += 1) {
                    nodes[i].n = 0;
                    const cx = clamp(Math.floor(nodes[i].x / GRID_CELL), 0, cols - 1);
                    const cy = clamp(Math.floor(nodes[i].y / GRID_CELL), 0, rows - 1);
                    cells[cy * cols + cx].push(i);
                }
                    for (let gy = Math.max(0, cy - 1); gy <= Math.min(rows - 1, cy + 1); gy += 1) {
                        for (let gx = Math.max(0, cx - 1); gx <= Math.min(cols - 1, cx + 1); gx += 1) {
                            const list = cells[gy * cols + gx];
                            for (let q = 0; q < list.length; q += 1) {
                                const j = list[q];
                                const b = nodes[j];
                                if (j <= i || a.n >= cap) {
                                    continue;
                                }
                                const capB = j === NODE_COUNT ? CURSOR_LINKS : MAX_LINKS;
                                if (b.n >= capB) {
                                    continue;
                                }
                                const d = Math.hypot(a.x - b.x, a.y - b.y);
                                if (d >= LINK_RADIUS) {
                                    continue;
                                }
                                a.n += 1;
                                b.n += 1;
                                const k = 1 - d / LINK_RADIUS;
                                ctx.strokeStyle = rgba(mix(a.c, b.c, 0.5), LINE_ALPHA * Math.pow(k, 1.3));

08꼬리가 점점 옅어지며 휘어 떨어지는 별똥별

별똥별은 틱마다 위치를 기록 배열 앞에 넣어 TRAIL_LEN 44 개 마디까지만 남기고, 마디의 투명도는 (1 − 나이)^1.6 굵기는 6·(1 − 나이) + 0.8 이다. 세로 속도에 틱마다 GRAVITY 0.06 을 더해 경로가 휘며, 배경을 알파 0.8 로만 덮어 잔광이 남는다. 측정은 누적면적 24.5%, 움직인 프레임 23/23 이다.

꼬리가 점점 옅어지며 휘어 떨어지는 별똥별
새 창에서 열기

08 · 꼬리가 점점 옅어지며 휘어 떨어지는 별똥별 — 측정 누적면적 24.526% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TRAIL_LENMETEOR_RATEANGLEGRAVITY
                for (let i = meteors.length - 1; i >= 0; i -= 1) {
                    const m = meteors[i];
                    if (m.live) {
                        m.hist.unshift(m.x, m.y);
                        m.vy += GRAVITY;
                        m.x += m.vx;
                        m.y += m.vy;
                        if (m.x > W + 70 || m.y > H + 70) {
                            m.live = false;
                        }
                    }
                    else {
                        m.hist.pop();
                        m.hist.pop();
                    }
                    if (m.hist.length > TRAIL_LEN * 2) {
                        m.hist.length = TRAIL_LEN * 2;
                    }
                    if (!m.live && m.hist.length === 0) {
                        meteors.splice(i, 1);
                    }
                }
                    for (let k = 0; k < n; k += 1) {
                        const age = k / TRAIL_LEN;
                        const a = Math.pow(1 - age, 1.6);
                        const c = mix(HEAD, TAIL, age);
                        const hx = m.hist[k * 2];
                        const hy = m.hist[k * 2 + 1];
                        ctx.strokeStyle = rgba(c, 0.22 * a);
                        ctx.lineWidth = (7 * (1 - age) + 1) * 2.2;
                        ctx.beginPath();
                        ctx.moveTo(px, py);
                        ctx.lineTo(hx, hy);
                        ctx.stroke();
                        ctx.strokeStyle = rgba(c, a);
                        ctx.lineWidth = 6 * (1 - age) + 0.8;
                        ctx.beginPath();
                        ctx.moveTo(px, py);
                        ctx.lineTo(hx, hy);
                        ctx.stroke();
                        px = hx;
                        py = hy;
                    }

09블랙홀 가까이에서 별빛이 고리로 휘는 중력 렌즈

별이 렌즈 중심에서 b 만큼 떨어져 있으면 상이 (b + √(b²+4M))÷2 와 반대쪽 (√(b²+4M) − b)÷2 두 곳에 맺히고, LENS_MASS 2600 이면 b 가 0 인 별이 반지름 √2600 ≈ 51px 고리로 번진다. 배경 그림은 두고 별 좌표만 옮기며 SOFTEN 5 가 b 가 0 일 때 확대율 식이 0 으로 나뉘는 것을 막는다. 측정은 누적면적 30.9%, 움직인 프레임 23/23 이다.

블랙홀 가까이에서 별빛이 고리로 휘는 중력 렌즈
새 창에서 열기

09 · 블랙홀 가까이에서 별빛이 고리로 휘는 중력 렌즈 — 측정 누적면적 30.934% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LENS_MASSRING_RADIUSFALLOFFSOFTEN
        function image(ctx, bx, by, r, c, m, alpha) {
            const b = Math.hypot(bx, by);
            const bs = Math.sqrt(b * b + SOFTEN * SOFTEN);
            const root = Math.sqrt(bs * bs + 4 * m);
            const thP = (bs + root) / 2;
            const thM = (root - bs) / 2;
            const u = bs / Math.sqrt(m);
            const mag = (u * u + 2) / (2 * u * Math.sqrt(u * u + 4));
            const muP = clamp(mag + 0.5, 1, 9);
            const muM = clamp(mag - 0.5, 0, 9);
            const ux = b > 0.001 ? bx / b : 1;
            const uy = b > 0.001 ? by / b : 0;
            const ang = Math.atan2(uy, ux) + Math.PI / 2;
            const stretchP = clamp(thP / bs, 1, 6);
            ctx.fillStyle = rgba(c, alpha * clamp(Math.pow(muP, FALLOFF) * 0.8, 0.3, 1));
            ctx.beginPath();
            ctx.ellipse(cx + ux * thP, cy + uy * thP, r * stretchP, r * 0.8, ang, 0, PI2);
            ctx.fill();
            if (muM > 0.04) {
                const stretchM = clamp(thM / bs, 0.4, 6);
                ctx.fillStyle = rgba(c, alpha * clamp(Math.pow(muM, FALLOFF) * 0.8, 0, 0.9));
                ctx.beginPath();
                ctx.ellipse(cx - ux * thM, cy - uy * thM, r * Math.max(stretchM, 0.8), r * 0.7, ang, 0, PI2);
                ctx.fill();
            }
        }

별 배경을 어디에 깔지 고르는 기준 — 히어로·로딩·연결 배경

장면마다 어울리는 자리가 다르다. 랜딩 첫 화면이나 히어로에는 01 의 워프와 06 의 하늘 돔이 맞고, 포인터나 스크롤을 따라 깊이가 느껴져야 하는 배경에는 02 의 세 겹 시차, 정지한 밤하늘에 숨을 불어넣을 때는 03, 우주나 SF 장면 색감을 맞출 때는 04 를 쓴다. 로딩이나 인트로 전환에는 05 의 점프가 170틱 한 주기로 도니 HOLD·RAMP 값을 로딩 시간에 맞춰 늘리면 되고, 네트워크나 연결을 말하는 배경은 07, 이벤트나 감성 배경은 08, 한 화면의 시선을 모으는 지점은 09 가 맡는다.

star field animation 을 배경으로 깔 때는 장면마다 한 프레임에 하는 일이 다르다는 점을 먼저 본다. 06 은 별 1420개와 위도선·경선 점 690개를 합쳐 한 프레임에 project 를 2110번 부르고, 03 은 vnoise 를 안개 2280픽셀에 3번씩, 별 360개에 2번씩 해서 7560번 부른다. 이 편에서는 프레임 시간을 따로 재지 않았으니 본문 뒤에 깔 장면은 내 화면에서 먼저 돌려 본다.

같은 사이트의 다른 캔버스 편과 겹치는 부분도 있다. 별을 서로 잡아당기게 하는 쪽은 은하 충돌 시뮬레이션 9종 — 서로 잡아당기는 별들로 나선팔·조석 꼬리를 만드는 캔버스에서 이어지고, 이 편의 09 는 별을 가만히 둔 채 렌즈만 계산한다. html5 canvas 애니메이션 9종 — 물방울·블랙홀 특수효과에서는 01 과 07 이 getImageData 로 픽셀을 읽고 블랙홀 터널인 05 는 읽지 않는데, 여기 아홉 파일에는 그 호출이 0건이라 모두 별 좌표만 옮긴다. 스크롤을 따라 흐르는 파티클 배경은 스크롤 파티클 배경 9종 — 값 하나로 흐름이 달라지는 캔버스 장면이 다룬다.

값이 속도감을 바꾸는 곳 — 속도·연결 반경·렌즈 질량

값을 하나만 바꿔도 장면이 달라지는 곳이 세 군데 있다. 01 의 SPEED, 07 의 LINK_RADIUS, 09 의 LENS_MASS 이고, 아래 숫자는 같은 소스를 노드에서 가짜 2D 컨텍스트로 200틱 돌린 뒤 한 프레임에서 센 시뮬레이션 값이라 브라우저 화면에서 직접 잰 수치와는 구별한다.

속도를 두 배로 올리면 줄기가 두 배 이상 길어진다

SPEED 12 에서 줄기 선분은 280개이고 평균 길이가 5.0px 이다. 24 로 올리면 선분 수는 그대로 280개이고 평균 길이는 11.4px 가 되어, 속도가 두 배일 때 줄기는 두 배를 넘게 자란다.

연결 반경을 넓혀도 선 수는 거의 늘지 않는다

LINK_RADIUS 72 에서 그어진 선은 158개이고 평균 길이는 47.3px 이다. 110 으로 넓히면 선은 161개로 거의 그대로이고 평균 길이만 80.7px 로 늘어나, 별마다 선 3개라는 MAX_LINKS 상한이 수를 붙잡고 있음을 보여 준다.

렌즈 질량이 두 배면 고리 반지름은 1.41배다

LENS_MASS 2600 에서 링 반지름은 51px 이고 그려진 상은 454개다. 5200 으로 올리면 반지름은 72px, 상은 510개가 되며 반지름이 √M 이라 질량이 두 배일 때 약 1.41배가 되는 식과 맞는다.

렌더로 잰 아홉 장면의 값은 아래 표에 모았다. 움직인 프레임은 아홉 장면 모두 23/23 이고 누적면적은 08 이 24.5%, 09 가 30.9% 로 가장 작으며 05 가 75.4%, 03 이 69.4% 로 가장 크다.

항목 누적면적 평균 강도 움직인 프레임
01 워프 줄기 57.2% 47.3 23/23
02 시차 세 겹 52.6% 49.8 23/23
03 노이즈 밤하늘 69.4% 23.0 23/23
04 색온도 62.4% 35.4 23/23
05 점프 곡선 75.4% 65.1 23/23
06 하늘 돔 56.9% 50.1 23/23
07 별자리 56.2% 41.8 23/23
08 별똥별 24.5% 68.4 23/23
09 중력 렌즈 30.9% 83.1 23/23

어디서 깨지는가 — 별 수가 많을 때·좁은 화면·움직임 줄이기 설정

가까운 별의 줄기가 화면을 가로지른다

01·04·05 는 별이 다가올수록 x·FOV÷z 가 커진다. 01 에서 SPREAD 760 인 별이 Z_NEAR 24 까지 오면 계산상 소실점에서 약 6017px 밖에 찍히는데, 화면 밖으로 허용하는 폭(01 80px·04 80px·05 100px)을 넘으면 place(s, true) 로 되돌리는 조건이 그걸 막는다. Z_NEAR 도 01 24·04 22·05 20 으로 장면마다 다르다. 그 조건과 Z_NEAR 검사를 빼면 선 하나가 프레임을 먹을 수 있다.

층을 밀어 넣으면 별이 반대편에서 튀어나온다

02 는 이동량이 깊이로만 나뉘므로 PARALLAX 를 키우면 앞 층 별이 한 번에 W 를 넘어 wrap 으로 반대편에서 나타난다. 흐름을 뒤집을 때 시간에 곱하면 점프가 생겨서 누적 변수 drift 에 dir 만 곱해 더한다.

시드를 바꾸면 하늘이 한 프레임에 갈아엎어진다

03 의 tap 은 seed 를 1 올리고, 해시를 통째로 바꾸기 때문에 이웃 시드의 하늘은 서로 닮지 않는다(보간이 아니다). 안개는 60×38 을 늘린 것이라 devicePixelRatio 2 에서는 가로세로 약 16배로 늘어 뭉개져 보이고, 매 프레임 createImageData 를 한 번 부른다.

시선각을 화면 거리로 근사해서 뒤쪽은 다루지 않는다

04 는 r = 240 인 가장자리에서도 코사인이 0.53(약 58°)까지만 내려간다. 시선 뒤쪽(90° 넘음)은 다루지 않는 근사라서 상태 문구도 「옆과 뒤」가 아니라 「가장자리」이고, 코사인이 0.72(CONE_COS)인 소실점에서 약 145px 지점이 흰빛이다.

점프를 누르면 섬광 도중에도 잘린다

05 는 JUMP_EASE 6 에서 p = 0.5 일 때 오를 폭의 11%((2³−1)÷(2⁶−1) = 0.111)만 올라 속도가 약 9.2 이고 최대는 70 이라, 오르는 구간 대부분이 정지처럼 보이다가 막판에 급히 오른다. tap 은 어느 구간에서든 t 를 HOLD 로 되돌려 섬광 도중에 누르면 갑자기 잘린다.

광각이라 가장자리 별이 늘어난다

06 의 FOV 150 은 가장자리에서 별이 늘어나 보이는 광각이다. 한 바퀴는 2π÷0.012 = 약 524틱(8.4초)이고 yaw 는 계속 쌓이는 값이라 오래 두면 큰 수의 삼각함수를 부르며, 움직임을 줄인 정지 장면에서는 점 격자가 옅게 보인다.

선이 살아남는 쪽이 번호로 정해진다

07 은 별 번호 순서로 먼저 그은 쪽이 MAX_LINKS 칸을 채우므로, 별이 움직일 때 어느 선이 남는지가 번호에 좌우되어 선이 깜박이듯 바뀔 수 있다. GRID_CELL 을 LINK_RADIUS 보다 작게 하면 이웃 칸 밖의 짝을 놓친다.

잔광이 완전한 검정으로 돌아가지 않는다

08 은 배경을 알파 0.8 로만 덮어서 어두운 잔상이 8비트 반올림 한계에 남는다. 꼬리를 unshift 로 앞에 넣는 배열이라 TRAIL_LEN 이 커지면 틱마다 복사 비용이 늘고, 둥근 선 끝이 겹쳐 꼬리가 구슬처럼 보인다.

질량을 키우면 구멍이 그대로이고 별이 밖으로 나간다

09 의 어두운 중심은 RING_RADIUS(소스 상수 LENS_MASS 의 제곱근)에 0.3 을 곱한 약 15px 로 정해지고, 탭으로 질량이 1.9배가 되어도 그릴 때만 링이 커질 뿐 이 구멍은 15px 그대로다. 링이 커지면 바깥 상이 원래 위치에서 멀어져 가장자리 별이 화면 밖으로 나간다.

좁은 화면과 직접 확인하는 법

캔버스는 aspect-ratio 480 / 300 으로 폭만 줄고, 340px 이하에서는 캡션·버튼·상태 글자가 10px 로 줄어든다. 위 경계는 소스의 상수를 하나씩 바꿔 가며 직접 찾아볼 수 있으니, 압축 파일은 비밀번호 2yqn4mvf 을 넣으면 열리고 그 안의 index.html 에서 SPREAD·PARALLAX·LINK_RADIUS·LENS_MASS 같은 값을 바꿔 보면 된다.

변형 세 가지, 그리고 움직임 줄이기 설정의 접근성

이름 값 느낌
속도 바꾸기 01 SPEED 12 → 24 줄기 평균 길이가 5.0px 에서 11.4px 로 늘어 중심 쪽 별이 순식간에 가장자리로 번진다.
연결 반경 바꾸기 07 LINK_RADIUS 72 → 110 선은 158개에서 161개로 거의 그대로인데 평균 길이가 47.3px 에서 80.7px 로 늘어 먼 별과도 이어진다.
렌즈 질량 바꾸기 09 LENS_MASS 2600 → 5200 링 반지름이 51px 에서 72px 로 커지고 그려진 상은 454개에서 510개로 늘어 별빛이 중심에서 더 멀리 밀려난다.

표의 숫자는 세 줄 모두 브라우저가 아니라 노드 시뮬레이션에서 센 값이다.

prefers-reduced-motion 이 켜져 있으면 아홉 파일 모두 requestAnimationFrame 루프를 시작하지 않고, 시작 장면을 advance(1400) 으로 88틱 더 돌려 한 장만 그린다. 버튼은 「다음 장면」 이 되어 한 번 누를 때 REDUCE_STEP_MS 900밀리초(56틱)를 진행하고 한 번 그리며, 포인터·키·탭 입력은 refresh() 가 6틱(96밀리초)을 돌려 바로 반영한다.

캔버스는 tabindex 0 이라 포커스를 받고 Enter·Space 는 tap(), 방향키는 가상 포인터를 KEY_STEP 14px 씩 옮긴다. 이 여섯 키만 preventDefault 를 부르고 Tab 을 포함한 나머지 키는 그냥 지나가며, 상태 글자는 장면이 정한 문구가 바뀔 때만 aria-live="polite" 영역을 고친다.

css starfield animation 은 CSS 키프레임만으로 충분한가?

점 몇 개가 제자리에서 깜박이거나 한 방향으로 흐르기만 하면 충분하다. 별이 다가오는 느낌, 층마다 다른 속도, 구 위에서 도는 하늘은 별마다 서로 다른 값을 계산해야 해서 별 배경을 캔버스 계산으로 넘기는 지점이 되고, 그때부터 css starfield background 는 틀만 CSS 가 잡고 별은 코드가 그린다.

경계를 정하는 기준은 별 하나가 자기 거리나 이웃을 알아야 하는가이다. 별이 서로의 위치를 몰라도 되는 장식이면 CSS 로 충분하고, 01 처럼 별마다 z 로 나누거나 07 처럼 가까운 별을 찾아 이어야 하면 코드가 맞다. 또 이 편의 코드는 프레임마다 흐른 시간을 requestAnimationFrame 타임스탬프로 재어 고정 16밀리초 틱으로 쪼개 진행하고, 밀린 시간은 한 프레임에 최대 160밀리초까지만 따라잡아서 화면이 느려져도 별의 속도는 같다.

그래도 CSS 는 틀을 맡는다. 이 편의 style.scss 는 캔버스를 aspect-ratio 로 맞추고 touch-action: none 으로 포인터 드래그가 스크롤로 새지 않게 하며, 장면 안의 별은 전부 캔버스 안에서 계산된다.

javascript starfield 를 만들기 전에 자주 묻는 질문

javascript starfield 에서 가장 먼저 바꿀 값은 무엇인가?

01 이면 SPEED 와 STAR_COUNT 다. 속도감이 부족하면 SPEED 부터 올리고 줄기가 너무 길면 STREAK_LEN 을 줄이면 되는데, 이 둘은 서로 독립이라 하나씩 바꿔야 어느 쪽 영향인지 보인다. 별 수 상수의 이름은 장면마다 STAR_TOTAL·NODE_COUNT 처럼 다르지만 모두 파일 맨 위에 모여 있어, 숫자 하나만 바꿔도 느낌이 달라지고 줄이면 화면이 가벼워진다.

별 색은 어디서 바꾸나?

별 색은 STAR_RGBS 다섯 값(흰청·살구·라일락·민트·핑크)과 배경 BG 에서 고른다. 04 는 WARM·WHITE·COOL 세 값을 따로 쓰고 08 은 HEAD·TAIL 로 꼬리 색을 정한다. 파스텔이 아닌 진한 색으로 바꿀 때는 배경 BG 와의 밝기 차를 같이 보고, 07 의 선처럼 두 별 색을 섞어 쓰는 곳은 양쪽을 함께 바꿔야 어긋나지 않는다.

라이브러리나 이미지가 필요한가?

필요 없다. 아홉 파일은 외부 주소를 부르지 않고 별을 전부 캔버스 2D 로 그려서, 파일 하나만 옮겨도 돈다.

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

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