Skip to content

Instantly share code, notes, and snippets.

@kujirahand
Created August 18, 2025 06:58
Show Gist options
  • Save kujirahand/e312341b1ed9cd43a1544ed21862f328 to your computer and use it in GitHub Desktop.
Save kujirahand/e312341b1ed9cd43a1544ed21862f328 to your computer and use it in GitHub Desktop.
花火の打ち上げプログラム
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Full Screen Fireworks</title>
<style>
html, body { height: 100%; margin: 0; }
body { background: radial-gradient(1200px 800px at 50% 80%, #06102a 0%, #020712 60%, #000 100%);
overflow: hidden; }
canvas { display: block; width: 100vw; height: 100vh; }
.hint { position: fixed; left: 12px; bottom: 10px; color: #cbd5e1; font-family: system-ui, -apple-system, Segoe UI, Roboto, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; font-size: 12px; opacity: .7; user-select: none; }
</style>
</head>
<body>
<canvas id="fw"></canvas>
<div class="hint">Full Screen Fireworks — 自動で連続&同時打ち上げ</div>
<script>
(() => {
'use strict';
// ====== Config ======
const CFG = {
dprMax: 2,
gravity: 0.15, // 重力(大きいほど垂れる)
airFriction: 0.995, // 空気抵抗
trailLen: 6, // パーティクルの軌跡の長さ
fadeAlpha: 0.18, // 尾を残すためのフェード
rocketMinVy: -12.5, // 打ち上げ初速(最小)
rocketMaxVy: -17.5, // 打ち上げ初速(最大)
rocketSpread: 0.9, // ロケットの左右ブレ
explodeAltitudeMin: 0.25, // 画面高に対する爆発の最低比率
explodeAltitudeMax: 0.55, // 爆発の最高比率
particlesPerBurstMin: 160,
particlesPerBurstMax: 320,
megaEvery: 6, // 何回に1回は巨大花火
megaMultiplier: 2.2, // 巨大花火の粒子倍率
launchEveryMs: 600, // 定常打ち上げの間隔
stormEveryMs: 3200, // 同時多発(連発)間隔
stormCountMin: 3,
stormCountMax: 6,
maxLiveParticles: 16000, // 安全上限(端末保護)
};
// ====== Canvas & sizing ======
const canvas = document.getElementById('fw');
const ctx = canvas.getContext('2d');
let W = 0, H = 0, DPR = Math.min(window.devicePixelRatio || 1, CFG.dprMax);
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, CFG.dprMax);
W = Math.max(1, Math.floor(innerWidth * DPR));
H = Math.max(1, Math.floor(innerHeight * DPR));
canvas.width = W; canvas.height = H;
}
addEventListener('resize', resize, { passive: true });
resize();
// ====== Utils ======
const rand = (a, b) => a + Math.random() * (b - a);
const randi = (a, b) => (Math.random() * (b - a + 1) + a) | 0;
const clamp = (x, a, b) => x < a ? a : (x > b ? b : x);
const TAU = Math.PI * 2;
function hsv2rgb(h, s, v) { // 0..360, 0..1, 0..1
let c = v * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = v - c;
let r=0,g=0,b=0;
if (h < 60) { r=c; g=x; b=0; }
else if (h < 120) { r=x; g=c; b=0; }
else if (h < 180) { r=0; g=c; b=x; }
else if (h < 240) { r=0; g=x; b=c; }
else if (h < 300) { r=x; g=0; b=c; }
else { r=c; g=0; b=x; }
return [(r+m)*255, (g+m)*255, (b+m)*255];
}
function rgba(r,g,b,a){ return `rgba(${r|0},${g|0},${b|0},${a})`; }
// ====== Objects ======
class Rocket {
constructor(x, vy, hueBase) {
this.x = x;
this.y = H + rand(20, 120);
this.vx = rand(-CFG.rocketSpread, CFG.rocketSpread);
this.vy = vy;
this.hue = hueBase ?? randi(0, 360);
const minY = H * CFG.explodeAltitudeMin;
const maxY = H * CFG.explodeAltitudeMax;
this.targetY = rand(minY, maxY);
this.dead = false;
}
update(dt) {
this.x += this.vx * dt;
this.y += this.vy * dt;
this.vy += CFG.gravity * 0.3 * dt; // 少しだけ重力
if (this.vy > 0 || this.y <= this.targetY) {
this.dead = true;
explode(this.x, this.y, this.hue);
}
}
draw() {
// 上昇中の光跡
ctx.beginPath();
ctx.arc(this.x, this.y, 2.5 * DPR, 0, TAU);
const [r,g,b] = hsv2rgb(this.hue, 0.8, 1.0);
ctx.fillStyle = rgba(r,g,b, 0.95);
ctx.fill();
}
}
class Particle {
constructor(x, y, spd, ang, hue, sparkle=false, life=1.0) {
this.x = x; this.y = y;
this.vx = Math.cos(ang) * spd;
this.vy = Math.sin(ang) * spd;
this.hue = hue;
this.life = life; // 1..0
this.sparkle = sparkle;
this.trail = [];
}
update(dt) {
// 保存する軌跡点
this.trail.push([this.x, this.y]);
if (this.trail.length > CFG.trailLen) this.trail.shift();
this.x += this.vx * dt;
this.y += this.vy * dt;
this.vx *= CFG.airFriction;
this.vy = this.vy * CFG.airFriction + CFG.gravity * dt;
this.life -= 0.009 * dt; // 寿命減衰
if (this.life <= 0) this.life = 0;
}
draw() {
const [r,g,b] = hsv2rgb(this.hue, 0.9, 1.0);
// 軌跡
ctx.beginPath();
for (let i=0;i<this.trail.length;i++) {
const p = this.trail[i];
if (i===0) ctx.moveTo(p[0], p[1]); else ctx.lineTo(p[0], p[1]);
}
ctx.strokeStyle = rgba(r,g,b, 0.35 * this.life);
ctx.lineWidth = 1.4 * DPR;
ctx.stroke();
// 本体
ctx.beginPath();
ctx.arc(this.x, this.y, 1.6*DPR, 0, TAU);
ctx.fillStyle = rgba(r,g,b, 0.8 * this.life);
ctx.fill();
// キラキラ
if (this.sparkle && Math.random() < 0.2) {
ctx.beginPath();
ctx.arc(this.x, this.y, 0.7*DPR, 0, TAU);
ctx.fillStyle = rgba(255,255,255, 0.4 * this.life);
ctx.fill();
}
}
}
// ====== Simulation state ======
const rockets = [];
const particles = [];
let launchCount = 0;
// ====== Explosion patterns ======
function explode(x, y, hueSeed) {
let count = randi(CFG.particlesPerBurstMin, CFG.particlesPerBurstMax);
const isMega = (++launchCount % CFG.megaEvery === 0);
if (isMega) count = (count * CFG.megaMultiplier) | 0;
const mode = randi(0, 5); // 6種類
const baseHue = (hueSeed + randi(-20, 20) + 360) % 360;
const V = isMega ? 1.0 : 0.95;
const angleStep = TAU / count;
for (let i = 0; i < count; i++) {
const a = angleStep * i + Math.random()*0.03;
let spd, ang = a, hue = (baseHue + randi(-10, 10) + 360) % 360;
let sparkle = false, life = 1.0;
switch (mode) {
case 0: // 球(菊)
spd = rand(3.6, 6.2) * DPR * (isMega ? 1.3 : 1);
break;
case 1: // リング
spd = 5.4 * DPR * (isMega ? 1.2 : 1);
break;
case 2: // ウィロー(柳)
spd = rand(2.8, 4.2) * DPR; life = 1.6; hue = baseHue; sparkle = true;
break;
case 3: // スパイラル
spd = rand(3.4, 5.4) * DPR; ang += i * 0.02;
break;
case 4: // パーム(中心から強め)
spd = rand(4.8, 7.0) * DPR; if (Math.random()<0.15) spd *= 1.4;
break;
case 5: // ハート
// パラメトリック心形: r = (1 - sinθ)
const r = (1 - Math.sin(a)) * 3.4 * DPR * (isMega ? 1.35 : 1);
spd = r; ang = a; sparkle = true; life = 1.2; hue = baseHue;
break;
}
// 安全上限(パフォーマンス保護)
if (particles.length > CFG.maxLiveParticles) break;
particles.push(new Particle(x, y, spd, ang, hue, sparkle, life));
}
}
// ====== Launch scheduling ======
function scheduleLaunch(amount = 1) {
for (let i = 0; i < amount; i++) {
const x = rand(W*0.08, W*0.92);
const vy = rand(CFG.rocketMaxVy, CFG.rocketMinVy);
// 同時発火感を出すため、微小遅延
const delay = i === 0 ? 0 : rand(20, 120);
const hue = randi(0, 360);
setTimeout(() => rockets.push(new Rocket(x, vy, hue)), delay);
}
}
// 定常打ち上げ
setInterval(() => {
// 1〜2発を連続
scheduleLaunch(randi(1, 2));
}, CFG.launchEveryMs);
// 同時多発の嵐
setInterval(() => {
scheduleLaunch(randi(CFG.stormCountMin, CFG.stormCountMax));
}, CFG.stormEveryMs);
// ====== Main loop ======
let last = performance.now();
function loop(now) {
requestAnimationFrame(loop);
const dtMs = now - last; last = now;
// 可変フレームレート対応(60fps基準で正規化)
const dt = clamp(dtMs / (1000/60), 0.5, 2.5);
// フェード(残像)
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = `rgba(0,0,0,${CFG.fadeAlpha})`;
ctx.fillRect(0, 0, W, H);
// オブジェクト更新
for (let i = rockets.length - 1; i >= 0; i--) {
const r = rockets[i];
r.update(dt);
if (r.dead) rockets.splice(i, 1);
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.update(dt);
if (p.life <= 0) particles.splice(i, 1);
}
// 描画
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < rockets.length; i++) rockets[i].draw();
for (let i = 0; i < particles.length; i++) particles[i].draw();
}
// 初期の景気づけ(いきなり大花火)
scheduleLaunch(4);
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
@kujirahand
Copy link
Author

kujirahand commented Aug 18, 2025

Claude sonnet 4で作成したプログラム

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>巨大花火大会 - 画面一杯の華やかな花火</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background: linear-gradient(to bottom, #000428, #004e92);
            overflow: hidden;
            font-family: 'Arial', sans-serif;
        }
        
        #fireworksCanvas {
            display: block;
            background: linear-gradient(to bottom, #0a0a2e, #16213e, #0f3460);
            cursor: crosshair;
        }
        
        .controls {
            position: absolute;
            top: 10px;
            left: 10px;
            color: white;
            font-size: 14px;
            z-index: 100;
        }
        
        button {
            background: rgba(255, 255, 255, 0.2);
            border: 1px solid rgba(255, 255, 255, 0.3);
            color: white;
            padding: 8px 16px;
            margin: 5px;
            border-radius: 5px;
            cursor: pointer;
            transition: background 0.3s;
            backdrop-filter: blur(10px);
        }
        
        button:hover {
            background: rgba(255, 255, 255, 0.3);
            transform: translateY(-2px);
        }
        
        .info {
            position: absolute;
            bottom: 10px;
            right: 10px;
            color: rgba(255, 255, 255, 0.7);
            font-size: 12px;
        }
    </style>
</head>
<body>
    <canvas id="fireworksCanvas"></canvas>
    <div class="controls">
        <button onclick="toggleFireworks()" id="toggleBtn">花火 停止</button>
        <button onclick="launchBigFirework()">特大花火</button>
        <button onclick="launchMultipleFireworks()">連続花火</button>
    </div>
    <div class="info">
        クリックで花火を打ち上げ
    </div>

    <script>
        const canvas = document.getElementById('fireworksCanvas');
        const ctx = canvas.getContext('2d');
        
        // キャンバスサイズの設定
        function resizeCanvas() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        }
        
        resizeCanvas();
        window.addEventListener('resize', resizeCanvas);
        
        // 花火のパーティクルクラス
        class Particle {
            constructor(x, y, color, velocity, life, size = 2, type = 'normal') {
                this.x = x;
                this.y = y;
                this.vx = velocity.x;
                this.vy = velocity.y;
                this.color = color;
                this.life = life;
                this.maxLife = life;
                this.size = size;
                this.originalSize = size;
                this.gravity = type === 'sparkle' ? 0.02 : 0.05;
                this.friction = type === 'sparkle' ? 0.99 : 0.98;
                this.alpha = 1;
                this.type = type;
                this.twinkle = Math.random() * Math.PI * 2;
            }
            
            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vy += this.gravity;
                this.vx *= this.friction;
                this.vy *= this.friction;
                
                this.life--;
                this.alpha = Math.max(0, this.life / this.maxLife);
                
                if (this.type === 'sparkle') {
                    this.twinkle += 0.3;
                    this.size = this.originalSize * (0.5 + 0.5 * Math.sin(this.twinkle));
                } else {
                    this.size *= 0.995;
                }
            }
            
            draw() {
                ctx.save();
                ctx.globalAlpha = this.alpha;
                
                if (this.type === 'sparkle') {
                    // キラキラ効果
                    ctx.shadowBlur = 20;
                    ctx.shadowColor = this.color;
                    ctx.fillStyle = this.color;
                    
                    // 星形を描画
                    this.drawStar(this.x, this.y, this.size);
                } else {
                    // 通常のパーティクル
                    ctx.shadowBlur = 15;
                    ctx.shadowColor = this.color;
                    ctx.fillStyle = this.color;
                    ctx.beginPath();
                    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    ctx.fill();
                }
                
                ctx.restore();
            }
            
            drawStar(x, y, size) {
                const spikes = 5;
                const outerRadius = size;
                const innerRadius = size * 0.4;
                
                ctx.beginPath();
                for (let i = 0; i < spikes * 2; i++) {
                    const angle = (i * Math.PI) / spikes;
                    const radius = i % 2 === 0 ? outerRadius : innerRadius;
                    const dx = x + Math.cos(angle) * radius;
                    const dy = y + Math.sin(angle) * radius;
                    
                    if (i === 0) {
                        ctx.moveTo(dx, dy);
                    } else {
                        ctx.lineTo(dx, dy);
                    }
                }
                ctx.closePath();
                ctx.fill();
            }
            
            isDead() {
                return this.life <= 0 || this.size <= 0.1;
            }
        }
        
        // 花火クラス
        class Firework {
            constructor(x, y, targetY, colors, size = 'normal') {
                this.x = x;
                this.y = y;
                this.targetY = targetY;
                this.colors = colors;
                this.speed = 6 + Math.random() * 4;
                this.particles = [];
                this.exploded = false;
                this.trail = [];
                this.trailLength = 25;
                this.size = size;
                this.hue = Math.random() * 360;
            }
            
            update() {
                if (!this.exploded) {
                    // 上昇中
                    this.y -= this.speed;
                    
                    // トレイル効果
                    this.trail.push({ 
                        x: this.x + (Math.random() - 0.5) * 2, 
                        y: this.y + Math.random() * 3,
                        life: this.trailLength
                    });
                    
                    // トレイルの寿命を減らす
                    this.trail = this.trail.filter(point => {
                        point.life--;
                        return point.life > 0;
                    });
                    
                    // 目標高度に達したら爆発
                    if (this.y <= this.targetY) {
                        this.explode();
                    }
                } else {
                    // 爆発後のパーティクル更新
                    this.particles = this.particles.filter(particle => {
                        particle.update();
                        return !particle.isDead();
                    });
                }
            }
            
            explode() {
                this.exploded = true;
                this.createFireworkLayers();
            }
            
            createFireworkLayers() {
                const sizeMultiplier = this.size === 'big' ? 2 : 1;
                
                // 芯(内層)- 明るい色
                this.createLayer(40 * sizeMultiplier, 4, 8, 100, 5, this.colors[0] || '#ffffff', 'core');
                
                // 中殻(中層)- メインカラー
                setTimeout(() => {
                    this.createLayer(60 * sizeMultiplier, 6, 14, 120, 4, this.colors[1] || this.colors[0], 'middle');
                }, 50);
                
                // 外殻(外層)- 補色
                setTimeout(() => {
                    this.createLayer(80 * sizeMultiplier, 10, 20, 140, 3, this.colors[2] || this.colors[0], 'outer');
                }, 100);
                
                // キラキラ効果
                setTimeout(() => {
                    for (let i = 0; i < 40 * sizeMultiplier; i++) {
                        const angle = Math.random() * Math.PI * 2;
                        const speed = Math.random() * 25 + 5;
                        
                        const velocity = {
                            x: Math.cos(angle) * speed,
                            y: Math.sin(angle) * speed
                        };
                        
                        const sparkle = new Particle(
                            this.x + (Math.random() - 0.5) * 20,
                            this.y + (Math.random() - 0.5) * 20,
                            '#ffffff',
                            velocity,
                            60 + Math.random() * 30,
                            2 + Math.random() * 3,
                            'sparkle'
                        );
                        
                        this.particles.push(sparkle);
                    }
                }, 150);
            }
            
            createLayer(count, minRadius, maxRadius, life, size, baseColor, layer) {
                for (let i = 0; i < count; i++) {
                    const angle = (Math.PI * 2 * i) / count + Math.random() * 0.5;
                    const speed = minRadius + Math.random() * (maxRadius - minRadius);
                    
                    // 角度に少しランダム性を加える
                    const randomAngle = angle + (Math.random() - 0.5) * 0.3;
                    
                    const velocity = {
                        x: Math.cos(randomAngle) * speed,
                        y: Math.sin(randomAngle) * speed
                    };
                    
                    // 色のバリエーションを追加
                    let color = baseColor;
                    if (Math.random() < 0.3) {
                        color = this.colors[Math.floor(Math.random() * this.colors.length)];
                    }
                    
                    const particle = new Particle(
                        this.x + (Math.random() - 0.5) * 10,
                        this.y + (Math.random() - 0.5) * 10,
                        color,
                        velocity,
                        life + Math.random() * 40,
                        size + Math.random() * 3
                    );
                    
                    this.particles.push(particle);
                }
            }
            
            draw() {
                if (!this.exploded) {
                    // 上昇中のトレイル描画
                    ctx.save();
                    this.trail.forEach((point, index) => {
                        const alpha = point.life / this.trailLength;
                        ctx.globalAlpha = alpha * 0.8;
                        
                        const gradient = ctx.createRadialGradient(point.x, point.y, 0, point.x, point.y, 5);
                        gradient.addColorStop(0, '#ffaa00');
                        gradient.addColorStop(1, 'transparent');
                        
                        ctx.fillStyle = gradient;
                        ctx.shadowBlur = 15;
                        ctx.shadowColor = '#ffaa00';
                        ctx.beginPath();
                        ctx.arc(point.x, point.y, 3 + Math.random(), 0, Math.PI * 2);
                        ctx.fill();
                    });
                    ctx.restore();
                } else {
                    // 爆発後のパーティクル描画
                    this.particles.forEach(particle => particle.draw());
                }
            }
            
            isDead() {
                return this.exploded && this.particles.length === 0;
            }
        }
        
        // 花火管理
        let fireworks = [];
        let isActive = true;
        let lastLaunchTime = 0;
        
        // カラーパレット(色とりどりの薬玉)
        const colorPalettes = [
            ['#ff1744', '#ff5722', '#ffeb3b', '#fff'],          // 赤系
            ['#3f51b5', '#2196f3', '#00bcd4', '#e1f5fe'],       // 青系
            ['#9c27b0', '#e91e63', '#ff9800', '#fff'],          // 紫系
            ['#4caf50', '#8bc34a', '#cddc39', '#ffeb3b'],       // 緑系
            ['#ff5722', '#ff9800', '#ffc107', '#fff176'],       // オレンジ系
            ['#e91e63', '#ad1457', '#f8bbd9', '#fff'],          // ピンク系
            ['#00bcd4', '#26c6da', '#4dd0e1', '#b2ebf2'],       // シアン系
            ['#ffeb3b', '#ffc107', '#ff9800', '#fff8e1']        // 黄色系
        ];
        
        function launchFirework(x = null, targetY = null, size = 'normal') {
            const launchX = x || (Math.random() * (canvas.width * 0.8) + canvas.width * 0.1);
            const launchTargetY = targetY || (Math.random() * (canvas.height * 0.5) + canvas.height * 0.1);
            const colors = colorPalettes[Math.floor(Math.random() * colorPalettes.length)];
            
            const firework = new Firework(launchX, canvas.height, launchTargetY, colors, size);
            fireworks.push(firework);
        }
        
        function launchBigFirework() {
            const x = canvas.width / 2 + (Math.random() - 0.5) * 200;
            const targetY = canvas.height * 0.15;
            const colors = ['#ff0040', '#ff8000', '#ffff00', '#40ff00', '#0080ff', '#8000ff', '#ff0080', '#ffffff'];
            
            const firework = new Firework(x, canvas.height, targetY, colors, 'big');
            fireworks.push(firework);
        }
        
        function launchMultipleFireworks() {
            for (let i = 0; i < 5; i++) {
                setTimeout(() => {
                    launchFirework();
                }, i * 300);
            }
        }
        
        function toggleFireworks() {
            isActive = !isActive;
            const btn = document.getElementById('toggleBtn');
            btn.textContent = isActive ? '花火 停止' : '花火 開始';
        }
        
        // 星空背景
        const stars = [];
        for (let i = 0; i < 200; i++) {
            stars.push({
                x: Math.random() * window.innerWidth,
                y: Math.random() * window.innerHeight * 0.7,
                size: Math.random() * 2 + 0.5,
                twinkle: Math.random() * Math.PI * 2,
                speed: Math.random() * 0.02 + 0.01
            });
        }
        
        // メインループ
        function animate() {
            // 背景のグラデーション
            const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
            gradient.addColorStop(0, '#000428');
            gradient.addColorStop(0.4, '#004e92');
            gradient.addColorStop(1, '#001845');
            
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 星空を描画
            stars.forEach(star => {
                star.twinkle += star.speed;
                const alpha = 0.3 + 0.7 * Math.sin(star.twinkle);
                
                ctx.save();
                ctx.globalAlpha = alpha;
                ctx.fillStyle = '#ffffff';
                ctx.shadowBlur = 3;
                ctx.shadowColor = '#ffffff';
                ctx.beginPath();
                ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
                ctx.fill();
                ctx.restore();
            });
            
            // 花火の更新と描画
            fireworks = fireworks.filter(firework => {
                firework.update();
                firework.draw();
                return !firework.isDead();
            });
            
            const currentTime = Date.now();
            
            // 定期的に花火を打ち上げ
            if (isActive && Math.random() < 0.08) {
                launchFirework();
                lastLaunchTime = currentTime;
            }
            
            // 同時に複数打ち上げ(華やかに)
            if (isActive && Math.random() < 0.03) {
                const count = Math.floor(Math.random() * 4) + 2;
                for (let i = 0; i < count; i++) {
                    setTimeout(() => launchFirework(), i * 150);
                }
            }
            
            // 特大花火を定期的に
            if (isActive && Math.random() < 0.005) {
                launchBigFirework();
            }
            
            requestAnimationFrame(animate);
        }
        
        // 初期花火を打ち上げ
        setTimeout(() => {
            for (let i = 0; i < 3; i++) {
                setTimeout(() => launchFirework(), i * 800);
            }
        }, 1000);
        
        // 華やかなオープニング
        setTimeout(() => {
            for (let i = 0; i < 5; i++) {
                setTimeout(() => launchBigFirework(), i * 600);
            }
        }, 3000);
        
        // アニメーション開始
        animate();
        
        // クリックで花火を打ち上げ
        canvas.addEventListener('click', (e) => {
            const rect = canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const targetY = Math.max(e.clientY - rect.top, canvas.height * 0.1);
            
            launchFirework(x, targetY);
        });
        
        // キーボードショートカット
        document.addEventListener('keydown', (e) => {
            switch(e.key) {
                case ' ':
                    e.preventDefault();
                    launchFirework();
                    break;
                case 'b':
                case 'B':
                    launchBigFirework();
                    break;
                case 'm':
                case 'M':
                    launchMultipleFireworks();
                    break;
                case 't':
                case 'T':
                    toggleFireworks();
                    break;
            }
        });
    </script>
</body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment