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

雲 アニメーション css 9種 — 綿雲の形から雲海まで

雲 アニメーション css は、キャンバスでマスごとに水滴がどれだけ濃いかを計算して空を描く場面です。綿雲の底はなぜ切りそろえたように平らなのでしょうか。03番は上がる空気が凝結する高さを越えた所からだけ綿を足し、04番は太陽の高さの分だけずれた影を丘に落とし、05番は高さごとに違う風で筋雲を長く引き伸ばします。

雲は絵ではなく濃さです — 雲 アニメーション css はマスごとに水滴がどれだけ濃いかを計算するところから始まります

自動再生 · タイルを押すとその項目へ · すべて1つのzip

九つの場面は同じノイズ関数をいくつか別の場所に差したものです。01・02 は雲ひとつの輪郭と光、03 は雲ができる高さ、04・05 は地面と風が雲に残す跡を扱います。06・07 は背景に敷ける平面と帯、08 はほどける雲で、09 は 06・02・07 の計算をひとつの画面に重ねます。

雲が濃さの数字ひとつになるまで

空のひとマスには、雲があるかないかではなく水滴の濃さの数字が入っています。その数字を重ねたノイズの合計で作り、光が濃さを通るたびにどれだけ削られるかを計算すると、雲の中は暗く縁は明るい形がひとりでに出ます。雲 アニメーション 作り方 を調べると描いた絵を重ねる方法が多く出ますが、この記事の場面は時刻を受け取って毎フレーム濃さを計算し直すので、太陽の高さや風の値を変えれば別の空になります。動く雲の gif のように描いたコマをくり返すのではありません。

キャンバス 2D だけで動く雲のソース

九つの場面とも外部ライブラリ・WebGL・CDN と写真ファイルを使わず、キャンバス 2D と requestAnimationFrame だけで動き、空・雲・地面は計算で塗っています。画素の計算は場面ごとの小さい格子(02 は 120×150 のパネル二枚、03・08 は 160×100、05・06・09 は 200×125、01・04・07 は 240×150)で行い、putImageData で載せた絵を 480×300 に引き伸ばして描きます。乱数はシードを固定した生成器ひとつだけで Math.random は 0/9 なので、同じコードは同じ空を描きます。「雲 背景 おしゃれ」で探す人にも、写真素材を用意せず色と光を値で整えられる点が合います。

となりの記事とちがう点

ノイズで地形を作る記事は キャンバス ノイズ 地形、同じキャンバス場面の基準は キャンバス 特殊効果 の記事が扱います。空の色の帯は 夕焼け空 グラデーション の記事、同じ計算の組み立てで雪を積もらせる場面は 雪のエフェクト の記事にあります。この記事は、雲ひとつがどうできて、光を受け、ほどけるかを九つに分けた点がちがいます。

雲 アニメーション css 9種 — 綿を溶かしてつなげたまんが雲の輪郭・太陽に背を向けた雲の銀の縁取り・凝結する高さで底が平らになる綿雲・太陽の高さの分だけずれて丘を渡る雲の影・高さごとに違う風で伸びる筋雲・飛行機の窓から広がる雲海・継ぎ目なく流れる重なった雲の帯・乾いた空気と混ざって縁からほどける雲・雲の上の日の出のヒーロー場面

01 は雲ひとつがどうつながるか、02 はそのかたまりが光をどう受けるかから始まります。03 は雲が空のどこでできるか、04・05 は地面と風が雲に残す跡を見せます。06・07 は背景向きの場面、08 は雲が消えていくほう、09 は前の計算をひとつの場面にまとめます。

九つの場面を描いて測った面積・フレーム・強さ

場面 累積面積 動いたフレーム 強さの平均
01 綿の輪郭 23.1% 23/23 40.0
02 銀の縁取り 18.7% 23/23 18.1
03 綿雲 8.3% 23/23 25.1
04 雲の影 17.8% 23/23 38.0
05 筋雲 29.6% 23/23 19.3
06 雲海 33.0% 23/23 24.3
07 雲の帯 43.2% 23/23 32.2
08 ほどける雲 14.2% 13/23 54.9
09 日の出のヒーロー 49.2% 23/23 18.7

画面の大半が空の色に変わる 09 が 49.2% でいちばん広く、雲ひとつが狭い所でだけわき立つ 03 が 8.3% でいちばん狭くなります。強さの平均は、濃さが引いては満ちる 08 が 54.9 で最高、銀の縁取りだけが変わる 02 が 18.1 で最低です。動いたフレームは 08 だけ 13/23 で、ほかの八つは 23/23 でした。08 がなぜ 13 なのかは、今回の測定では調べていません。

01綿を溶かしてつなげたまんが雲の輪郭

ランディングページ最初の画面の天気イラストに使うと、円 11 個がひとつの輪郭に溶けてつながったまま、息をするようにゆれます。smin は二つの距離を SMOOTH_K 18px の範囲でなめらかに混ぜ、8px あいた二つの円のまん中の距離を最小値の 4 から −0.5 にして内側へ埋め、すき間 9px(SMOOTH_K / 2)までつなぎます。綿の半径は PUFF_STIFFNESS 60 と PUFF_DAMPING 5 のばねなので、一度けると最大 +9.97px ふくらんでから −3.39px 行きすぎて戻り、減衰比は 0.323 です。

綿を溶かしてつなげたまんが雲の輪郭
新しいタブで開く

01 · 綿を溶かしてつなげたまんが雲の輪郭 — 計測 累積面積 23.124% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SMOOTH_KPUFF_STIFFNESSPUFF_DAMPING
    function smin(a, b, k) {
        const h = Math.max(k - Math.abs(a - b), 0) / k;
        return Math.min(a, b) - h * h * k / 4;
    }
    // 画面の点 (x, y) から雲の輪郭までの距離(px、内側は負)— 綿の距離を順になめらかに混ぜる
    function cloudDistance(x, y, k) {
        let d = 1e9;
        for (let c = 0; c < 2; c += 1) {
            const cc = cloudCenter(c, sceneT);
            let dc = 1e9;
            for (let i = 0; i < puffs.length; i += 1) {
                const p = puffs[i];
                if (p.cloud !== c) {
                    continue;
                }
                const di = Math.hypot(x - cc[0] - p.ox, y - cc[1] - p.oy) - p.r;
                dc = k > 0 ? smin(dc, di, k) : Math.min(dc, di);
            }
            d = Math.min(d, dc);
        }
        return d;
    }

02太陽に背を向けた雲の銀の縁取り

サービス紹介ページの夕暮れの背景に向いています。lightSum はマスごとに太陽のほうへ 6 歩進んで濃さを集め、e^(−ABSORB × S) で光を削るので、濃さの合計 1 を通ると透過は 0.407 しか残りません。演出 2 秒の画面では雲の中(濃さ 0.85 超)の平均透過が 0.135 で、空の光の下限がある右のパネルは薄い縁が明るさ 0.879、中が 0.742 と、縁のほうが明るくなります。

太陽に背を向けた雲の銀の縁取り
新しいタブで開く

02 · 太陽に背を向けた雲の銀の縁取り — 計測 累積面積 18.733% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LIGHT_STEPSAMBIENT_LIGHTFORWARD_POWER
    // マスから太陽のほうへ歩いて集めた濃さの合計 S(自分のマスの半歩ぶんを含む)
    function lightSum(gx, gy, steps) {
        const x = (gx + 0.5) * CELL;
        const y = (gy + 0.5) * CELL;
        const dx = SUN_X - x;
        const dy = SUN_Y - y;
        const len = Math.hypot(dx, dy) || 1;
        const ux = dx / len * LIGHT_STEP_PX / CELL;
        const uy = dy / len * LIGHT_STEP_PX / CELL;
        let s = dens02.v[gy * GRID_W + gx] * 0.5;
        for (let k = 1; k <= steps; k += 1) {
            s += sampleField(dens02, gx + ux * k, gy + uy * k);
        }
        return s * LIGHT_STEP_FRAC;
    }

03凝結する高さで底が平らになる綿雲

天気アプリの午後のにわか雨予報カードに向いています。空気のかたまり 70 個が buoyancy の浮力で上がり、CONDENSE_H 112px を越えたものだけが stampPuff で濃さを残すので、底は 10 秒間ずっと 110px の行にそろい、頂の近くの幅は 2 秒の 105px から 10 秒の 261px へ広がります。凝結する高さより上では MOIST_LAPSE 0.04 ずつしか冷えないのでさらに上がり、上向きの速さが SPREAD_SPEED 14 より遅くなると中心から遠いほうへ押し出されます。

凝結する高さで底が平らになる綿雲
新しいタブで開く

03 · 凝結する高さで底が平らになる綿雲 — 計測 累積面積 8.347% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CONDENSE_HMOIST_LAPSESPREAD_ACCEL
    function buoyancy(p) { return (p.temp - envTemp(p.h)) * BUOYANCY; }
    function stepParcel(p, dt) {
        const a = buoyancy(p) - DRAG * p.vh;
        p.vh += a * dt;
        const dh = p.vh * dt;
        p.temp -= dh > 0 ? coolBy(p.h, dh) : (dh * DRY_LAPSE);
        p.h += dh;
        let ax = Math.sin(sceneT * 1.7 + p.seed) * 4 - DRAG * p.vx;
        if (p.h > CONDENSE_H && p.vh < SPREAD_SPEED) {
            ax += (p.x >= SOURCE_X ? 1 : -1) * SPREAD_ACCEL;
        }
        p.vx += ax * dt;
        p.x += p.vx * dt;
        p.age += dt;
        if (p.h < 0) {
            p.h = 0;
            p.vh = 0;
        }
        if (p.age > 9 || p.x < -20 || p.x > W + 20 || (p.h < 4 && p.vh <= 0 && p.age > 1)) {
            spawn(p, 0);
        }
    }

04太陽の高さの分だけずれて丘を渡る雲の影

旅行紹介ページの草原のヒーローに向いています。shadowSourceX は地面の点から太陽のほうへ雲の層の高さまでさかのぼった場所を x + (CLOUD_H − h) / tan(高度角) で求め、太陽の高さ 50° では高さ 34px の地面が右 149.4px の雲の影を、126px の丘が右 72.2px の雲の影を受けます。sunFacing は斜面の法線と太陽の向きの内積で地面の明るさを決め、太陽に向いた斜面は平均 0.882、背を向けた斜面は平均 0.515 に分かれます。

太陽の高さの分だけずれて丘を渡る雲の影
新しいタブで開く

04 · 太陽の高さの分だけずれて丘を渡る雲の影 — 計測 累積面積 17.765% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SUN_ELEV_DEGCLOUD_HshadowSourceX
    // 地面の点 (x, h) から太陽のほうへ雲の層の高さまでさかのぼった場所 — x + (CLOUD_H − h) / tan(高度角)
    function shadowSourceX(x, h) { return x + (CLOUD_H - h) / Math.tan(toRad(SUN_ELEV_DEG)); }
    // 斜面の法線と太陽の向きの内積 — 1 なら太陽の正面、0 以下なら太陽に背を向けた斜面
    function sunFacing(x) {
        const s = terrainSlope(x);
        const v = sunVec();
        return (-s * v[0] + v[1]) / Math.hypot(s, 1);
    }

05高さごとに違う風で伸びる筋雲

