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

css sunset gradient 9가지 — 해가 낮아질수록 하늘과 바다가 따라 변한다

css sunset gradient 를 색 두 개를 섞은 배경으로만 만들면 해가 낮아져도 하늘은 그대로고, 바다엔 빛의 길이 없고, 구름 아랫면은 어두운 채다 — 01번·04번·05번이 계산으로 푼다. 해 높이 하나가 아홉 장면 전체를 다시 그린다.

해 지는 아홉 장면을 화면에서 바로 본다

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

css sunset gradient 9가지 — 해 높이·빛의 길이·반사를 계산해야 노을이 산다

01~08 은 캔버스 480×300 한 장에 상태 글자 한 줄과 버튼 하나가 붙은 꼴이고, 색 띠를 읽는 ramp·하늘 한 점을 칠하는 skyAt·바다를 까는 seaBase·빛번짐 glow 를 같은 글자로 나눠 쓴다. 장면마다 다른 것은 scene.draw 하나이며, 09 만 <script> 태그가 없는 순수 CSS 다.

입력은 해 높이 alt 하나다. 0 이면 해가 지평선에 걸리고 1 이면 높이 뜨며 음수는 지평선 아래이고, 화면 y 는 186 − alt × 150 이라 alt 0.5 는 111, 0.2 는 156, −0.12 는 204 에 놓인다. 01~07 의 자동 시연은 PERIOD_MS 4400 을 한 주기로 삼아 from 에서 to 까지 2.2초에 내려가고 2.2초에 올라오며, 08 은 AUTO_MS 7600 을 한 주기로 호를 오간다.

배경색 두 개를 섞는 방식이 노을을 못 만드는 까닭은 색이 해 높이를 모르기 때문이다. 이 편은 하늘 한 줄의 색을 해 높이와 줄 위치 두 값의 함수로 만들고 바다·구름·별이 같은 alt 를 받게 했으니, sunset background css 를 찾아 온 사람이 얻는 차이는 그라디언트 두 줄이 아니라 입력 하나가 층마다 다른 색 띠를 고른다는 점이다.

getImageData 는 아홉 파일 어디에도 없고 픽셀을 직접 쓰는 곳은 05 의 createImageData·putImageData 뿐이다. requestAnimationFrame·matchMedia·pointermove 는 01~08 의 여덟 파일에 있고 09 는 CSS 미디어 쿼리만 쓴다. Math.random 은 어디에도 없으며 별 자리는 시드를 고정한 mulberry32 로 뽑는다(07 은 시드 448, 08 은 시드 808).

css sunset beach 풍경을 기법별 코드로 본다

아래 조각은 각 파일에서 이어진 줄을 그대로 잘라 붙인 것이고 한 장면에 한두 조각씩 실었다. 하늘색은 01 의 ZENITH_RAMP·HORIZON_RAMP·SUN_RAMP 세 배열이 정하는데, sunset color css 를 손보려는 사람은 여기서 시작하면 된다. 처음부터 끝까지 이어진 파일은 이 글 아래쪽 압축 파일에 있다.

01해 높이에 따라 하늘 색이 이어서 바뀌는 노을 배경

skyAt 이 줄마다 v = 줄 ÷ 186 을 만들어 정점 색 띠와 지평선 색 띠에서 해 높이로 고른 두 색을 v 의 SKY_BLEND 1.7 제곱만큼 섞는다. 해가 내려오면 두 띠에서 고르는 자리가 같이 옮겨 가 하늘 전체가 한꺼번에 따뜻해지니 노을 히어로 배경에 얹기 좋다. 측정에서는 누적면적 74.2%, 움직인 프레임 23/23 이 나왔다.

해 높이에 따라 하늘 색이 이어서 바뀌는 노을 배경
새 창에서 열기

01 · 해 높이에 따라 하늘 색이 이어서 바뀌는 노을 배경 — 측정 누적면적 74.222% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SUN_ALTZENITH_RAMPHORIZON_RAMPSKY_BLEND
    function ramp(stops, a) {
        if (a <= stops[0][0]) {
            return stops[0][1];
        }
        for (let i = 1; i < stops.length; i += 1) {
            if (a <= stops[i][0]) {
                const p = stops[i - 1];
                const q = stops[i];
                return mix(p[1], q[1], (a - p[0]) / (q[0] - p[0]));
            }
        }
        return stops[stops.length - 1][1];
    }
    // 하늘 한 점의 색 — v 는 0(정점)에서 1(지평선)
    function skyAt(alt, v) {
        return mix(ramp(ZENITH_RAMP, alt), ramp(HORIZON_RAMP, alt), Math.pow(clamp(v, 0, 1), SKY_BLEND));
    }
    const scene = {
        from: SUN_ALT,
        to: -0.12,
        hi: 1,
        lo: ALT_NIGHT,
        draw: function (ctx, alt, t) {
            skyRows(ctx, alt, 0, HORIZON, SKY_ROW);
            const sc = ramp(SUN_RAMP, alt);
            ctx.save();
            ctx.beginPath();
            ctx.rect(0, 0, W, HORIZON);
            ctx.clip();
            sunDisc(ctx, SUN_X, sunY(alt), SUN_R, SUN_R, sc, 96);
            ctx.restore();
            seaBase(ctx, alt, t);
            ctx.save();
            ctx.translate(SUN_X, HORIZON + 40);
            ctx.scale(1, 2.2);
            glow(ctx, 0, 0, 48, sc, 0.3 * smooth((alt + 0.1) / 0.3));
            ctx.restore();
            swatches(ctx, alt);
        }
    };

