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

GPT-5で作成したプログラム ver.3

<!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: 4,            // 軌跡短め(軽量)
        fadeAlpha: 0.16,        // 残像抑えめ
        rocketMinVy: -12.5,
        rocketMaxVy: -17.5,
        rocketSpread: 0.9,
        explodeAltitudeMin: 0.25,
        explodeAltitudeMax: 0.55,
        particlesPerBurstMin: 160,
        particlesPerBurstMax: 320,
        megaEvery: 6,
        megaMultiplier: 2.0,
        launchEveryMs: 560,
        stormEveryMs: 3000,
        stormCountMin: 3,
        stormCountMax: 6,
        maxLiveParticles: 16000,
        // 描画(スプライト方式)
        particleRadius: 2.8,
        sparkleRadius: 1.2,
        trailAlpha: 0.42,
        trailWidth: 1.6
      };

      // ================= Canvas =================
      const canvas = document.getElementById('fw');
      const ctx = canvas.getContext('2d');
      let W = 0, H = 0, DPR = Math.min(window.devicePixelRatio || 1, CFG.dprMax);

      // 発光スプライト(1回生成)
      let glowSprite = null, sparkleSprite = null;
      function buildSprites(){
        const mk = (size, innerAlpha=0.95) => {
          const c = document.createElement('canvas');
          c.width = c.height = size;
          const g = c.getContext('2d');
          const r = size/2;
          const grad = g.createRadialGradient(r, r, 0, r, r, r);
          grad.addColorStop(0, `rgba(255,255,255,${innerAlpha})`);
          grad.addColorStop(0.25, 'rgba(255,255,255,0.55)');
          grad.addColorStop(0.6, 'rgba(255,255,255,0.18)');
          grad.addColorStop(1, 'rgba(255,255,255,0)');
          g.fillStyle = grad;
          g.beginPath(); g.arc(r, r, r, 0, Math.PI*2); g.fill();
          return c;
        };
        const gsz = Math.ceil(32 * DPR);
        glowSprite = mk(gsz, 0.9);
        sparkleSprite = mk(Math.ceil(18 * DPR), 1.0);
      }

      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;
        buildSprites();
      }
      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})`; }
      function cycle360(h){ return (h % 360 + 360) % 360; }
      function pick(arr){ return arr[(Math.random() * arr.length) | 0]; }
      function makePalette(base){
        base = cycle360(base);
        const t = randi(0,4);
        switch(t){
          case 0: return [base, cycle360(base+180), cycle360(base+30), cycle360(base-30)];
          case 1: return [base, cycle360(base+120), cycle360(base+240)];
          case 2: return [cycle360(base-25), cycle360(base-10), base, cycle360(base+15), cycle360(base+30)];
          case 3: { const shift = randi(0,59); return [0,60,120,180,240,300].map(h=>cycle360(h+shift)); }
          default: return [cycle360(base), cycle360(base+40), 25, 50, 200, 220];
        }
      }

      // FPSに応じた動的品質スケーラ
      const PERF = { ema: 16, q: 1.0 };

      // ================= 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() {
          const size = (CFG.particleRadius * 1.4) * DPR;
          ctx.globalAlpha = 0.95;
          ctx.drawImage(glowSprite, this.x - size/2, this.y - size/2, size, size);
          ctx.globalAlpha = 1.0;
        }
      }

      class Particle {
        constructor(x, y, spd, ang, hue, sparkle=false, life=1.0, sat=0.9, val=1.0, hueDrift=0, splitCapable=false, palette=null) {
          this.x = x; this.y = y;
          this.vx = Math.cos(ang) * spd;
          this.vy = Math.sin(ang) * spd;
          this.hue = hue; this.sat = sat; this.val = val;
          this.hueDrift = hueDrift;
          this.life = life;
          this.sparkle = sparkle;
          this.splitCapable = splitCapable; this.split = false;
          this.palette = palette;
          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.hue = cycle360(this.hue + this.hueDrift * dt);
          const load = particles.length / CFG.maxLiveParticles;
          this.life -= (0.009 + load * 0.006) * dt;
          if (this.life <= 0) this.life = 0;

          if (this.splitCapable && !this.split && this.life < 0.6) {
            this.split = true;
            const n = 3 + (Math.random() < 0.3 ? 1 : 0);
            for (let k=0;k<n;k++){
              if (particles.length > CFG.maxLiveParticles) break;
              const ang = Math.random() * TAU;
              const spd = rand(2.4, 3.8) * DPR;
              const hue = this.palette ? pick(this.palette) : cycle360(this.hue + randi(-20,20));
              particles.push(new Particle(this.x, this.y, spd, ang, hue, true, 0.8, this.sat, this.val, 0.6, false, this.palette));
            }
          }
        }
        draw() {
          const [r,g,b] = hsv2rgb(this.hue, this.sat, this.val);
          // 軌跡(軽量ライン)
          if (this.trail.length > 1) {
            ctx.beginPath();
            ctx.moveTo(this.trail[0][0], this.trail[0][1]);
            for (let i=1;i<this.trail.length;i++) ctx.lineTo(this.trail[i][0], this.trail[i][1]);
            ctx.lineWidth = CFG.trailWidth * DPR;
            ctx.strokeStyle = rgba(r,g,b, CFG.trailAlpha * this.life);
            ctx.stroke();
          }
          // 本体(スプライトで発光)
          const size = (CFG.particleRadius * 2.2) * DPR;
          ctx.globalAlpha = 0.95 * this.life;
          ctx.drawImage(glowSprite, this.x - size/2, this.y - size/2, size, size);
          ctx.globalAlpha = 1.0;
          // 芯の白点
          ctx.beginPath();
          ctx.arc(this.x, this.y, (CFG.particleRadius*0.55)*DPR, 0, TAU);
          ctx.fillStyle = rgba(255,255,255, 0.35 * this.life);
          ctx.fill();
          // キラキラ
          if (this.sparkle && Math.random() < 0.22) {
            const s = (CFG.sparkleRadius * 2.0) * DPR;
            ctx.globalAlpha = 0.7 * this.life;
            ctx.drawImage(sparkleSprite, this.x - s/2, this.y - s/2, s, s);
            ctx.globalAlpha = 1.0;
          }
        }
      }

      // ================= State =================
      const rockets = [];
      const particles = [];
      let launchCount = 0;

      // ================= Explosion =================
      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;
        count = (count * PERF.q) | 0; // 品質スケール適用

        const baseHue = cycle360(hueSeed + randi(-20, 20));
        const palette = makePalette(baseHue);
        const mode = randi(0, 5);
        const V = isMega ? 1.0 : 0.95;
        const S = 0.92;

        const spawn = (spd, ang, hue, opt={}) => {
          if (particles.length > CFG.maxLiveParticles) return;
          particles.push(new Particle(
            x, y,
            spd, ang,
            hue,
            opt.sparkle ?? false,
            opt.life ?? 1.0,
            opt.sat ?? S,
            opt.val ?? V,
            opt.hueDrift ?? 0,
            opt.split ?? false,
            palette
          ));
        };

        const coreN = (count * 0.14) | 0;
        const midN  = (count * 0.42) | 0;
        const outN  = count - coreN - midN;

        // 芯(金白)
        for (let i = 0; i < coreN; i++) {
          const a = Math.random() * TAU;
          const spd = rand(2.4, 3.3) * DPR * (isMega ? 1.2 : 1);
          const hue = 45 + randi(-8, 8);
          spawn(spd, a, hue, { sparkle: true, life: 1.1, sat: 0.65, val: 1.0 });
        }
        // 中殻(形状)
        for (let i = 0; i < midN; i++) {
          let a = (TAU * i / Math.max(1, midN)) + Math.random()*0.02;
          let spd;
          switch (mode) {
            case 0: spd = rand(3.6, 6.2) * DPR; break;                 // 菊
            case 1: spd = 5.4 * DPR; break;                              // リング
            case 2: spd = rand(2.8, 4.2) * DPR; break;                   // 柳
            case 3: spd = rand(3.4, 5.4) * DPR; a += i * 0.02; break;    // スパイラル
            case 4: spd = rand(4.8, 7.0) * DPR; if (Math.random()<0.15) spd *= 1.35; break; // パーム
            case 5: {
              const r = (1 - Math.sin(a)) * 3.4 * DPR * (isMega ? 1.2 : 1); // ハート
              spd = r; break;
            }
          }
          const hue = pick(palette);
          const hueDrift = Math.random() < 0.5 ? rand(0.2, 0.8) : rand(-0.8, -0.2);
          const split = Math.random() < 0.18 && (mode !== 1);
          spawn(spd, a, hue, { sparkle: Math.random()<0.4, life: mode===2?1.6:1.2, hueDrift, split });
        }
        // 外殻(色漂い)
        for (let i = 0; i < outN; i++) {
          const a = Math.random() * TAU;
          let spd = rand(3.8, 6.8) * DPR * (isMega ? 1.15 : 1);
          const hue = pick(palette);
          const hueDrift = rand(-1.1, 1.1);
          spawn(spd, a, hue, { sparkle: true, life: 1.1, hueDrift });
        }
      }

      // ================= Launch =================
      function scheduleLaunch(amount = 1) {
        if (PERF.ema > 22) amount = Math.max(1, Math.floor(amount * 0.7));
        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(() => { scheduleLaunch(randi(1, 2)); }, CFG.launchEveryMs);
      setInterval(() => { scheduleLaunch(randi(CFG.stormCountMin, CFG.stormCountMax)); }, CFG.stormEveryMs);

      // ================= Loop =================
      let last = performance.now();
      function loop(now) {
        requestAnimationFrame(loop);
        const dtMs = now - last; last = now;
        const dt = clamp(dtMs / (1000/60), 0.5, 2.5);

        // FPSの指数移動平均を更新し、品質qを調整
        PERF.ema = PERF.ema * 0.9 + dtMs * 0.1;
        if (PERF.ema > 22) PERF.q = Math.max(0.65, PERF.q * 0.985);
        else if (PERF.ema < 16) PERF.q = Math.min(1.1, PERF.q * 1.01);

        // フレーム描画
        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