瞑想アプリのスタート画面の空に向いています。windAt は高さの比 hr に SHEAR を掛けて、下 30px/秒から頂 48px/秒(1.6倍)まで風を強め、wispPhase は読む x をその風で押しながら横だけを 6 倍に引き伸ばして読みます。16 秒ごとに巻き戻す二枚の板を半周期ずらして混ぜ、押す距離がどこまでも大きくならないようにしてあり、位相のまん中(8 秒)の筋は 25.6° かたむいています。

高さごとに違う風で伸びる筋雲
新しいタブで開く

05 · 高さごとに違う風で伸びる筋雲 — 計測 累積面積 29.586% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

WIND_PXPHASE_SwindAt
    // 高さの比 hr での風(px/秒)
    function windAt(hr) { return WIND_PX * (1 + SHEAR * hr); }
    // 位相の板ひとつぶんの濃さ — 読む x をその高さの風で tp 秒ぶんさかのぼって押してから、横だけ STRETCH 倍に引き伸ばして読む
    function wispPhase(x, y, tp, salt) {
        const hr = 1 - y / H;
        const u = (x - tp * windAt(hr)) / (NOISE_PX * STRETCH);
        const v = y / NOISE_PX + salt;
        return layeredNoise(u, v, 4, 55);
    }
    // マス (x, y) の筋雲の濃さ — 二枚の位相の板を三角の重みで混ぜ、高さの比 0.28〜0.52 で少しずつ現れて、それより高い空にだけ残す
    function wispDensity(x, y, t) {
        const f = t / PHASE_S - Math.floor(t / PHASE_S);
        const g = (f + 0.5) - Math.floor(f + 0.5);
        const wa = 1 - Math.abs(2 * f - 1);
        const n = wispPhase(x, y, f * PHASE_S, 0) * wa + wispPhase(x, y, g * PHASE_S, 17.3) * (1 - wa);
        const hr = 1 - y / H;
        return band(0.52, 0.66, n) * band(0.28, 0.52, hr);
    }

06飛行機の窓から広がる雲海

航空券検索の最初の画面の背景に向いています。planeDistance は地平線より下の画素の傾きから平面までの距離を CAM_HEIGHT 40 / 傾き で求め、画面 y 300 は 44、y 120 は 4000 だけはなれた場所を読みます。遠いほどノイズの層数を 4 から 2 に減らし、霧の割合を 1 − e^(−z × FOG) で混ぜるので、y 120 の近くは 0.988 まで空の色に溶けます。

飛行機の窓から広がる雲海
新しいタブで開く

06 · 飛行機の窓から広がる雲海 — 計測 累積面積 33.032% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CAM_HEIGHTFOCAL_PXplaneDistance
    function buildSea(t) {
        const travel = t * FLY_SPEED;
        for (let gy = 0; gy < GRID_H; gy += 1) {
            const y = (gy + 0.5) * CELL;
            const z = planeDistance(y);
            for (let gx = 0; gx < GRID_W; gx += 1) {
                const i = gy * GRID_W + gx;
                seaZ[i] = z;
                if (z < 0) {
                    continue;
                }
                const X = ((gx + 0.5) * CELL - W / 2) / FOCAL_PX * z;
                const oct = octavesFor(z);
                const n = seaHeight(X, z + travel, oct);
                seaN[i] = n;
                seaLit[i] = clamp(0.5 + (n - seaHeight(X, z + travel + LIGHT_PROBE, oct)) * 6, 0, 1);
            }
        }
    }

07継ぎ目なく流れる重なった雲の帯

Webサイトのヘッダーに流れる背景に向いています。四つの重ねの上の線を bandProfile の周期ノイズ(PERIOD 8 マス)で作り、8 マス流れると最初の値に戻ります。深さ 1 の重ねは 89.6px/秒、深さ 4 は 12.8px/秒とおそくなり、空の色の割合は 0 から 0.72 に増え、一周は近い重ねが 5 秒、遠い重ねが 20 秒です。