02줄마다 빛이 지나온 길이로 색을 칠하는 대기 산란 하늘

rowColor 가 가로줄마다 시선이 대기를 지나는 길이 1 ÷ (el + 0.1) 로 산란량을 구하고, 해 빛이 온 길이로 파랑부터 깎아 낮은 해일수록 붉은 빛이 줄 색에 남게 한다. 시선이 대기를 지나는 길이가 정점의 0.91 에서 지평선의 10 까지 급해지므로 시선 높이 0.1 이하에 색 변화가 몰리며, 노을 하늘 배경을 식 하나로 생성하려는 쪽에 맞다. 측정에서는 누적면적 73.9%, 움직인 프레임 23/23 이 나왔다.

줄마다 빛이 지나온 길이로 색을 칠하는 대기 산란 하늘
새 창에서 열기

02 · 줄마다 빛이 지나온 길이로 색을 칠하는 대기 산란 하늘 — 측정 누적면적 73.88% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

AIR_DENSITYPATH_GAINRED_KEEPROW_STEP
    // 한 줄의 색 — el 은 그 줄을 보는 시선의 높이(1 = 정점, 0 = 지평선). 낮을수록 대기를 지나는 길이 길어진다
    function rowColor(alt, el) {
        const sunPath = 1 / (0.9 * Math.max(alt, 0) + 0.1);
        const loss = [0.05 * (1 - RED_KEEP), 0.24, 0.42];
        const day = smooth((alt + 0.16) / 0.34);
        const rowPath = 1 / (el + 0.1);
        const out = [0, 0, 0];
        for (let c = 0; c < 3; c += 1) {
            const direct = Math.exp(-loss[c] * PATH_GAIN * sunPath * 4);
            const seen = 1 - Math.exp(-SCATTER[c] * AIR_DENSITY * rowPath * 0.18);
            const rad = direct * seen * (0.1 + 0.9 * day) + NIGHT_FLOOR[c] * (1 - day);
            out[c] = 255 * (0.2 + 0.8 * (1 - Math.exp(-EXPOSURE * rad)));
        }
        return out;
    }

03지평선에 닿을수록 납작해지고 붉어지는 해

해 원반은 세로 반지름이 FLATTEN_MAX 0.34 만큼 줄고, 가로 반지름은 코드에 적힌 리터럴 0.28 만큼 늘며, 색은 연노랑에서 붉은 산호빛으로 옮겨 가는데, 셋 다 low 한 값을 따른다. alt 0 에서 low 는 1 이라 세로 13.2·가로 25.6 의 납작한 해가 되고, 이 원반을 지평선 축으로 뒤집어 바다에 물결만큼 흔들어 비춘다. 측정에서는 누적면적 75.4%, 움직인 프레임 23/23 이 나왔다.

지평선에 닿을수록 납작해지고 붉어지는 해
새 창에서 열기

03 · 지평선에 닿을수록 납작해지고 붉어지는 해 — 측정 누적면적 75.381% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FLATTEN_MAXSUN_RADIUSHALO_SPREADREDDEN_AT
            // 낮아질수록 0 에서 1 로 — 납작함·붉음·번짐이 모두 이 한 값을 따른다
            const low = 1 - smooth(alt / REDDEN_AT);
            const ry = SUN_RADIUS * (1 - FLATTEN_MAX * low);
            const rx = SUN_RADIUS * (1 + 0.28 * low);
            const col = mix([255, 246, 220], [255, 128, 112], low);
            const sy = sunY(alt);
            const halo = SUN_RADIUS * (2.2 + HALO_SPREAD * low);
            ctx.save();
            ctx.beginPath();
            ctx.rect(0, 0, W, HORIZON);
            ctx.clip();
            glow(ctx, SUN_X, HORIZON, 170, col, 0.34 * low);
            sunDisc(ctx, SUN_X, sy, rx, ry, col, halo);
            ctx.restore();
            // 바다에 비친 해 — 지평선을 축으로 뒤집은 원반을 줄마다 잘라 물결만큼 흔든다
            const yr = 2 * HORIZON - sy;
            for (let y = HORIZON + 1; y < H; y += 2) {
                const dist = (y - yr) / ry;
                if (Math.abs(dist) < 1) {
                    const hw = rx * Math.sqrt(1 - dist * dist);
                    const shift = 5 * (0.3 + 0.7 * (y - HORIZON) / 60) * Math.sin(y * 0.5 + t * 0.003);
                    ctx.fillStyle = css(col, 0.62 * (1 - 0.5 * Math.min(1, (y - HORIZON) / 90)));
                    ctx.fillRect(SUN_X - hw + shift, y, hw * 2, 2);
                }
            }

