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

css 雨を降らせる 9パターン — 窓辺のストーム背景

このcss 雨を降らせるコレクションは、窓辺の嵐の場面を9つの独立したキャンバス層に分け、一本のループ動画ではなく層ごとに別々の計算で動かしています。この背景シーンでは、ひとつの風ベクトル値が九つの雨層の傾きと速度を同時に決めている。以下の各見出しでは、その値を実際に読んでいる関数をひとつずつコードで開いていきます。

九つの雨レイヤーをこの場で動かして確かめる

下のグリッドをスクロールすると、9つのキャンバスはすでにそれぞれ自分のループで動いています — クリックを待つものはありません。

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

css 雨を降らせる 9パターン — 層ごとの計算が重なって嵐になる

このセットは奥行きで分かれた雨筋の層、時間で向きが変わる風ベクトル、合体する窓の水滴、地面の飛沫、ゆっくり周期を描く強さ、枝分かれする雷、ポインターで傾く風、傘による遮蔽、そして4つの技法を重ねたヒーロー場面まで、一式そろっています。同じ「層を重ねて一つの場面にする」考え方はhtml5 canvas アニメーション9種 — 水滴とブラックホールでも詳しく扱っています。下の各項目を開くと、それを生み出している関数をそのまま読めます。

01層ごとに太さと速度が違う雨筋パララックス背景

雨筋ごとに仮の距離を与え、太さと落下速度をその距離に反比例させることで、近い筋は太く速く、遠い筋は細くゆっくり落ちます。streakDepthZ() が RAIN_LAYER_COUNT までの各層でその深さを計算し、できた streaks 配列は深さ順に並べ替えられるため、遠い層から先に描かれて近い層がその上に重なります。

層ごとに太さと速度が違う雨筋パララックス背景
新しいタブで開く

01 · 層ごとに太さと速度が違う雨筋パララックス背景 — 計測 累積面積 63.301% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

streakDepthZRAIN_NEAR_SPEED_PX_SRAIN_LAYER_COUNT
  function streakDepthZ(layerIndex) {
    return RAIN_LAYER_COUNT <= 1 ? 1 : layerIndex / (RAIN_LAYER_COUNT - 1);
  }

  var streaks = [];
  for (var li = 0; li < RAIN_LAYER_COUNT; li += 1) {
    var z = streakDepthZ(li);
    var count = Math.round(32 - z * 18);
    var speed = RAIN_FAR_SPEED_PX_S + (RAIN_NEAR_SPEED_PX_S - RAIN_FAR_SPEED_PX_S) * z;
    var thick = 0.5 + z * 2.1;
    var alpha = 0.28 + z * 0.62;
    for (var i = 0; i < count; i += 1) {
      streaks.push({
        x: rRand() * CALC_W,
        y: rRand() * CALC_H,
        z: z, speed: speed, thick: thick, alpha: alpha,
        len: 4 + z * 12
      });
    }
  }
  streaks.sort(function (a, b) { return a.z - b.z; });

項目01はフレームの63.3%を平均強度111.8で塗り替え、23/23フレームで動きが測定されました。

02時間とともに向きが変わる風の雨背景

ゆっくり揺れる波の値で風の強さと角度を計算し続け、雨筋全体の傾きと落下速度が同じ風の値に合わせて一緒に傾きます。windGustAngle() は RAIN_WIND_CYCLE_MS ごとに異なる周期で回る二つのサイン波を合成し、その結果を WIND_GUST_MAX_DEG で上限まで拡大しています。

時間とともに向きが変わる風の雨背景
新しいタブで開く

02 · 時間とともに向きが変わる風の雨背景 — 計測 累積面積 57.836% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