継ぎ目なく流れる重なった雲の帯
新しいタブで開く

07 · 継ぎ目なく流れる重なった雲の帯 — 計測 累積面積 43.224% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LAYER_COUNTSKY_MIXperiodicLayered
    // 深さ depth の重ねが流れた距離(格子のマス)— PERIOD で余りにして数字がどこまでも大きくならないようにする
    function layerOffset(depth, t) {
        const s = t * layerSpeed(depth);
        return s - Math.floor(s / PERIOD) * PERIOD;
    }
    // 重ねの上の線の高さ(0〜1)— 周期をもつ重ねたノイズの合計
    function bandProfile(u, depth) { return periodicLayered(u, depth * 3.1, PERIOD, 4, 77); }

08乾いた空気と混ざって縁からほどける雲

ブログ記事の冒頭の季節が移る場面に向いています。stepGrid は濃さを近所の平均で広げ、空の近所が多い外側のマスほど蒸発を大きく削るので、乾いた 1 秒(0.5〜1.5 秒)に外側のマスは平均 0.334、内側のマスは 0.156 減り、外側が 2.1 倍速く乾きます。湿度が 4.6〜5.4 秒で 0.12 から 0.92 へ上がると中心の重みが大きいマスから満ち、5.6 秒には重み 0.9 超のマスの平均の濃さが 0.591、0.2〜0.5 のマスは 0.093 です。

乾いた空気と混ざって縁からほどける雲
新しいタブで開く

08 · 乾いた空気と混ざって縁からほどける雲 — 計測 累積面積 14.21% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

EVAP_RATEEMPTY_LEVELSPIKE_CAP_S
                const avg = (l + r + u + dn) / 4;
                let empty = 0;
                for (let oy = -1; oy <= 1; oy += 1) {
                    for (let ox = -1; ox <= 1; ox += 1) {
                        if (ox === 0 && oy === 0) {
                            continue;
                        }
                        const x = gx + ox;
                        const y = gy + oy;
                        const nb = (x < 0 || y < 0 || x >= GRID_W || y >= GRID_H) ? 0 : v[y * GRID_W + x];
                        if (nb < EMPTY_LEVEL) {
                            empty += 1;
                        }
                    }
                }
                let nv = v[i] + DIFFUSE * dt * (avg - v[i]);
                nv -= EVAP_RATE * (1 - hum) * (EDGE_BASE + empty / 8) * dryMix(gx, gy) * dt;
                nv += FILL_RATE * hum * core08[i] * (1 - v[i]) * dt;
                next08[i] = (nv < CUT_LEVEL && nv < v[i]) ? 0 : clamp(nv, 0, 2);
            }
        }
        v.set(next08);
    }

09雲の上の日の出のヒーロー場面

ポートフォリオ最初の画面のヒーローに向いています。06 の平面の上で sunBlock が太陽側へ 5 歩(SUN_STEPS)のあいだ光線より高くそびえる雲の高さを集め、sunTransmit が e^(−ABSORB_09 × S / 22) で透過を求めます。太陽が 0° のとき頂の上位 20% は透過 0.841、谷の下位 20% は 0.339 で頂から光が当たり、12° では 0.996 と 0.967 でほぼ同じになります。

雲の上の日の出のヒーロー場面
新しいタブで開く

09 · 雲の上の日の出のヒーロー場面 — 計測 累積面積 49.187% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SUN_STEPSAMBIENT_09sunTransmit
    // 頂の点 (X, Z, h) から太陽側(+Z、上へ tan 高度角)へ歩いて集めた遮り S — 光線より高くそびえる雲の分だけ足す
    function sunBlock(X, Z, h, elevDeg) {
        const rise = Math.tan(toRad(elevDeg)) * SUN_STEP;
        let s = 0;
        for (let k = 1; k <= SUN_STEPS; k += 1) {
            const ray = h + rise * k;
            const top = topHeight(X, Z + SUN_STEP * k, 2);
            if (top > ray) {
                s += top - ray;
            }
        }
        return s;
    }
    function sunTransmit(X, Z, h, elevDeg) { return Math.exp(-ABSORB_09 * sunBlock(X, Z, h, elevDeg) / TOP_HEIGHT); }