04해 아래로 길게 이어지는 바다 윤슬 길

glitterRoad 가 바다를 2px 줄 57개로 나눠 줄마다 파도로 흔들린 중심을 구하고, 그 중심 둘레 폭 안의 조각 아홉 개를 vnoise 로 켜고 꺼서 해 아래에 끊어진 빛의 길을 만든다. 폭의 반은 줄 위치 d 가 0 인 지평선에서 7.7px, d 가 1 인 맨 아래에서 31.9px 이라 원근이 붙고 바다 풍경 배경으로 바로 쓸 수 있다. 측정에서는 누적면적 74.9%, 움직인 프레임 23/23 이 나왔다.

해 아래로 길게 이어지는 바다 윤슬 길
새 창에서 열기

04 · 해 아래로 길게 이어지는 바다 윤슬 길 — 측정 누적면적 74.935% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WAVE_AMPGLITTER_WIDTHROW_JITTERSUN_X
    // 바다 윤슬 길 — 수면 가로줄마다 파도로 흔들린 위치를 구하고, 해 아래 폭 안의 조각만 밝게 칠한다
    function glitterRoad(ctx, alt, cx0, t, col) {
        const vis = smooth((alt + 0.08) / 0.22);
        if (vis <= 0.01) {
            return;
        }
        const lit = mix(col, [255, 255, 255], 0.4);
        for (let y = HORIZON + 1; y < H; y += 2) {
            const d = (y - HORIZON) / (H - HORIZON);
            const jit = ROW_JITTER * (hash2(y, 11) - 0.5) * 2;
            const shift = WAVE_AMP * (0.18 + 0.82 * d) * (Math.sin(y * 0.31 + t * 0.0021) + 0.6 * Math.sin(y * 0.77 - t * 0.0033 + jit * 3) + jit * 0.5);
            const half = GLITTER_WIDTH * (0.35 + 1.1 * d);
            const seg = 9;
            const segW = half * 2 / seg;
            for (let s = 0; s < seg; s += 1) {
                const u = (s + 0.5) / seg * 2 - 1;
                const on = smooth((vnoise(s * 1.7 + y * 0.11, t * 0.0016 + y * 0.05) - 0.42) * 5);
                const a = vis * (1 - u * u) * on * (0.95 - 0.4 * d);
                if (a > 0.02) {
                    ctx.fillStyle = css(lit, a);
                    ctx.fillRect(cx0 + shift - half + s * segW, y, segW * 0.9, 2);
                }
            }
        }
    }

05해가 낮을수록 아랫면이 먼저 붉어지는 구름 띠

160×64 ImageData 한 장을 만들어 픽셀마다 fbm 으로 밀도를 구하고, 세로 네 층마다 underlit 이 불빛 받는 정도를 따로 재서 아랫층부터 붉게 물들인다. alt 0.3 에서는 아래 두 층이, alt 0 에서는 아래 세 층이 붉고 맨 위 층은 LIGHT_REACH 0.9 가 1 에 못 미쳐 본래 색에 남는다. 측정에서는 누적면적 75.3%, 움직인 프레임 23/23 이 나왔다.

해가 낮을수록 아랫면이 먼저 붉어지는 구름 띠
새 창에서 열기