windGustAngleWIND_GUST_MAX_DEGRAIN_WIND_CYCLE_MS
  function windGustAngle(nowMs) {
    var a = Math.sin(nowMs / RAIN_WIND_CYCLE_MS * Math.PI * 2) * 0.62;
    var b = Math.sin(nowMs / (RAIN_WIND_CYCLE_MS * 1.73) * Math.PI * 2 + 1.3) * 0.38;
    return (a + b) * WIND_GUST_MAX_DEG;
  }

  var streaks = [];
  for (var i = 0; i < RAIN_STREAK_COUNT; i += 1) {
    streaks.push({
      x: rRand() * CALC_W,
      y: rRand() * CALC_H,
      speed: RAIN_FALL_SPEED_PX_S * (0.6 + rRand() * 0.7),
      thick: 0.8 + rRand() * 1.4
    });
  }

  var elapsed = 0;
  function stepSim(dtS, nowMs) {

項目02はフレームの57.8%を平均強度104.1で塗り替え、23/23フレームで動きが測定されました。

03流れ落ちながら合わさる窓の水滴

水滴ごとに重さと抵抗を計算して流れ落ちる速度を決め直し、通り道で別の水滴と重なると二つを一つに合わせて大きくします。dropletMassMerge() が二つの質量を合算して小さい方の vy を捨て、DROPLET_GRAVITY_PX_S から求めた目標速度に stepSim() が毎フレーム近づけています。

流れ落ちながら合わさる窓の水滴
新しいタブで開く

03 · 流れ落ちながら合わさる窓の水滴 — 計測 累積面積 8.967% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

dropletMassMergeDROPLET_GRAVITY_PX_SDROPLET_MERGE_DIST_PX
  function dropletMassMerge(a, b) {
    var mass = a.mass + b.mass;
    var r = Math.sqrt(mass) * 0.92;
    return {
      x: (a.x * a.mass + b.x * b.mass) / mass,
      y: Math.max(a.y, b.y),
      r: r, mass: mass, vy: Math.max(a.vy, b.vy), seed: a.seed
    };
  }

  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    spawnAcc += dtMs * DROPLET_SPAWN_PER_S / 1000;
    while (spawnAcc >= 1 && drops.length < 46) { spawnDroplet(); spawnAcc -= 1; }
    for (var i = 0; i < drops.length; i += 1) {
      var d = drops[i];
      var targetVy = (DROPLET_GRAVITY_PX_S * Math.sqrt(d.mass)) / DROPLET_DRAG_COEF;
      d.vy += (targetVy - d.vy) * Math.min(1, 5 * dtS);
      d.y += d.vy * dtS;
      d.x += Math.sin(nowMs * 0.0012 + d.seed) * 1.6 * dtS;
    }
    // 合体 — 重なった水滴同士を一つに
    for (var a = 0; a < drops.length; a += 1) {
      for (var b = a + 1; b < drops.length; b += 1) {
        var p1 = drops[a], p2 = drops[b];

項目03はフレームの9.0%を平均強度80.9で塗り替え、23/23フレームで動きが測定されました。

04落ちた場所ごとに跳ねる雨のしぶき

雨粒が地面に触れた瞬間に方向と速度をランダムに分けた複数の飛沫粒子を生み、各粒子に毎フレーム重力を足し直すことで放物線を描いて散らばります。splashBurst() は着地のたびに SPLASH_PARTICLE_COUNT 個の粒子を生成し、stepSim() が雨筋の到達を検知するたびに SPLASH_GRAVITY_PX_S2 がその粒子を引き戻します。

落ちた場所ごとに跳ねる雨のしぶき
新しいタブで開く

04 · 落ちた場所ごとに跳ねる雨のしぶき — 計測 累積面積 48.23% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPLASH_PARTICLE_COUNTsplashBurstSPLASH_GRAVITY_PX_S2
  function splashBurst(x, nowMs) {
    for (var i = 0; i < SPLASH_PARTICLE_COUNT; i += 1) {
      var ang = Math.PI + sRand() * Math.PI; // 上半円に広がる
      var speed = 16 + sRand() * 26;
      splashes.push({
        x: x, y: GROUND_Y,
        vx: Math.cos(ang) * speed,
        vy: Math.sin(ang) * speed,
        born: nowMs
      });
    }
  }

  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    for (var i = 0; i < streaks.length; i += 1) {
      var s = streaks[i];
      s.y += s.speed * dtS;
      if (s.y >= GROUND_Y) {
        splashBurst(s.x, nowMs);

項目04はフレームの48.2%を平均強度78.5で塗り替え、23/23フレームで動きが測定されました。

05小雨から豪雨まで強さが行き来する場面

ゆっくり周期を描く一つの関数で嵐の強さを上下させ続け、その値に合わせて雨筋の数と風の強さを毎フレーム計算し直すことで場面全体が弱まったり強まったりします。stormIntensity() は STORM_CYCLE_MS の周期で動き、stepSim() がその値を読んで風のオフセットと RAIN_SPAWN_RATE_BASE 付近まで落ちる雨筋の数の両方を決めています。

小雨から豪雨まで強さが行き来する場面
新しいタブで開く

05 · 小雨から豪雨まで強さが行き来する場面 — 計測 累積面積 60.446% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

stormIntensitySTORM_CYCLE_MSRAIN_SPAWN_RATE_BASE
  function stormIntensity(nowMs) {
    return Math.sin((nowMs / STORM_CYCLE_MS) * Math.PI * 2 - Math.PI / 2) * 0.5 + 0.5;
  }

  var streaks = [];
  for (var i = 0; i < RAIN_POOL_SIZE; i += 1) {
    streaks.push({
      x: rRand() * CALC_W,
      y: rRand() * CALC_H,
      thresh: rRand(),
      speed: 90 + rRand() * 90,
      thick: 0.7 + rRand() * 1.3
    });
  }

  function stepSim(dtS, nowMs) {
    var inten = stormIntensity(nowMs);
    var wind = (inten - 0.5) * 20;
    for (var i = 0; i < streaks.length; i += 1) {
      var s = streaks[i];

項目05はフレームの60.4%を平均強度122.6で塗り替え、23/23フレームで動きが測定されました。

06枝分かれして伸びる雷と画面フラッシュ

雷の経路は中間点を繰り返しずらして枝分かれさせて描き、同時に画面の明るさを急に上げてゆっくり戻す曲線で計算し、光る瞬間を作ります。branchLightning() は LIGHTNING_BRANCH_DEPTH まで再帰し、稲妻が着地するとフラッシュの明るさは LIGHTNING_FLASH_DECAY_MS をかけて消えていきます。

枝分かれして伸びる雷と画面フラッシュ
新しいタブで開く

06 · 枝分かれして伸びる雷と画面フラッシュ — 計測 累積面積 73.354% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LIGHTNING_FLASH_DECAY_MSbranchLightningLIGHTNING_BRANCH_DEPTH
  function branchLightning(segs, x, y, ang, len, depth) {
    if (depth <= 0 || len < 3) return;
    var steps = 3 + Math.floor(rRand() * 2);
    var cx = x, cy = y, cang = ang;
    for (var i = 0; i < steps; i += 1) {
      cang += (rRand() - 0.5) * 0.9;
      var stepLen = len / steps;
      var nx = cx + Math.sin(cang) * stepLen;
      var ny = cy + Math.cos(cang) * stepLen;
      segs.push({ x0: cx, y0: cy, x1: nx, y1: ny, depth: depth });
      if (depth > 1 && rRand() < 0.3) {
        var side = rRand() < 0.5 ? -1 : 1;
        branchLightning(segs, nx, ny, cang + side * (0.6 + rRand() * 0.5), len * 0.5, depth - 1);
      }
      cx = nx; cy = ny;
    }
  }

  function makeBolt() {
    var segs = [];
    var startX = 24 + rRand() * 112;

項目06はフレームの73.4%を平均強度51.7で塗り替え、23/23フレームで動きが測定されました。

07ポインター位置に風が傾く背景

ポインターが画面中央からずれた分だけ風ベクトルの向きを計算し直し、ポインターが動く間は雨筋全体がその方向に一緒に傾いて落ちます。windLeanTarget() がそのずれを傾き角に変換し、POINTER_WIND_LERP が毎フレーム現在の傾きをその目標へ近づけ、ポインター入力がないときは virtualPointerSweep() が同じ目標を代わりに動かします。

ポインター位置に風が傾く背景
新しいタブで開く

07 · ポインター位置に風が傾く背景 — 計測 累積面積 16.376% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

windLeanTargetPOINTER_WIND_LERPvirtualPointerSweep
  function windLeanTarget(normX) {
    return normX * WIND_LEAN_MAX_DEG;
  }

  function virtualPointerSweep(nowMs) {
    return Math.sin((nowMs / SWEEP_PERIOD_MS) * Math.PI * 2);
  }

  var leanDeg = 0;
  function stepSim(dtS, nowMs) {
    var normX = manual ? pointerNormX : virtualPointerSweep(nowMs);
    var target = windLeanTarget(normX);
    leanDeg += (target - leanDeg) * Math.min(1, POINTER_WIND_LERP * (dtS * 60));

項目07はフレームの16.4%を平均強度62.6で塗り替え、23/23フレームで動きが測定されました。

08傘の後ろでは隠れる雨筋

傘のシルエットの後ろを通る雨筋は描かずに隠し、傘面に当たった雨粒は当たった位置から計算した角度で跳ね返らせます。umbrellaOcclusion() は傘の曲線状の縁と雨筋の座標を毎フレーム照合し、umbrellaBounce() は命中を検知するたびに UMBRELLA_BOUNCE_COUNT 個の粒子を外側へ生み出します。

傘の後ろでは隠れる雨筋
新しいタブで開く

08 · 傘の後ろでは隠れる雨筋 — 計測 累積面積 40.252% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

umbrellaOcclusionUMBRELLA_BOUNCE_COUNTumbrellaBounce
  function umbrellaOcclusion(x, y, umbX) {
    var dx = (x - umbX) / UMBRELLA_RADIUS_PX;
    if (Math.abs(dx) > 1) return false;
    var edgeY = umbY - UMBRELLA_HEIGHT_PX * Math.sqrt(Math.max(0, 1 - dx * dx));
    return y >= edgeY && y <= umbY;
  }

  function umbrellaBounce(x, y, umbX) {
    var dx = (x - umbX) / UMBRELLA_RADIUS_PX;
    var nx = dx;
    for (var i = 0; i < UMBRELLA_BOUNCE_COUNT; i += 1) {
      var spread = (rRand() - 0.5) * 40;
      bounces.push({
        x: x,
        y: y,
        vx: nx * (40 + rRand() * 30) + spread,
        vy: -(30 + rRand() * 40),
        born: 0,
        life: 380 + rRand() * 200
      });
    }
  }

  function stepSim(dtS, nowMs) {

項目08はフレームの40.3%を平均強度52.7で塗り替え、23/23フレームで動きが測定されました。

09層・風・水滴・雷を一画面にまとめた嵐のヒーローシーン

01の層の視差、02の突風、03の窓の水滴、06の稲妻のフラッシュを一つの画面で同時に計算し、嵐の窓辺の完成シーンを作ります。この場面は windGustAngle() と dropletMassMerge() を HERO_WIND_GUST_MAX_DEG や HERO_GLASS_TOP といった専用の定数でローカルに再実装しており、自身のポインターリスナーは持ちません — このセットの中でポインターやキーボード入力を読むのは項目07だけです。

層・風・水滴・雷を一画面にまとめた嵐のヒーローシーン
新しいタブで開く

09 · 層・風・水滴・雷を一画面にまとめた嵐のヒーローシーン — 計測 累積面積 73.926% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HERO_WIND_GUST_MAX_DEGHERO_LIGHTNING_FLASH_DECAY_MSHERO_GLASS_TOP
  function windGustAngle(nowMs) {
    var a = Math.sin((nowMs / HERO_WIND_CYCLE_MS) * Math.PI * 2) * 0.7;
    var b = Math.sin((nowMs / (HERO_WIND_CYCLE_MS * 0.37)) * Math.PI * 2) * 0.3;
    return (a + b) * HERO_WIND_GUST_MAX_DEG;
  }

  var droplets = [];
  var dropletSpawnAcc = 0;
  function spawnDroplet() {
    droplets.push({
      x: HERO_GLASS_X0 + rRand() * (HERO_GLASS_X1 - HERO_GLASS_X0),
      y: HERO_GLASS_TOP + rRand() * 3,
      vy: 0,
      r: 0.9 + rRand() * 1.1
    });
  }
  var HERO_GLASS_X0 = CALC_W * 0.06, HERO_GLASS_X1 = CALC_W * 0.94;

  function dropletMassMerge(a, b) {
    var ar = a.r * a.r, br = b.r * b.r;
    a.r = Math.sqrt(ar + br) * 0.86;
    a.vy = (a.vy * ar + b.vy * br) / (ar + br);
  }

項目09はフレームの73.9%を平均強度83.5で塗り替え、23/23フレームで動きが測定されました。

層の技法別に見るコード

9行をまとめて見ると、合成シーン(09)と枝分かれの多い2つの層(06の雷、05の嵐周期)は再描画の割合が上位近くに来ます。雷は一撃で画面の縦に長いくさび形を塗り、嵐周期は雨筋の集合をまとめて毎フレーム動かすため、面積そのものが広くなるからです。項目03の水滴が最下位なのは、ガラスの上の小さな円がいくつかあるだけで、合体の計算自体は毎フレーム走っていても塗る面積は雨筋の集団よりずっと狭いからです。平均強度は再描画の面積とはまったく連動しません — 項目05は再描画割合が中程度にもかかわらず平均強度(122.6)が最も高く、これは測定フレームの間に雨筋が速く動いて強く着地するためです。逆に項目06の平均強度(51.7)は再描画割合が大きくても低く抑えられます。ボルトの形自体はほぼ静止していて、フラッシュのオーバーレイ部分だけが明るさを変えているからです。このようなcss アニメーション 雨の計算差は、項目ごとの構造を見比べるとはっきり分かります。

項目 層 再描画面積 平均強度 動いたフレーム
01 深さパララックス雨筋 63.3% 111.8 23/23
02 風の突風ドリフト 57.8% 104.1 23/23
03 窓の水滴トリクル 9.0% 80.9 23/23
04 地面の飛沫物理 48.2% 78.5 23/23
05 嵐の強さ周期 60.4% 122.6 23/23
06 雷の枝分かれフラッシュ 73.4% 51.7 23/23
07 ポインター風の傾き 16.4% 62.6 23/23
08 傘による遮蔽 40.3% 52.7 23/23
09 ヒーロー合成シーン 73.9% 83.5 23/23

値が嵐の強さを変える場所 — 風・間隔・雷・跳ね

三つの定数が、それぞれの技法をどこまで読ませると別の嵐に見え始めるかを決めています。項目02の WIND_GUST_MAX_DEG は突風で雨をどれだけ強く傾けられるかの上限を決め、項目04の SPLASH_PARTICLE_COUNT は雨筋が着地したときに地面がどれだけ混み合うかを決め、項目06の LIGHTNING_FLASH_DECAY_MS は雷が落ちたあと画面がどれだけ明るいままでいるかを制御します。三つとも図形やタイミングのループ自体には触れず、同じ stepSim() が毎フレーム読んでいる数値をただ拡大縮小しているだけで、これは物理シミュレーション 作り方 9選 — バネから慣性スクラブまでで扱ったバネや慣性の定数と同じ考え方です。下の変形の表で、それぞれが他の8層から独立して動く様子を試せます。

どこで壊れるか — 動作を減らす設定・狭い画面・層が多いとき

9つのキャンバスはすべて、ループを始める前に matchMedia で prefers-reduced-motion を確認します。動作を減らす設定を有効にした訪問者には、動く嵐ではなく静止した1フレームが表示されます — これは層ごとの任意設定ではなく、どの項目のセットアップコードにも同じ形で組み込まれています。狭いビューポートでは fit() によってキャンバスは縮小しますが、重い層(01・02・05・09)を1ページに何枚も重ねるとその分だけ積み上がります — 層ごとに独自の requestAnimationFrame ループが走るため、重い層4枚を同じスクロール位置に置くと1枚分のおよそ4倍のフレームごとの計算になります。項目09はすでに単体で2技法分の雨筋と水滴を1ティックで計算しているため、これを01や05と同じ画面に組み合わせるのが、遅い端末で目に見えてカクつく最も起きやすい組み合わせです — 対策は画面ごとに重い層を1枚に絞ることです。この css 背景 雨の9項目それぞれの背後にある index.html は、比べられるようにzipにまとめてパスワード qms3kcmq で提供しています。

変形3つとアクセシビリティ

変形 値 感じ方
最大風傾斜角を調整する 02 WIND_GUST_MAX_DEG 8 → 20 値を小さくすると風が強く吹いても雨筋はほぼ垂直に落ち、値を大きくすると風が少し吹いただけで雨筋が大きく傾いて落ちる。
飛沫粒子の数を調整する 04 SPLASH_PARTICLE_COUNT 6 → 14 値を小さくすると水滴が触れても飛沫はほとんど見えず、値を大きくすると地面全体が跳ねる粒子で埋まり豪雨のように見える。
雷フラッシュの減衰時間を調整する 06 LIGHTNING_FLASH_DECAY_MS 220 → 500 値を小さくすると雷は一瞬だけ光ってすぐ暗くなり、値を大きくすると光った明るさがしばらく画面に残り残光のように見える。

ここでのアクセシビリティは後付けの別レイヤーではありません — 9つすべてのCanvas APIループはprefers-reduced-motionの確認から始まり、その確認が通ったときだけrequestAnimationFrameを呼びます。項目07はこのセットの中で直接操作を前提に作られた唯一の項目です — ポインターを動かすか左右の矢印キーを押すと、風がその方向へリアルタイムに傾きます。キャンバスにはフォーカス可能でラベル付きの要素があり、マウスを使わないキーボードだけの訪問者でも同じ傾きを起こせます。残り8項目は操作するコントロールのない提示型の場面なので、そのアクセシビリティ責任は動作を減らす設定を正しく尊重することだけで完結します。

css アニメーション 雨 は窓辺の場面で何を計算しているのか?

上の9項目はどれも、同じ問いに別の答えを出しています — 少し前に読んだ風・重力・強さの値を踏まえて、このピクセルは今このフレームで何をすべきか、という問いです。9つのループのどれも録画したパスを再生してはいません — それぞれ雨筋の位置・水滴の大きさ・飛沫の方向・雷の形を、毎ティック計算し直すわずかな数値から導き出しており、これはcss 波紋 アニメーション 9パターン — キャンバスで計算するピクセル波と同じ「記録ではなく計算」の考え方です。上の変形の表が、絵を描き直すのではなく定数をひとつ書き換えるだけで層全体の雰囲気を変えられるのも、そもそも形が固定されていないからです。

この雨のcss場面を使う前によくある質問

この cssアニメーション 雨 は実際の気象データを読んでいますか?

いいえ。風の角度・嵐の強さ・雷のタイミングなど、すべての値はシード付きの疑似乱数生成と RAIN_WIND_CYCLE_MS や STORM_CYCLE_MS のような固定周期のサイン波から作られており、外部のデータは一切使っていません。

項目09は01・02・03・06を自分で組み合わせたものと何が違いますか?

項目09は実行時に他の4項目の関数を呼び出しているわけではありません — windGustAngle() と dropletMassMerge() を HERO_WIND_GUST_MAX_DEG などヒーロー専用の定数で独自にローカル実装しているため、技法の考え方は同じでも数値は別々に調整されています。

この9項目のうち1つだけを既存のページに入れられますか?

はい — 各項目のキャンバス・seedRand() のセットアップ・stepSim() と draw() の組は、それぞれの index.html の中で完結しています。1項目をコピーしても、残り8項目のコードを一緒に持ち込む必要はありません。

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

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