雲の影を描くとき、雲の位置に何を足せば地面の上に正しく落ちるのでしょうか?

雲の影は雲の真下に置くとずれます。04 は地面の点 (x, h) の影が x + (CLOUD_H − h) / tan(高度角) の場所の雲から来ると計算するので、雲の x に太陽の高さで決まる距離を足してはじめて影が合う場所に落ちます。

高い丘ほど雲に近い影

同じ太陽の高さ 50° で、高さ 34px の地面は雲と 149.4px はなれた影を受け、80px は 110.8px、126px は 72.2px に縮みます。丘の頂が雲の層に近いほど、太陽のほうへさかのぼる高さが短くなるからです。

太陽は地面から測った角で置く

SUN_ELEV_DEG は地面から測った高度角です。角が小さいほど tan が小さくなり、割り算の結果であるずれの距離が長くなるので、真上から測った角と取りちがえるとずれの距離の計算が合わなくなります。

背景の上にのせるとき

雲を背景に敷くときは、04 のように計算の格子を画面より小さくして引き伸ばして描くほうが軽くなります。旅行紹介ページの草原のヒーローなら、丘の高さ h(x) をページの地形の絵に合わせ、状態の文は左上、一時停止は右上の位置のまま使えます。

ノイズで濃さを作り、太陽のほうへ歩いて光を削り、風で押して運びます — ノイズ一枚が雲になるまでの順番です

雲になる道は四段です。濃さを作り、太陽のほうへ歩いて光を削り、風で押し、小さい格子で計算して引き伸ばして描きます。

段階 すること 使う名前 使う場面
1 濃さを作る 重ねたノイズを層ごとに頻度 2 倍・強さ半分で足して 0〜1 に合わせる layeredNoise 02〜09
2 光を削る 太陽のほうへ歩きながら濃さを足し、指数で削る lightSum・sunBlock 02・09
3 風で押す 読む座標を流れた距離だけ押して雲を流す windAt・layerOffset 05・07
4 引き伸ばして描く 小さい格子の画素を作って 480×300 に拡大する makePix・putPx・blit 01〜09

重ねたノイズの合計が雲の筋になる理由

ノイズ一層ではなめらかな丘しかできません。層ごとに頻度を 2 倍、強さを半分にして重ね、強さの合計で割ると、大きなかたまりの上に細かい筋がのりながら値は 0〜1 にとどまります。

光を足して指数で削る理由

透過を e^(−ABSORB × S) と置くと、濃さ 0.5 を二回通った光と 1 を一回通った光が同じになります。歩数を決めて濃さを足すだけで済むので、マスごとに太陽のほうへ 6 歩進むだけで、雲の中は暗く薄い縁は明るい形が出ます。

風で押すと雲が流れる理由

雲 動く理由 は、読む座標を風で押しているからです。05 は高さごとに違う風で、07 は重ねごとに違う速さで押すので、雲全体がひとつの絵のようには動かず奥行きが出ます。

小さい格子で計算して引き伸ばす理由

画素の計算はマスの数に比例します。場面ごとに 120×150 から 240×150 の小さい格子で計算して drawImage で引き伸ばせば、雲のやわらかい境界は拡大が代わりに作ってくれます。

雲が壊れるのは、継ぎ目でぷつりと切れるくり返し・太陽に背を向けた側で真っ黒に沈む光・隠れたタブのあとで一気に跳ねる雲です

継ぎ目で切れるくり返し

