花火アニメーション

打ち上げ弾が頂点で爆発し放射状に火花が飛ぶ物理風の花火。自動打ち上げに加えクリックでも発射できます。お祝い・達成演出に。

#canvas#particles#physics#interactive

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura:ライブ大団円の花火フィナーレ画面 -->
<section class="sk-final">
  <!-- 主役:自動+クリックで打ち上がる花火 -->
  <canvas class="sk-final__fw" id="skFireworks"></canvas>

  <!-- 前景UI:感謝メッセージ -->
  <div class="sk-final__inner">
    <span class="sk-final__tag">FINAL ENCORE</span>
    <h1 class="sk-final__title">5周年、<br>ありがとう。</h1>
    <p class="sk-final__lead">Sakura 5th Anniversary LIVE「満開」<br>本日、無事に幕を閉じました。画面をクリックでお祝いの花火が上がります。</p>

    <div class="sk-final__stat">
      <div><b>5</b><span>周年</span></div>
      <div><b>32</b><span>公演</span></div>
      <div><b>18</b><span>楽曲</span></div>
    </div>
    <a class="sk-final__btn" href="#">ライブ写真を見る</a>
  </div>
</section>
CSS
/* Sakura:花火フィナーレ(夜空+桜色の花火) */
:root {
  --pink: #ffd1e0;
  --pink-deep: #ff7fa8;
}

* { box-sizing: border-box; }

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