05 · 해가 낮을수록 아랫면이 먼저 붉어지는 구름 띠 — 측정 누적면적 75.346% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CLOUD_LAYERSUNDERLIT_BIASDRIFT_BANDLIGHT_REACH
    // 구름 한 층(0 = 맨 위, 1 = 맨 아래)이 해 불빛을 받는 정도 — 해가 낮을수록 빛이 닿는 깊이가 위로 올라온다
    function underlit(layerK, alt) {
        const reach = (1 - smooth(alt / 0.55)) * LIGHT_REACH;
        const u = Math.pow(1 - layerK, 1 + UNDERLIT_BIAS);
        return smooth((reach - u) / 0.4);
    }
    function cloudTone(layerK, alt) {
        const dusk = 1 - smooth((alt + 0.1) / 0.5);
        const body = mix([246, 246, 253], [124, 120, 172], dusk);
        const shaded = mix(body, [body[0] * 0.9, body[1] * 0.9, body[2] * 0.96], layerK * 0.4);
        const flame = mix([255, 204, 156], [255, 132, 128], layerK);
        return mix(shaded, flame, underlit(layerK, alt));
    }
        function paintClouds(alt, t) {
            const tones = [];
            for (let l = 0; l < CLOUD_LAYERS; l += 1) {
                tones.push(cloudTone(l / (CLOUD_LAYERS - 1), alt));
            }
            for (let j = 0; j < CLOUD_Y; j += 1) {
                const rowK = j / (CLOUD_Y - 1);
                const layer = Math.min(CLOUD_LAYERS - 1, Math.floor(rowK * CLOUD_LAYERS));
                const tone = tones[layer];
                const envelope = Math.pow(Math.sin(Math.PI * (0.06 + 0.88 * rowK)), 0.8);
                for (let i = 0; i < CLOUD_X; i += 1) {
                    const nx = i / CLOUD_X * 3.4 + t * DRIFT_BAND * (0.5 + rowK);
                    const d = fbm(nx, rowK * 3.3 + 2.1) * envelope;
                    const a = smooth((d - 0.3) / 0.22);
                    const k = 0.9 + 0.2 * d;
                    const o = (j * CLOUD_X + i) * 4;
                    px[o] = Math.min(255, tone[0] * k);
                    px[o + 1] = Math.min(255, tone[1] * k);
                    px[o + 2] = Math.min(255, tone[2] * k);
                    px[o + 3] = 255 * a;
                }
            }
            octx.putImageData(img, 0, 0);
        }

06멀수록 하늘색에 녹아드는 겹겹의 산 능선

ridgeTop 이 능선 다섯 겹의 높이를 fbm 으로 구하고, 먼 겹일수록 지금 하늘색을 더 섞어 안개 비율을 0.80·0.57·0.35·0.15·0 으로 낮춘다. 어두운 쪽으로 섞는 비율은 0.25 에서 0.77 로 올라 가까운 능선이 짙고 먼 능선이 하늘에 녹으니 풍경 일러스트 히어로에 깊이를 준다. 측정에서는 누적면적 75.1%, 움직인 프레임 23/23 이 나왔다.

멀수록 하늘색에 녹아드는 겹겹의 산 능선
새 창에서 열기

06 · 멀수록 하늘색에 녹아드는 겹겹의 산 능선 — 측정 누적면적 75.139% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RIDGE_LAYERSHAZE_STRENGTHNEAR_DARKENRIDGE_ROUGH
    // 능선 하나의 높이 — 층 번호 i(0 = 가장 먼 것)마다 기준선·진폭·주파수·흐름 속도가 다르다
    function ridgeTop(i, x, t) {
        const k = i / (RIDGE_LAYERS - 1);
        const base = lerp(176, RIDGE_GROUND, Math.pow(k, 0.9));
        const amp = (16 + 26 * k) * RIDGE_ROUGH;
        const freq = 0.0105 + 0.004 * k;
        return base - amp * fbm(x * freq + i * 31.7 + t * 0.00002 * (1 + 2.4 * k), i * 5.31);
    }
            for (let i = 0; i < RIDGE_LAYERS; i += 1) {
                const k = i / (RIDGE_LAYERS - 1);
                // 가까울수록 어두운 쪽으로, 멀수록 지금 하늘 색 쪽으로 — 두 값 모두 능선 번호 하나에서 나온다
                const solid = mix(body, [34, 38, 80], 0.25 + NEAR_DARKEN * k);
                const haze = HAZE_STRENGTH * Math.pow(1 - k, 1.2);
                const top = mix(solid, hazeSky, haze);
                const bottom = mix(solid, hazeSky, haze * 0.55);
                const g = ctx.createLinearGradient(0, 130, 0, H);
                g.addColorStop(0, css(top, 1));
                g.addColorStop(1, css([bottom[0] * 0.82, bottom[1] * 0.82, bottom[2] * 0.86], 1));
                ctx.fillStyle = g;
                for (let x = 0; x < W; x += RIDGE_STEP) {
                    const y = ridgeTop(i, x, t);
                    ctx.fillRect(x, y, RIDGE_STEP + 1, H - y);
                }
            }

07하늘이 어두워지는 만큼 하나둘 드러나는 별