07 の上の試験帯二本は、同じ u を別々のノイズに入れたものです。流れた距離を PERIOD で余りにしても周期のないノイズは、u が 8 から 0 へ戻る場所で別の格子の値を読み、u 7.999 と 0 の値の差(64 本の高さの行)が平均 0.175、最大 0.553 になります。周期ノイズは平均も最大も 0 で、高さ 17px の試験帯で周期なしは継ぎ目で 5px ぷつりと切れ、周期ノイズは 0px です。

下の seamJump は画面からは呼ばれず、この値を測るための試験用の関数です。

    // 失敗版 — 同じ場所を周期のない重ねたノイズの合計で読む(u を PERIOD で余りにした値をそのまま入れる)
    function bandProfileNaive(u, depth) {
        const m = u - Math.floor(u / PERIOD) * PERIOD;
        return layeredNoise(m, depth * 3.1, 4, 77);
    }
    // 継ぎ目の値の差 — u が PERIOD の直前(PERIOD − 0.001)と 0 で読んだ値の差
    function seamJump(read, depth) {
        return Math.abs(read(PERIOD - 0.001, depth) - read(0, depth));
    }

太陽に背を向けた側で真っ黒に沈む光

02 の左のパネルは透過だけで塗った版なので、太陽に背を向けた雲の中で光が指数で減って底を打ちます。右のパネルは paintPanel02 に AMBIENT_LIGHT 0.6 を渡して空の光の下限を足し、同じ濃さで雲のマスの最低の明るさが 0.029 から 0.629 に上がり、明るさ 0.15 未満のマスが 80.4% から 0% に消えます。09 も同じ直しを AMBIENT_09 で行い、日の当たらない谷の明るさの割合は下限が 0 なら 0.2、あれば 0.88 です。

                const dv = dens02.v[i];
                if (dv > 0) {
                    const L = trans02[i] * (BASE_LIGHT + forwardPhase(x, y));
                    const cl = [
                        SUN_TINT[0] * L * 1.15 + AMBIENT_TINT[0] * ambient,
                        SUN_TINT[1] * L * 1.15 + AMBIENT_TINT[1] * ambient,
                        SUN_TINT[2] * L * 1.15 + AMBIENT_TINT[2] * ambient,
                    ];
                    c = mix(c, cl, 1 - Math.exp(-dv * OPACITY));
                }

隠れたタブのあとに一気に跳ねる雲

08 は演出 2 秒に 1.2 秒ぶんのフレームが入る場面をわざと作ります。直した版は SPIKE_CAP_S 0.05 秒で切って拡散の割合 0.04 の一歩だけ進み、失敗版は 1.2 秒を一歩に使って割合が 0.96 になり、濃さが 0.2 を超えて変わったマスが格子全体の割合で 0% 対 6.82% になります。そのあと失敗版は空のマスが増えて蒸発が速まり、4 秒は 22.4 対 17.4%、5 秒は 18.2 対 12.1% と開いたあと、湿った 6 秒以降は 26.7 対 26.6% でまたそろいます。

    function update(dt) {
        const hum = humidityAt(sceneT);
        mixT08 = sceneT;
        if (lastT08 < SPIKE_AT && sceneT >= SPIKE_AT) {
            stepGrid(fix08, Math.min(HIDDEN_FRAME_S, SPIKE_CAP_S), hum);
            stepGrid(fail08, HIDDEN_FRAME_S, hum);
        }
        lastT08 = sceneT;
        stepGrid(fix08, dt, hum);
        stepGrid(fail08, dt, hum);
    }

実際にタブが隠れるとフレームのループが止まり、戻るときは lastTs を −1 に空けたまま DT_MAX 0.05 秒でもう一度切って使います。SPIKE_CAP_S と DT_MAX はどちらも 0.05 秒ですが、前者は 08 の演出だけ、後者はフレームの時間に効く別々の定数です。圧縮ファイルは暗号 n97f273v で開き、07・02・08 のフォルダーの index.html を一時停止ボタンで止めておくと、壊れた版と直した版が分かれる場所を目で確かめられます。

