降雪・雨エフェクト切替

ゆらゆら舞う雪と斜めに降る雨をボタンで切り替えられる天候エフェクト。季節のランディングページやヒーロー背景に重ねて使えます。

#canvas#particles#animation#weather

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW:冬季限定メニューのヒーロー(雪/雨 切替) -->
<section class="mb-winter">
  <!-- 主役:天候エフェクト(雪・雨) -->
  <canvas class="mb-winter__fx" id="mbSnow"></canvas>

  <!-- 前景UI:冬の告知 -->
  <div class="mb-winter__inner">
    <span class="mb-winter__tag">WINTER LIMITED</span>
    <h1 class="mb-winter__title">雪の日の、<br>あたたかな一杯。</h1>
    <p class="mb-winter__lead">スパイス香る「ホットモカ・キャラメル」が今だけ登場。窓の外を眺めながら、ゆっくりと。</p>

    <div class="mb-winter__menu">
      <div class="mb-item"><b>ホットモカ</b><span>¥620</span></div>
      <div class="mb-item"><b>ジンジャーラテ</b><span>¥600</span></div>
    </div>

    <div class="mb-winter__ctrl">
      <button class="mb-toggle is-on" id="mbBtnSnow" type="button">❄ 雪</button>
      <button class="mb-toggle" id="mbBtnRain" type="button">☂ 雨</button>
    </div>
  </div>
</section>
CSS
/* MOON BREW:冬の天候エフェクト付きヒーロー */
:root {
  --cream: #f5ede1;
  --brown: #2b1d12;
  --amber: #c98a3b;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.mb-winter {
  position: relative;
  height: 400px;
  overflow: hidden;
  color: #fff;
  background:
    linear-gradient(180deg, rgba(43,29,18,0.45), rgba(43,29,18,0.78)),
    url("https://picsum.photos/700/500?random=51") center/cover no-repeat;
}

/* 主役:雪/雨キャンバス(写真とUIの間) */
.mb-winter__fx {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
  pointer-events: none;
}

.mb-winter__inner {
  position: relative;
  z-index: 2;
  padding: 34px 30px;
  max-width: 430px;
}
.mb-winter__tag {
  display: inline-block;
  font-size: 10px;
  letter-spacing: 0.3em;
  color: var(--amber);
  font-weight: 700;
  font-family: "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
}
.mb-winter__title {
  margin: 12px 0 12px;
  font-size: 30px;
  line-height: 1.4;
  font-weight: 700;
  font-family: "Hiragino Mincho ProN", "Yu Mincho", serif;
}
.mb-winter__lead {
  margin: 0 0 20px;
  font-size: 13px;
  line-height: 1.8;
  color: rgba(255,255,255,0.9);
  max-width: 350px;
}

.mb-winter__menu { display: flex; gap: 14px; margin-bottom: 22px; flex-wrap: wrap; }
.mb-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 9px 16px;
  border-radius: 12px;
  background: rgba(245,237,225,0.14);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(255,255,255,0.18);
}
.mb-item b { font-size: 13.5px; }
.mb-item span { font-size: 13px; color: var(--amber); font-weight: 700; }

.mb-winter__ctrl { display: flex; gap: 10px; }
.mb-toggle {
  padding: 9px 18px;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.35);
  background: rgba(255,255,255,0.1);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s ease, transform 0.15s ease;
}
.mb-toggle:hover { transform: translateY(-1px); }
.mb-toggle.is-on {
  background: var(--amber);
  border-color: var(--amber);
  box-shadow: 0 6px 16px rgba(201,138,59,0.45);
}