.sk-final {
  position: relative;
  height: 400px;
  overflow: hidden;
  background:
    radial-gradient(700px 380px at 50% 110%, #3a1f3a, transparent),
    linear-gradient(180deg, #1a0f1f, #2a1428 70%, #3a1d33);
}

/* 主役:花火キャンバス(全面) */
.sk-final__fw {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}

.sk-final__inner {
  position: relative;
  z-index: 2;
  padding: 34px 30px;
  max-width: 440px;
  pointer-events: none;
}
.sk-final__inner a { pointer-events: auto; }

.sk-final__tag {
  display: inline-block;
  font-size: 10px;
  letter-spacing: 0.32em;
  font-weight: 800;
  color: var(--pink-deep);
}
.sk-final__title {
  margin: 12px 0 14px;
  font-size: 36px;
  line-height: 1.3;
  font-weight: 900;
  color: #fff;
  text-shadow: 0 2px 18px rgba(255,127,168,0.5);
}
.sk-final__lead {
  margin: 0 0 22px;
  font-size: 12.5px;
  line-height: 1.8;
  color: rgba(255,224,236,0.85);
}

.sk-final__stat { display: flex; gap: 22px; margin-bottom: 22px; }
.sk-final__stat b {
  display: block;
  font-size: 26px;
  font-weight: 900;
  color: var(--pink);
}
.sk-final__stat span { font-size: 11px; color: rgba(255,224,236,0.7); }

.sk-final__btn {
  display: inline-block;
  padding: 11px 24px;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--pink-deep), #ff9cbb);
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  text-decoration: none;
  box-shadow: 0 10px 26px rgba(255,127,168,0.5);
  transition: transform 0.2s ease;
}
.sk-final__btn:hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .sk-final__btn { transition: none; }
}
JavaScript
// Sakura:花火フィナーレ。自動打ち上げ+クリックで発射
(() => {
  const canvas = document.getElementById('skFireworks');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  let w = 0, h = 0, raf = 0, running = true, timer = 0;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const rockets = []; // 打ち上げ弾
  const sparks = [];  // 爆発火花
  // 桜テーマの色相(ピンク〜紫〜白寄り)
  const hues = [330, 340, 350, 310, 290, 0];

  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);
  }
  resize();

  // 打ち上げ弾を生成(目標の高さまで上昇)
  function launch(tx, ty) {
    const targetY = ty != null ? ty : h * (0.2 + Math.random() * 0.3);
    rockets.push({
      x: tx != null ? tx : w * (0.2 + Math.random() * 0.6),
      y: h,
      targetY,
      vy: -(5 + Math.random() * 2),
      hue: hues[Math.floor(Math.random() * hues.length)]
    });
  }

  // 爆発:放射状に火花を飛ばす
  function explode(x, y, hue) {
    const count = 46 + Math.floor(Math.random() * 24);
    for (let i = 0; i < count; i++) {
      const ang = (Math.PI * 2 * i) / count;
      const sp = Math.random() * 3 + 1.2;
      sparks.push({
        x, y,
        vx: Math.cos(ang) * sp,
        vy: Math.sin(ang) * sp,
        life: 1,
        hue: hue + (Math.random() * 20 - 10)
      });
    }
  }

  function step() {
    // 残像(夜空に余韻)
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle = 'rgba(20,10,22,0.22)';
    ctx.fillRect(0, 0, w, h);
    ctx.globalCompositeOperation = 'lighter';

    // 打ち上げ弾
    for (let i = rockets.length - 1; i >= 0; i--) {
      const r = rockets[i];
      r.y += r.vy;
      ctx.beginPath();
      ctx.arc(r.x, r.y, 2, 0, Math.PI * 2);
      ctx.fillStyle = `hsl(${r.hue},90%,75%)`;
      ctx.fill();
      if (r.y <= r.targetY) {
        explode(r.x, r.y, r.hue);
        rockets.splice(i, 1);
      }
    }

    // 火花
    for (let i = sparks.length - 1; i >= 0; i--) {
      const s = sparks[i];
      s.x += s.vx;
      s.y += s.vy;
      s.vy += 0.035; // 重力
      s.vx *= 0.985;
      s.life -= 0.016;
      if (s.life <= 0) { sparks.splice(i, 1); continue; }
      ctx.beginPath();
      ctx.arc(s.x, s.y, 2 * s.life + 0.4, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${s.hue},90%,68%,${s.life})`;
      ctx.fill();
    }

    ctx.globalCompositeOperation = 'source-over';

    // 自動打ち上げ
    if (--timer <= 0) {
      launch();
      timer = 35 + Math.floor(Math.random() * 35);
    }
    raf = requestAnimationFrame(step);
  }

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

  // クリックでその場へ打ち上げ
  canvas.addEventListener('pointerdown', (e) => {
    const r = canvas.getBoundingClientRect();
    launch(e.clientX - r.left, e.clientY - r.top);
  });

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

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

コード

HTML
<!-- 花火 -->
<div class="stage">
  <canvas id="fireworksCanvas"></canvas>
  <div class="tip">クリックで打ち上げ</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, #05060f 0%, #0a0f24 55%, #14213f 100%);
  cursor: pointer;
}
#fireworksCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
.tip {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, .4);
  font-size: 12px;
  letter-spacing: .2em;
  pointer-events: none;
}
JavaScript
// 花火デモ(自動打ち上げ+クリックで追加)
(() => {
  const canvas = document.getElementById('fireworksCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let w = 0, h = 0;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const rockets = [];   // 上昇中の弾
  const sparks = [];    // 爆発後の火花

  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);
  }
  resize();
  window.addEventListener('resize', resize);

  // 打ち上げ弾を生成(目標の高さまで上がって爆発)
  function launch(tx, ty) {
    const startX = tx ?? (w * (0.2 + Math.random() * 0.6));
    const targetY = ty ?? (h * (0.15 + Math.random() * 0.35));
    rockets.push({
      x: startX, y: h,
      vy: -(Math.sqrt(2 * 0.06 * (h - targetY))), // 目標高度に届く初速
      hue: Math.floor(Math.random() * 360)
    });
  }

  // 爆発させて火花を放射状に飛ばす
  function explode(x, y, hue) {
    const count = 60 + Math.floor(Math.random() * 40);
    for (let i = 0; i < count; i++) {
      const ang = (Math.PI * 2 * i) / count;
      const sp = Math.random() * 3 + 1.5;
      sparks.push({
        x, y,
        vx: Math.cos(ang) * sp,
        vy: Math.sin(ang) * sp,
        life: 1,
        hue: hue + (Math.random() * 30 - 15)
      });
    }
  }

  function step() {
    // 残像を残すために半透明で塗りつぶし
    ctx.fillStyle = 'rgba(5,6,15,0.22)';
    ctx.fillRect(0, 0, w, h);
    ctx.globalCompositeOperation = 'lighter';

    // 上昇弾の処理
    for (let i = rockets.length - 1; i >= 0; i--) {
      const r = rockets[i];
      r.y += r.vy;
      r.vy += 0.06; // 重力で減速
      ctx.beginPath();
      ctx.arc(r.x, r.y, 2.4, 0, Math.PI * 2);
      ctx.fillStyle = `hsl(${r.hue},90%,70%)`;
      ctx.fill();
      if (r.vy >= 0) { // 頂点で爆発
        explode(r.x, r.y, r.hue);
        rockets.splice(i, 1);
      }
    }

    // 火花の処理
    for (let i = sparks.length - 1; i >= 0; i--) {
      const s = sparks[i];
      s.x += s.vx;
      s.y += s.vy;
      s.vy += 0.03;
      s.vx *= 0.99;
      s.life -= 0.012;
      if (s.life <= 0) { sparks.splice(i, 1); continue; }
      ctx.beginPath();
      ctx.arc(s.x, s.y, 2 * s.life + 0.3, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${s.hue},90%,62%,${s.life})`;
      ctx.fill();
    }
    ctx.globalCompositeOperation = 'source-over';
    requestAnimationFrame(step);
  }

  // 一定間隔で自動的に打ち上げ
  if (!reduced) setInterval(() => { if (rockets.length < 4) launch(); }, 900);

  canvas.addEventListener('pointerdown', (e) => {
    const r = canvas.getBoundingClientRect();
    launch(e.clientX - r.left, e.clientY - r.top);
  });

  launch();
  requestAnimationFrame(step);
})();

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

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

