Created
August 18, 2025 06:58
-
-
Save kujirahand/e312341b1ed9cd43a1544ed21862f328 to your computer and use it in GitHub Desktop.
花火の打ち上げプログラム
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
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
GPT-5で作成したプログラム ver.3