@media (prefers-reduced-motion: reduce) {
  .mb-toggle { transition: none; }
}
JavaScript
// MOON BREW:雪/雨を切り替える天候エフェクト
(() => {
  const canvas = document.getElementById('mbSnow');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;
  const btnSnow = document.getElementById('mbBtnSnow');
  const btnRain = document.getElementById('mbBtnRain');

  let w = 0, h = 0, raf = 0, running = true, mode = 'snow';
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  let drops = [];

  function resize() {
    const r = canvas.getBoundingClientRect();
    w = r.width; h = r.height;
    canvas.width = Math.max(1, w * dpr);
    canvas.height = Math.max(1, h * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // モードに応じた粒を生成
  function makeDrops() {
    const count = mode === 'snow'
      ? Math.max(60, Math.min(140, Math.floor((w * h) / 5500)))
      : Math.max(80, Math.min(180, Math.floor((w * h) / 4200)));
    drops = Array.from({ length: count }, () => spawn(true));
  }
  // 1粒の初期値(topで再投入する場合 fromTop=false)
  function spawn(anywhere) {
    if (mode === 'snow') {
      return {
        x: Math.random() * w,
        y: anywhere ? Math.random() * h : -5,
        r: Math.random() * 2.2 + 1,
        sway: Math.random() * Math.PI * 2,
        vy: Math.random() * 0.6 + 0.4
      };
    }
    return {
      x: Math.random() * w,
      y: anywhere ? Math.random() * h : -20,
      len: Math.random() * 12 + 8,
      vy: Math.random() * 4 + 7
    };
  }

  resize();
  makeDrops();

  function step() {
    ctx.clearRect(0, 0, w, h);

    if (mode === 'snow') {
      // ふわふわ舞う雪
      ctx.fillStyle = 'rgba(255,255,255,0.85)';
      for (const d of drops) {
        d.sway += 0.02;
        d.y += d.vy;
        d.x += Math.sin(d.sway) * 0.6; // 横揺れ
        if (d.y > h + 5) Object.assign(d, spawn(false));
        ctx.beginPath();
        ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
        ctx.fill();
      }
    } else {
      // 斜めに降る雨
      ctx.strokeStyle = 'rgba(200,220,235,0.6)';
      ctx.lineWidth = 1;
      for (const d of drops) {
        d.y += d.vy;
        d.x += d.vy * 0.25; // 斜め
        if (d.y > h + 10) Object.assign(d, spawn(false));
        ctx.beginPath();
        ctx.moveTo(d.x, d.y);
        ctx.lineTo(d.x - d.len * 0.25, d.y - d.len);
        ctx.stroke();
      }
    }
    raf = requestAnimationFrame(step);
  }

  function start() {
    if (running) return;
    running = true;
    raf = requestAnimationFrame(step);
  }
  function stop() {
    running = false;
    cancelAnimationFrame(raf);
  }

  // モード切替
  function setMode(next) {
    mode = next;
    makeDrops();
    if (btnSnow) btnSnow.classList.toggle('is-on', next === 'snow');
    if (btnRain) btnRain.classList.toggle('is-on', next === 'rain');
  }
  if (btnSnow) btnSnow.addEventListener('click', () => setMode('snow'));
  if (btnRain) btnRain.addEventListener('click', () => setMode('rain'));

  window.addEventListener('resize', () => { resize(); makeDrops(); });
  document.addEventListener('visibilitychange', () => {
    document.hidden ? stop() : start();
  });

  running = false;
  start();
})();

コード

HTML
<!-- 降雪/雨 切替デモ -->
<div class="stage">
  <canvas id="weatherCanvas"></canvas>
  <div class="controls">
    <button type="button" data-mode="snow" class="is-active">❄ 雪</button>
    <button type="button" data-mode="rain">☔ 雨</button>
  </div>
</div>
CSS
/* 降雪/雨ステージ */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: linear-gradient(180deg, #1a2238 0%, #2b3552 60%, #3a4566 100%);
}
#weatherCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
/* モード切替ボタン */
.controls {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
}
.controls button {
  appearance: none;
  border: 1px solid rgba(255, 255, 255, .25);
  background: rgba(255, 255, 255, .08);
  color: #e8edf7;
  font-size: 13px;
  padding: 6px 16px;
  border-radius: 999px;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background .2s, transform .1s;
}
.controls button:hover { background: rgba(255, 255, 255, .18); }
.controls button:active { transform: scale(.95); }
.controls button.is-active {
  background: rgba(255, 255, 255, .9);
  color: #25304d;
  font-weight: 600;
}
JavaScript
// 降雪/雨デモ(ボタンで切替)
(() => {
  const canvas = document.getElementById('weatherCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  const buttons = Array.from(document.querySelectorAll('.controls button'));

  let w = 0, h = 0, mode = 'snow';
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  let drops = [];

  function resize() {
    const r = canvas.getBoundingClientRect();
    w = r.width; h = r.height;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // モードに応じた粒子を生成
  function makeDrops() {
    const count = mode === 'snow' ? 120 : 200;
    drops = Array.from({ length: count }, () => create());
  }
  function create(fromTop = false) {
    if (mode === 'snow') {
      return {
        x: Math.random() * w,
        y: fromTop ? -10 : Math.random() * h,
        r: Math.random() * 2.5 + 1,
        sp: Math.random() * 0.8 + 0.4,
        drift: Math.random() * 1.2 - 0.6,
        phase: Math.random() * Math.PI * 2
      };
    }
    return {
      x: Math.random() * w,
      y: fromTop ? -20 : Math.random() * h,
      len: Math.random() * 14 + 8,
      sp: Math.random() * 4 + 6
    };
  }

  resize();
  makeDrops();
  window.addEventListener('resize', () => { resize(); makeDrops(); });

  function step() {
    ctx.clearRect(0, 0, w, h);

    if (mode === 'snow') {
      ctx.fillStyle = 'rgba(255,255,255,0.9)';
      for (const d of drops) {
        d.y += d.sp;
        d.phase += 0.02;
        d.x += Math.sin(d.phase) * 0.5 + d.drift * 0.3; // ふわふわ揺れる
        if (d.y > h + 5) Object.assign(d, create(true));
        ctx.beginPath();
        ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
        ctx.fill();
      }
    } else {
      ctx.strokeStyle = 'rgba(174,194,224,0.6)';
      ctx.lineWidth = 1.2;
      for (const d of drops) {
        d.y += d.sp;
        d.x += 1.2; // 斜めに降る雨
        if (d.y > h + 10) Object.assign(d, create(true));
        ctx.beginPath();
        ctx.moveTo(d.x, d.y);
        ctx.lineTo(d.x - 2, d.y - d.len);
        ctx.stroke();
      }
    }
    requestAnimationFrame(step);
  }

  // ボタンでモード切替
  buttons.forEach((b) => {
    b.addEventListener('click', () => {
      mode = b.dataset.mode;
      buttons.forEach((x) => x.classList.toggle('is-active', x === b));
      makeDrops();
    });
  });

  requestAnimationFrame(step);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「降雪・雨エフェクト切替」の効果を追加してください。

# 追加してほしい効果
降雪・雨エフェクト切替(Canvas エフェクト)
ゆらゆら舞う雪と斜めに降る雨をボタンで切り替えられる天候エフェクト。季節のランディングページやヒーロー背景に重ねて使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 降雪/雨 切替デモ -->
<div class="stage">
  <canvas id="weatherCanvas"></canvas>
  <div class="controls">
    <button type="button" data-mode="snow" class="is-active">❄ 雪</button>
    <button type="button" data-mode="rain">☔ 雨</button>
  </div>
</div>

【CSS】
/* 降雪/雨ステージ */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: linear-gradient(180deg, #1a2238 0%, #2b3552 60%, #3a4566 100%);
}
#weatherCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
/* モード切替ボタン */
.controls {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
}
.controls button {
  appearance: none;
  border: 1px solid rgba(255, 255, 255, .25);
  background: rgba(255, 255, 255, .08);
  color: #e8edf7;
  font-size: 13px;
  padding: 6px 16px;
  border-radius: 999px;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background .2s, transform .1s;
}
.controls button:hover { background: rgba(255, 255, 255, .18); }
.controls button:active { transform: scale(.95); }
.controls button.is-active {
  background: rgba(255, 255, 255, .9);
  color: #25304d;
  font-weight: 600;
}

【JavaScript】
// 降雪/雨デモ(ボタンで切替)
(() => {
  const canvas = document.getElementById('weatherCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  const buttons = Array.from(document.querySelectorAll('.controls button'));

  let w = 0, h = 0, mode = 'snow';
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  let drops = [];

  function resize() {
    const r = canvas.getBoundingClientRect();
    w = r.width; h = r.height;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // モードに応じた粒子を生成
  function makeDrops() {
    const count = mode === 'snow' ? 120 : 200;
    drops = Array.from({ length: count }, () => create());
  }
  function create(fromTop = false) {
    if (mode === 'snow') {
      return {
        x: Math.random() * w,
        y: fromTop ? -10 : Math.random() * h,
        r: Math.random() * 2.5 + 1,
        sp: Math.random() * 0.8 + 0.4,
        drift: Math.random() * 1.2 - 0.6,
        phase: Math.random() * Math.PI * 2
      };
    }
    return {
      x: Math.random() * w,
      y: fromTop ? -20 : Math.random() * h,
      len: Math.random() * 14 + 8,
      sp: Math.random() * 4 + 6
    };
  }

  resize();
  makeDrops();
  window.addEventListener('resize', () => { resize(); makeDrops(); });

  function step() {
    ctx.clearRect(0, 0, w, h);

    if (mode === 'snow') {
      ctx.fillStyle = 'rgba(255,255,255,0.9)';
      for (const d of drops) {
        d.y += d.sp;
        d.phase += 0.02;
        d.x += Math.sin(d.phase) * 0.5 + d.drift * 0.3; // ふわふわ揺れる
        if (d.y > h + 5) Object.assign(d, create(true));
        ctx.beginPath();
        ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
        ctx.fill();
      }
    } else {
      ctx.strokeStyle = 'rgba(174,194,224,0.6)';
      ctx.lineWidth = 1.2;
      for (const d of drops) {
        d.y += d.sp;
        d.x += 1.2; // 斜めに降る雨
        if (d.y > h + 10) Object.assign(d, create(true));
        ctx.beginPath();
        ctx.moveTo(d.x, d.y);
        ctx.lineTo(d.x - 2, d.y - d.len);
        ctx.stroke();
      }
    }
    requestAnimationFrame(step);
  }

  // ボタンでモード切替
  buttons.forEach((b) => {
    b.addEventListener('click', () => {
      mode = b.dataset.mode;
      buttons.forEach((x) => x.classList.toggle('is-active', x === b));
      makeDrops();
    });
  });

  requestAnimationFrame(step);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。