별 90개마다 밝기 b 와 보이기 시작하는 하늘 휘도 문턱 thr 를 0.2~0.62 사이에서 따로 두고, 그 자리 하늘이 문턱 아래로 어두워지면 smooth 로 서서히 드러낸다. 밝은 별이 먼저 나오고 흐린 별이 뒤따라서 보이는 별 수가 alt 0.14 에서 4, 0 에서 13, −0.16 에서 48 로 늘며, 해질녘에서 밤으로 넘어가는 전환 화면에 맞다. 측정에서는 누적면적 74.2%, 움직인 프레임 23/23 이 나왔다.

하늘이 어두워지는 만큼 하나둘 드러나는 별
새 창에서 열기

07 · 하늘이 어두워지는 만큼 하나둘 드러나는 별 — 측정 누적면적 74.174% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STAR_COUNTVISIBLE_MINVISIBLE_MAXMAG_SPREADTWINKLE_BAND
    function makeStars() {
        const rnd = mulberry32(448);
        const out = [];
        for (let i = 0; i < STAR_COUNT; i += 1) {
            const b = Math.pow(rnd(), MAG_SPREAD);
            out.push({ x: 8 + rnd() * (W - 16), y: 6 + Math.pow(rnd(), 0.9) * (HORIZON - 26), b: b, thr: lerp(VISIBLE_MIN, VISIBLE_MAX, b), phase: rnd() * PI2 });
        }
        return out;
    }
            for (let i = 0; i < stars.length; i += 1) {
                const s = stars[i];
                // 별 하나의 기준 — 그 자리 하늘 밝기가 문턱 아래로 내려가야 보이기 시작한다
                const sky = luma(skyAt(alt, s.y / HORIZON));
                const vis = smooth((s.thr - sky) / 0.07);
                if (vis > 0.02) {
                    shown += 1;
                    const a = vis * (1 - TWINKLE_BAND * (0.5 + 0.5 * Math.sin(t * 0.004 + s.phase)));
                    const r = 0.7 + 1.5 * s.b;
                    ctx.fillStyle = 'rgba(255,252,240,' + a.toFixed(3) + ')';
                    ctx.beginPath();
                    ctx.arc(s.x, s.y, r, 0, PI2);
                    ctx.fill();
                    if (s.b > 0.45) {
                        glow(ctx, s.x, s.y, 7 + 8 * s.b, [255, 244, 220], 0.35 * a);
                    }
                    if (s.b > 0.35) {
                        const ry = HORIZON + (HORIZON - s.y) * 0.55;
                        if (ry < H) {
                            ctx.fillStyle = 'rgba(255,248,230,' + (0.5 * a).toFixed(3) + ')';
                            ctx.fillRect(s.x + 3 * Math.sin(t * 0.003 + s.phase), ry, 2 + 3 * s.b, 1.5);
                        }
                    }
                }
            }

08해를 끌어 하루를 되감는 노을 시간 스크러버

arcPoint 한 함수가 호 위 위치 s(0~1)에서 해 좌표와 alt 를 내는데, 하늘·별·구름 색·바다·윤슬 길은 alt 를 받고 해의 x 는 빛무리와 윤슬 중심에만 쓰이며, 화면 s 는 목표 s 를 DRAG_SMOOTH 0.22 비율로 따라온다. 시각 표시는 6시에 12시간 × s 를 더한 값이라 s 0.5 가 h 12:00 이 되고, 호 아래를 누르면 가까운 끝으로 붙어 시간대 미리보기 컨트롤로 쓸 수 있다. 측정에서는 누적면적 75.5%, 움직인 프레임 23/23 이 나왔다.

해를 끌어 하루를 되감는 노을 시간 스크러버
새 창에서 열기

08 · 해를 끌어 하루를 되감는 노을 시간 스크러버 — 측정 누적면적 75.469% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ARC_RADIUSDRAG_SMOOTHDAY_CLAMPSCRUB_STEP
    // 호 위의 위치 s(0 = 왼쪽 끝, 1 = 오른쪽 끝) -> 해의 화면 좌표와 높이
    function arcPoint(s) {
        const x = ARC_CX - ARC_RADIUS * Math.cos(Math.PI * s);
        const y = ARC_CY - ARC_RADIUS * Math.sin(Math.PI * s);
        return { x: x, y: y, alt: (HORIZON - y) / SUN_RISE };
    }
    // 포인터 위치 -> 호 위 s. 호 아래로 내려가면 가까운 끝으로 붙는다
    function sFromPointer(px, py) {
        const ang = Math.atan2(ARC_CY - py, px - ARC_CX);
        if (ang < 0) {
            return px < ARC_CX ? DAY_CLAMP : 1 - DAY_CLAMP;
        }
        return clamp(1 - ang / Math.PI, DAY_CLAMP, 1 - DAY_CLAMP);
    }
        function tick(ts) {
            if (last === null) {
                last = ts;
            }
            const dt = Math.min(ts - last, 160);
            last = ts;
            if (!paused) {
                if (!manual) {
                    t += dt;
                    target = 0.5 + (0.5 - DAY_CLAMP) * Math.cos(PI2 * t / AUTO_MS);
                }
                shownS += (target - shownS) * (1 - Math.pow(1 - DRAG_SMOOTH, dt / 16.7));
                show();
            }
            raf = requestAnimationFrame(tick);
        }