# 追加してほしい効果
花火アニメーション(Canvas エフェクト)
打ち上げ弾が頂点で爆発し放射状に火花が飛ぶ物理風の花火。自動打ち上げに加えクリックでも発射できます。お祝い・達成演出に。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 花火 -->
<div class="stage">
  <canvas id="fireworksCanvas"></canvas>
  <div class="tip">クリックで打ち上げ</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, #05060f 0%, #0a0f24 55%, #14213f 100%);
  cursor: pointer;
}
#fireworksCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
.tip {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, .4);
  font-size: 12px;
  letter-spacing: .2em;
  pointer-events: none;
}

【JavaScript】
// 花火デモ(自動打ち上げ+クリックで追加)
(() => {
  const canvas = document.getElementById('fireworksCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let w = 0, h = 0;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const rockets = [];   // 上昇中の弾
  const sparks = [];    // 爆発後の火花

  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);
  }
  resize();
  window.addEventListener('resize', resize);

  // 打ち上げ弾を生成(目標の高さまで上がって爆発)
  function launch(tx, ty) {
    const startX = tx ?? (w * (0.2 + Math.random() * 0.6));
    const targetY = ty ?? (h * (0.15 + Math.random() * 0.35));
    rockets.push({
      x: startX, y: h,
      vy: -(Math.sqrt(2 * 0.06 * (h - targetY))), // 目標高度に届く初速
      hue: Math.floor(Math.random() * 360)
    });
  }

  // 爆発させて火花を放射状に飛ばす
  function explode(x, y, hue) {
    const count = 60 + Math.floor(Math.random() * 40);
    for (let i = 0; i < count; i++) {
      const ang = (Math.PI * 2 * i) / count;
      const sp = Math.random() * 3 + 1.5;
      sparks.push({
        x, y,
        vx: Math.cos(ang) * sp,
        vy: Math.sin(ang) * sp,
        life: 1,
        hue: hue + (Math.random() * 30 - 15)
      });
    }
  }

  function step() {
    // 残像を残すために半透明で塗りつぶし
    ctx.fillStyle = 'rgba(5,6,15,0.22)';
    ctx.fillRect(0, 0, w, h);
    ctx.globalCompositeOperation = 'lighter';

    // 上昇弾の処理
    for (let i = rockets.length - 1; i >= 0; i--) {
      const r = rockets[i];
      r.y += r.vy;
      r.vy += 0.06; // 重力で減速
      ctx.beginPath();
      ctx.arc(r.x, r.y, 2.4, 0, Math.PI * 2);
      ctx.fillStyle = `hsl(${r.hue},90%,70%)`;
      ctx.fill();
      if (r.vy >= 0) { // 頂点で爆発
        explode(r.x, r.y, r.hue);
        rockets.splice(i, 1);
      }
    }

    // 火花の処理
    for (let i = sparks.length - 1; i >= 0; i--) {
      const s = sparks[i];
      s.x += s.vx;
      s.y += s.vy;
      s.vy += 0.03;
      s.vx *= 0.99;
      s.life -= 0.012;
      if (s.life <= 0) { sparks.splice(i, 1); continue; }
      ctx.beginPath();
      ctx.arc(s.x, s.y, 2 * s.life + 0.3, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${s.hue},90%,62%,${s.life})`;
      ctx.fill();
    }
    ctx.globalCompositeOperation = 'source-over';
    requestAnimationFrame(step);
  }

  // 一定間隔で自動的に打ち上げ
  if (!reduced) setInterval(() => { if (rockets.length < 4) launch(); }, 900);

  canvas.addEventListener('pointerdown', (e) => {
    const r = canvas.getBoundingClientRect();
    launch(e.clientX - r.left, e.clientY - r.top);
  });

  launch();
  requestAnimationFrame(step);
})();

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

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