吸収を上げると底はどれだけ暗くなるのでしょうか — 吸収率・太陽の高さ・高さごとの風の差を変えて比べます

名前 変えた値 感じ
光をもっと吸わせる 02 ABSORB 0.9 → 1.6 濃さの合計 1 を通った光が 41% から 20% に減り、雲の中の透過が平均 0.14 から 0.04 に下がって、環境光なしの版のほぼ真っ黒なマスが雲の 80% から 91% に増え、薄い縁の明るさも 0.88 から 0.79 に下がって銀の縁取りが細くなります
太陽を低く 04 SUN_ELEV_DEG 50 → 20 高度角のタンジェントが 1.19 から 0.36 に下がり、影が雲からずれる距離が 72〜149px から 236〜489px へ三倍以上遠くなり、太陽に向いた斜面と背を向けた斜面の明るさが 0.88・0.52 から 0.53・0.12 に開いて、日の当たらない地面が 0.2% から 15% に増えます
高さごとの風の差を大きく 05 SHEAR 0.6 → 1.4 上の風が下の 1.6 倍から 2.4 倍(毎秒 48px から 72px)になり、位相のまん中(8 秒)で筋の傾きが 25.6° から 48.2° に寝て、高さの比 0.3 の筋と頂の筋のずれが 100.8px から 235.2px に開きます

値を変える前に見ること

02 の ABSORB は指数の中に入るので、1.6 に上げると雲の中の透過が掛け算で急に落ちます。04 の SUN_ELEV_DEG は地面から測った角なので、20 に下げると tan が小さくなって割り算の結果が大きくなります。05 の SHEAR は高さの比に掛かるので、1.4 では頂の風が下の 2.4 倍になります。

動きを減らす設定では止まった空が一枚です — いまの空を読み上げる文と一時停止ボタンのアクセシビリティです

状態の文と一時停止

九つの場面のキャンバスは role="img" と場面の説明の aria-label をもち、状態の行は「雲の量 40% · 太陽の高さ 50° · 風 右へ」の形でいまの空を文で知らせます。一時停止ボタンは Space・Enter で押すと aria-pressed が変わってループが止まり、フォーカスは 3px の外枠で見えます。演出中の状態の行は aria-live が off で、最初の入力のあとに polite に変わります。

動きを減らす設定

prefers-reduced-motion: reduce のときは九つの場面ともループを止め、代表の瞬間の一枚だけを描きます。その瞬間は 01 が 1.2 秒・02 が 2 秒・03 が 8 秒・04 が 3 秒・05 が 8 秒・06 が 2 秒・07 が 3 秒・08 が 4 秒・09 が 8 秒で、流れ・ふくらみ・ほどけは出ません。設定を戻すとまた動き、規格は MDN の prefers-reduced-motion にあります。

雲 アニメーション css のよくある質問 — CSSだけで作る方法・スマホでの負荷・背景として敷く方法

css 背景 雲 の指定だけで、この記事と同じ雲を作れますか

グラデーションを重ねた止まった雲は CSS だけで作れますが、この記事の場面はマスごとに濃さと光を計算するので、キャンバスと JavaScript が合います。原理が分からなくても ABSORB・SUN_ELEV_DEG・SHEAR のような定数を変えるだけで別の空になります。

スマホでも動かせますか

キャンバスの内部座標は 480×300 に固定で、devicePixelRatio は 2 までしか使わず、画素の計算は 120×150〜240×150 の小さい格子で行います。それでも重いときは、格子のマス数を減らすところから試してみてください。

ページの背景として敷いてもいいですか

06・07・09 はヘッダーやヒーローの背景に向いていて、タブが隠れるとループが止まります。文字を読む区間では、雲の上に半透明の板を一枚置いて文字のコントラストを守るほうがよいです。

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

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