09색 위치를 등록 속성으로 움직이는 순수 CSS 노을 그라디언트

색 세 개·멈춤 위치 두 값·해 위치·해 색·바다 두 색·윤슬 투명도를 @property 등록 속성 10개로 선언하고, 키프레임 하나가 0·30·55·80·100% 다섯 지점의 값 사이를 이어 주므로 스크립트 없이 하늘이 저문다. 멈춤은 체크박스와 <label> 이 맡아 :has 로 애니메이션을 일시정지시키니 가벼운 노을 배경이 필요한 자리에 얹기 좋다. 측정에서는 누적면적 73.1%, 움직인 프레임 23/23 이 나왔다.

색 위치를 등록 속성으로 움직이는 순수 CSS 노을 그라디언트
새 창에서 열기

09 · 색 위치를 등록 속성으로 움직이는 순수 CSS 노을 그라디언트 — 측정 누적면적 73.117% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@property--sky-top--stop-a--sun-y
@property --sky-top {
  syntax: "<color>";
  inherits: true;
  initial-value: #807eb6;
}

@property --sky-mid {
  syntax: "<color>";
  inherits: true;
  initial-value: #b38fa8;
}

@property --sky-low {
  syntax: "<color>";
  inherits: true;
  initial-value: #e6b7ab;
}
.sc__glint {
  position: absolute;
  inset: -1em 0;
  background:
    repeating-linear-gradient(180deg, color-mix(in srgb, var(--sun) 55%, white) 0 2px, transparent 2px 7px),
    repeating-linear-gradient(180deg, white 0 1px, transparent 1px 14px);
  clip-path: polygon(61% 0, 69% 0, 82% 100%, 48% 100%);
  mask-image:
    linear-gradient(90deg, transparent 40%, #000 56%, #000 74%, transparent 90%),
    repeating-linear-gradient(90deg, #000 0 6%, transparent 6% 7.5%, #000 7.5% 15%, transparent 15% 17%, #000 17% 24%, transparent 24% 25%);
  mask-composite: intersect;
  opacity: var(--glint-a);
  animation: shimmer 1.6s linear infinite;
}

값이 장면을 바꾸는 곳 — 해 높이·안개 세기·반사 폭

시작 높이를 낮추면 첫 화면이 노을 쪽에 놓인다

01 의 SUN_ALT 는 자동 시연이 출발하는 해 높이다. 0.5 에서 0.2 로 바꾸면 해 y 가 111 에서 156 으로 내려오고, 첫 화면의 상태 문구가 해가 기운다 — 금빛 오후 에서 해가 낮다 — 노을 로 바뀐다.

안개 비율은 먼 능선부터 하늘로 보낸다

06 의 HAZE_STRENGTH 는 안개 비율 식 HAZE_STRENGTH × (1 − k)^1.2 의 앞 계수다. 0.8 에서 0.95 로 올린 값을 식에 넣어 계산하면 가장 먼 겹이 0.8 에서 0.95 로, 둘째 겹이 0.57 에서 0.67 로 하늘색에 가까워지고 가장 가까운 겹은 0 그대로다.

반사 폭은 원근을 따라 넓어진다

04 의 GLITTER_WIDTH 22 는 윤슬 폭의 반 22 × (0.35 + 1.1 × d) 에 들어가 d 0·0.5·1 에서 7.7·19.8·31.9px 를 만든다. 34 로 넓히면 11.9·30.6·49.3px 가 되고, 줄마다 위치를 어긋내는 ROW_JITTER 를 0 으로 두면 끊김 없는 한 줄 기둥이 된다.

마지막 전체 렌더에서 잰 값을 항목별로 모았다. 누적면적은 73.1~75.5% 에 들어오고 움직인 프레임은 아홉 항목 모두 23/23 이다.

항목 누적면적 평균 강도 움직인 프레임
01 하늘 색 띠 74.2% 31.3 23/23
02 산란 줄 73.9% 37.4 23/23
03 납작한 해 75.4% 32.7 23/23
04 윤슬 길 74.9% 32.3 23/23
05 구름 아랫불빛 75.3% 22.9 23/23
06 능선 안개 75.1% 39.3 23/23
07 드러나는 별 74.2% 36.6 23/23
08 시간 스크러버 75.5% 29.5 23/23
09 순수 CSS 노을 73.1% 59.7 23/23

어디서 깨지는가 — 좁은 화면·해가 지평선 아래일 때·색 대비가 낮을 때

해가 지평선 아래로 내려가면 하나씩 꺼진다

04 의 윤슬은 alt −0.08 에서 가시도가 0 이 되어 완전히 꺼진다. 03 의 원반은 alt 0 에서 지평선에 반쯤 잘려 위쪽 반만 보이고 윗끝이 지평선 밑으로 가는 alt −0.088 보다 낮으면 사라진다. 바다에 비친 원반 중심은 2 × 186 − 해 y 라서 alt −0.1 이면 중심 y 171 에 세로 반지름 13.2 로 아랫끝이 184.2 에 그쳐 바다 줄(y 187 이상)에 걸리지 않는다. 색 띠는 −0.15 아래에서 끝 색에 고정되므로 07 의 to −0.16 도 −0.15 와 같은 색을 쓴다.

색 변화가 지평선 근처에 몰린다

01 의 줄 중간(v 0.5)에서 지평선 색 비중은 0.5 의 1.7 제곱인 0.31 뿐이다. SKY_BLEND 를 1 로 낮추면 비중이 0.5 가 되어 직선 그라데이션에 가까워진다. 02 는 그 길이가 시선 높이 0.5 이상에서 0.9~1.7 로 좁다가 아래 10% 에서 5~10 으로 커져 같은 방식으로 지평선 쪽에 색이 쏠린다.

02 는 가로 방향을 모른다

02 의 색은 줄, 곧 세로 위치만으로 정해져서 해 쪽이 더 밝은 하늘은 만들지 못한다. RED_KEEP 를 1 로 올리면 붉은 채널 손실이 0 이 되어 낮은 해에서도 붉은 빛이 깎이지 않는다. 해 둘레의 밝기는 같은 장면의 glow 호출이 따로 얹는다.

03 의 납작함은 alt 0.32 위에서 보이지 않는다

REDDEN_AT 0.32 보다 높은 해는 low 가 0 이라 완전한 원이다. 납작함과 붉은 기운을 보여 주고 싶으면 REDDEN_AT 를 키우거나 시작 높이를 낮춘다.

05 맨 위 층은 끝까지 불빛을 받지 못한다

LIGHT_REACH 가 0.9 라서 맨 위 층은 alt 0 까지 한 번도 불빛을 받지 않는다. 1 이상으로 올리면 맨 위까지 번지고, 160×64 저해상도를 3배로 키우는 탓에 구름 가장자리가 흐린 것은 의도한 모양이다.

07 은 흐린 별이 마지막까지 거의 안 보인다

가장 흐린 별(b 가 0 에 가까운 것)의 문턱은 0.2 인데 하늘 정점 휘도의 최저가 0.184(alt −0.16)라서 smooth 가 0.14 밖에 올라오지 못한다. 그래서 별 90개 중 48개만 보이며, VISIBLE_MIN 을 0.184 아래로 내리면 흐린 별도 나온다.

08 은 호 밖을 누르면 끝으로 붙는다

sFromPointer 는 호 아래를 누르면 가까운 끝(DAY_CLAMP 0.015, alt −0.065)으로 붙인다. 점 (240, 230)을 누르면 s 가 0.985 가 된다. 드래그하는 동안 목표 s 는 바로 바뀌지만 화면 s 는 한 프레임(60fps 기준)에 22% 씩 늦게 따라온다.

09 는 새 문법을 요구한다

@property 를 지원하지 않는 브라우저는 색을 보간하지 못해 키프레임 경계에서 색이 뚝뚝 바뀐다. color-mix·:has·mask-composite 도 필요하다. 체크박스는 투명 1px 로 숨겨져 있어 포커스 링이 라벨에 그려진다.

상태 글자와 방향키

상태 글자는 흰 알약 위의 어두운 글자라서 하늘색이 바뀌어도 하늘 위에 직접 얹히지 않는다. 01~07 의 방향키는 해 높이를 옮기는 데 쓰이는 만큼 캔버스에 포커스가 있는 동안 페이지 스크롤을 막으므로, 긴 페이지에 붙일 때는 키보드 스크롤이 멈추는지 먼저 본다.

값을 직접 바꿔 보며 경계를 확인하는 법

위의 경계는 01 의 SKY_BLEND, 03 의 REDDEN_AT, 05 의 LIGHT_REACH, 07 의 VISIBLE_MIN 을 하나씩 바꾸며 직접 볼 수 있다. 압축 파일은 비밀번호 nx9tfgpu 을 입력하면 열리고, 안에는 아홉 장면의 완결본이 들어 있다.

변형 세 가지와 접근성

이름 값 느낌
해 높이 바꾸기 01 SUN_ALT 0.5 → 0.2 시작 높이를 0.5 에서 0.2 로 낮추면 첫 화면부터 하늘이 노을 쪽에 가 있고, 거기서 지평선 아래까지 내려간다.
안개 세기 바꾸기 06 HAZE_STRENGTH 0.8 → 0.95 0.8 에서 0.95 로 올리면 가장 먼 능선이 하늘 색에 거의 녹아 윤곽만 남는다.
반사 폭 바꾸기 04 GLITTER_WIDTH 22 → 34 22 에서 34 로 넓히면 해 아래 빛의 길이 굵어지고 수평선 쪽에서도 끊김이 눈에 띄게 줄어든다.

세 변형 모두 소스의 const NAME = 값; 한 줄이고, 앞 절의 7.7~31.9px 와 11.9~49.3px 같은 값은 소스 식에 넣어 계산한 것이다.

prefers-reduced-motion 이 켜져 있으면 01~07 은 requestAnimationFrame 루프를 시작하지 않고 from 높이로 장면을 한 장만 그린다. 버튼은 다음 장면 이 되어 누를 때마다 from 에서 to 를 3등분한 네 높이를 돌려 가며 다시 그리고, 포인터와 키 입력은 takeOver 를 거쳐 그 자리에서 한 장씩 바로 반영된다.

08 은 reduce 에서도 호 드래그·←→ 키·버튼이 동작하되 시작 s 가 0.8 이고 화면 s 가 목표를 따라가지 않고 입력 즉시 같은 값으로 바뀐다. 09 는 두 애니메이션이 animation: none 이라 등록 속성 초깃값인 금빛 장면 한 장으로 멈춘다.

01~08 의 캔버스는 tabindex 0 이라 포커스를 받고, 상태 글자는 문구가 바뀔 때만 aria-live="polite" 영역을 고친다. 09 는 체크박스와 <label> 로 Space 키 멈춤이 되고 aria-live 문구는 고정이다.

css sunset animation 은 배경색만 갈아 끼우면 충분한가?

장면이 정지한 한 장이거나 짧은 루프라면 충분하고, 그 자리를 09 가 맡는다. 해 높이 하나에 구름·바다·별이 따라 움직여야 하는 순간부터는 색을 갈아 끼우는 키프레임으로는 층마다 다른 시작 시각을 맞출 수 없어 계산이 필요해진다.

자리별로 맞는 장면도 갈린다. 01 은 노을 히어로, 02 는 하늘 배경 생성, 04 는 바다 풍경 배경, 06 은 풍경 일러스트 히어로, 07 은 해질녘에서 밤으로 넘어가는 전환 화면, 08 은 시간대 미리보기 컨트롤, 09 는 가벼운 배경에 맞다.

비용은 장면마다 다르다. 06 은 한 프레임에 능선 조각을 1,200번(240열 × 5겹) 그리고 05 는 구름 픽셀 10,240개(160×64)의 밀도를 구하므로, 배경으로 깔 때는 이 두 장면의 프레임 비용을 먼저 본다.

바다 쪽 계산을 더 보고 싶으면 캔버스 바다 애니메이션 9종 — 겹친 정현파로 파도장을 계산한다가, 해 위치에서 그림자를 구하는 방식은 css 그림자 효과 9가지 — 광원 위치에서 그림자를 계산하는 장면이 이어 준다.

css sunset gradient 를 만들기 전에 자주 묻는 질문

css sunset animation 의 속도는 어느 값을 바꾸나?

01~07 은 PERIOD_MS 4400 이 한 주기라 2.2초에 내려가고 2.2초에 올라오며, 08 은 AUTO_MS 7600 이다. 09 는 scss 맨 위 $duration 3.2s 하나가 속도를 정한다.

노을 색을 내 페이지에 맞추려면 어디를 고치나?

01~08 은 ZENITH_RAMP·HORIZON_RAMP·SUN_RAMP 의 [해 높이, [r, g, b]] 쌍을 바꾸고, 색 띠 밖(−0.15 아래, 1 위)은 끝 색에 고정된다. 09 는 키프레임 다섯 곳의 hex 값을 바꾼다.

라이브러리나 WebGL 이 있어야 돌아가나?

둘 다 필요 없다. 아홉 파일에는 외부 주소와 WebGL 호출이 한 번도 없고 캔버스 2d 와 CSS 만으로 그리므로, 파일 하나만 옮겨도 그 장면이 돈다.

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

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