花火アニメーション
打ち上げ弾が頂点で爆発し放射状に火花が飛ぶ物理風の花火。自動打ち上げに加えクリックでも発射できます。お祝い・達成演出に。
ライブデモ
使用例(お題: アイドルグループ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。