GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

css sunset gradient 9パターン — 太陽が低くなると空と海もついて変わる

css sunset gradient を色二つを混ぜた背景だけで作ると、太陽が低くなっても空は変わらず、海に光の道はなく、雲の下側は暗いまま — 01番・04番・05番が計算で直す。太陽の高さ一つが、九つの場面すべてを描き直します。

日没の九つの場面をこのページで見る

自動再生 · タイルを押すとその項目へ · すべて1つの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ミリ秒で SCRUB_STEP ずつ進めます。

背景に色を二つ混ぜるだけの夕焼けが生きない理由は、その色が太陽の高さを知らないことです。この記事は空の一行の色を、太陽の高さと行の位置という二つの値の関数にしました。海・雲・星にも同じ 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 オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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ぶん縮み、横の半径は rx に直接書かれた0.28ぶん広がり(こちらは定数ではなくリテラルです)、色は薄い黄色から赤い珊瑚色へ移ります。三つとも low という一つの値に従い、alt 0 では low が1なので縦13.2・横25.6の平たい太陽になり、この円盤を地平線で折り返して波の分だけ揺らし、海に映します。測定では、累積面積が75.4%、動いたフレームが23/23でした。

地平線に近づくほど平たく赤くなる太陽
新しいタブで開く

03 · 地平線に近づくほど平たく赤くなる太陽 — 計測 累積面積 75.381% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 で点けたり消したりして、途切れた光の道を作ります。幅の半分は式の値で地平線に7.7px、いちばん下に31.9pxと遠近が付く(測定ではなく計算で、実際の最後の行は約31.7pxです)ので、海辺の風景背景にそのまま使えます。測定では、累積面積が74.9%、動いたフレームが23/23でした。

太陽の下へ長く続く海のきらめきの道
新しいタブで開く

04 · 太陽の下へ長く続く海のきらめきの道 — 計測 累積面積 74.935% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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空が暗くなるぶん一つずつ現れる星

星九十個のそれぞれに明るさ 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 オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 になり(範囲は06:11〜17:49)、時間帯プレビューのコントロールとして使えます。測定では、累積面積が75.5%、動いたフレームが23/23でした。

太陽をドラッグして一日を巻き戻す夕焼けのタイムスクラバー
新しいタブで開く

08 · 太陽をドラッグして一日を巻き戻す夕焼けのタイムスクラバー — 計測 累積面積 75.469% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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 オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@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) に入り、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までしか上がりません。そのため九十個のうち見えるのは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 名前 = 値; の一行で、前の節の7.7〜31.9pxと11.9〜49.3pxといった値は、ソースの式に数を入れて計算したものです。

reduce と矢印キーのときの動き

prefers-reduced-motion が有効だと、01〜07は requestAnimationFrame のループを始めず、from の高さで場面を一枚だけ描きます。ボタンは 次の場面 になり、押すたびに from から to を三等分した四つの高さを順に回して描き直し、ポインターとキーの入力は takeOver を通って、その場で一枚ずつすぐ反映されます。

08は reduce でも、弧のドラッグ・左右キー・ボタンが動きますが、開始の s が REST_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 の [alt, [r, g, b]] の組を書き換えます。色の帯の外側(−0.15より下と1より上)は端の色に固定されます。09はキーフレーム五か所の16進の値を書き換えます。

ライブラリや WebGL は必要ですか?

どちらも要りません。九つのファイルに外部のアドレスも WebGL の呼び出しも一度も出てこず、キャンバスの2dとCSSだけで描くので、ファイル一つだけ持ち出しても、その場面は動きます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。