ORIGINAL SOURCE / MODEL 09
Qwen3.8 27B — 生成コード原文
生成された index.html の内容をそのままテキスト表示しています。このページではコードを実行しません。ゲームの評価・修正は行っていません。
ゲームを開く際は音量にご注意ください。動作を保証するものではありません。
index.htmlORIGINAL / UNEDITED
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ブロックくずし(Breaker)</title>
<style>
html, body {
margin: 0; padding: 0; min-height: 100%;
background: #0a0e1e;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", "Noto Sans JP", "Meiryo", sans-serif;
}
h1 { color: #e8ecff; margin: 14px 0 6px; font-size: 22px; letter-spacing: 6px; font-weight: 700; }
canvas {
background: #0d1226;
border-radius: 12px;
box-shadow: 0 12px 48px rgba(0, 0, 0, .65);
max-width: 96vw; height: auto;
touch-action: none;
cursor: crosshair;
}
.hint { color: #8b93b8; font-size: 12px; margin-top: 10px; letter-spacing: 1px; }
</style>
</head>
<body>
<h1>B R E A K E R</h1>
<canvas id="game" width="800" height="600"></canvas>
<div class="hint">← →(A / D):ラケット移動 | Space:開始・発球 | M:ミュート</div>
<script>
"use strict";
/* =====================================================================
* ブロックくずし(Breaker)
* ・単一ファイル / オフライン動作 / 音声は Web Audio API でリアルタイム合成
* (外部音源・BGMファイルを使用しないため著作権に抵触しない)
* ===================================================================== */
const W = 800, H = 600;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
/* ---------------- 音(Web Audio 合成) ---------------- */
const A = {
ctx: null, master: null, sfx: null, music: null, noiseBuf: null, muted: false,
ensure() {
if (this.ctx) { if (this.ctx.state === 'suspended') this.ctx.resume(); return true; }
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return false;
const c = new AC();
this.ctx = c;
this.master = c.createGain(); this.master.gain.value = 0.6; this.master.connect(c.destination);
this.sfx = c.createGain(); this.sfx.gain.value = 1.0; this.sfx.connect(this.master);
this.music = c.createGain(); this.music.gain.value = 0.30; this.music.connect(this.master);
// ハイハット用のノイズバッファ(0.06 秒)
const len = Math.floor(c.sampleRate * 0.06);
const buf = c.createBuffer(1, len, c.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2);
this.noiseBuf = buf;
return true;
},
// 指定時刻に発音(オシレーター + エンベロープ)
tone(freq, t, dur, type, vol, dest, slideTo) {
const c = this.ctx;
const o = c.createOscillator(), g = c.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, t);
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(30, slideTo), t + dur);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(vol, t + 0.012);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(dest || this.sfx);
o.start(t); o.stop(t + dur + 0.05);
},
hat(t, vol) {
const c = this.ctx;
const s = c.createBufferSource(); s.buffer = this.noiseBuf;
const f = c.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = 6500;
const g = c.createGain(); g.gain.value = vol;
s.connect(f); f.connect(g); g.connect(this.music);
s.start(t);
},
toggleMute() {
this.muted = !this.muted;
if (this.ctx) this.master.gain.value = this.muted ? 0.0001 : 0.6;
return this.muted;
}
};
// 音名(C4 等)→ 周波数
function nf(name) {
const m = name.match(/^([A-G])(#|b)?(-?\d)$/);
let s = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }[m[1]];
if (m[2] === '#') s += 1; else if (m[2] === 'b') s -= 1;
const midi = (parseInt(m[3]) + 1) * 12 + s;
return 440 * Math.pow(2, (midi - 69) / 12);
}
/* ---------------- BGM(チップチューン風ループ、128 BPM) ---------------- */
const LEAD = [ // メロディ(8 分音符 32 歩)
'E4','G4','C5','G4','E4','G4','C5','E5',
'D4','F4','A4','F4','D4','F4','A4','D5',
'C4','E4','G4','E4','G4','B4','D5','B4',
'A3','C4','E4','C4','G4','B4','D5','G4'
];
const BASS = [ // ベース(4 分音符 16 歩)
'C3','C3','G2','G2','F2','F2','C3','C3',
'C3','C3','G2','G2','A2','A2','G2','D3'
];
const BGM = {
playing: false, step: 0, nextT: 0, timer: null,
stepDur() { return 60 / 128 / 2; }, // 8 分音符の長さ(秒)
start() {
if (!A.ensure() || this.playing) return;
this.playing = true;
this.step = 0;
this.nextT = A.ctx.currentTime + 0.1;
const tick = () => {
if (!this.playing) return;
const ahead = A.ctx.currentTime + 0.18; // 先行量
while (this.nextT < ahead) {
this.playStep(this.step, this.nextT);
this.nextT += this.stepDur();
this.step = (this.step + 1) % LEAD.length;
}
};
this.timer = setInterval(tick, 40);
tick();
},
stop() {
this.playing = false;
if (this.timer) { clearInterval(this.timer); this.timer = null; }
},
playStep(i, t) {
const sd = this.stepDur();
const l = LEAD[i];
if (l) A.tone(nf(l), t, sd * 0.9, 'square', 0.10, A.music);
const b = BASS[(i / 2 | 0) % BASS.length];
if (b) A.tone(nf(b), t, sd * 1.7, 'triangle', 0.28, A.music);
A.hat(t, 0.05);
}
};
/* ---------------- 効果音 ---------------- */
const Sfx = {
now() { return A.ctx.currentTime; },
paddle() { if (A.ensure()) A.tone(340, this.now(), 0.09, 'square', 0.22, A.sfx, 520); },
wall() { if (A.ensure()) A.tone(190, this.now(), 0.06, 'triangle', 0.18, A.sfx); },
brick(row) { if (A.ensure()) A.tone(430 + (5 - row) * 60, this.now(), 0.08, 'square', 0.2, A.sfx); },
miss() { if (A.ensure()) A.tone(300, this.now(), 0.5, 'sawtooth', 0.22, A.sfx, 70); },
launch() { if (A.ensure()) A.tone(500, this.now(), 0.12, 'square', 0.18, A.sfx, 900); },
start() {
if (!A.ensure()) return; const t = this.now();
A.tone(523, t, 0.1, 'square', 0.18, A.sfx);
A.tone(784, t + 0.1, 0.18, 'square', 0.18, A.sfx);
},
gameover() {
if (!A.ensure()) return; const t = this.now();
A.tone(392, t, 0.18, 'square', 0.2, A.sfx);
A.tone(311, t + 0.2, 0.18, 'square', 0.2, A.sfx);
A.tone(233, t + 0.4, 0.5, 'square', 0.2, A.sfx);
},
win() {
if (!A.ensure()) return; const t = this.now();
['C5','E5','G5','C6'].forEach((n, i) => A.tone(nf(n), t + i * 0.14, 0.16, 'square', 0.2, A.sfx));
A.tone(nf('C6'), t + 0.56, 0.6, 'square', 0.2, A.sfx);
}
};
/* ---------------- ゲーム定数 / 状態 ---------------- */
const ROWS = 6, COLS = 10;
const BRICK = { w: 70.4, h: 22, gap: 4, top: 64 };
// 層ごとに色を変える(左:本体、右:ハイライト)
const ROW_COLORS = [
['#ff5964', '#ff97a0'], // 赤
['#ff9f43', '#ffc78f'], // 橙
['#feca57', '#ffe9a3'], // 黄
['#2ed573', '#7deda8'], // 緑
['#1e90ff', '#6cb8ff'], // 青
['#a55eea', '#c78ff2'] // 紫
];
const PADDLE = { w: 110, h: 14, y: H - 38, speed: 540 };
const BALL_R = 8;
const BASE_SPEED = 380, MAX_SPEED = 720, SPEED_UP = 7;
const MAX_LIVES = 5;
let mode = 'start'; // start | playing | gameover | win | quit
let bricks = [];
let score = 0;
let lives = MAX_LIVES;
let paddleX = (W - PADDLE.w) / 2;
let ball = { x: W / 2, y: 0, vx: 0, vy: 0, speed: BASE_SPEED, stuck: true };
let particles = [];
let trail = [];
let lastTime = 0;
const keys = { left: false, right: false };
const mouse = { x: W / 2 };
function buildBricks() {
bricks = [];
const totalW = COLS * BRICK.w + (COLS - 1) * BRICK.gap;
const left = (W - totalW) / 2;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
bricks.push({
x: left + c * (BRICK.w + BRICK.gap),
y: BRICK.top + r * (BRICK.h + BRICK.gap),
row: r, hit: false
});
}
}
}
function resetBall() {
ball.speed = BASE_SPEED;
ball.stuck = true;
ball.vx = 0; ball.vy = 0;
ball.x = paddleX + PADDLE.w / 2;
ball.y = PADDLE.y - BALL_R - 1;
trail = [];
}
function newGame() {
score = 0;
lives = MAX_LIVES;
particles = [];
paddleX = (W - PADDLE.w) / 2;
buildBricks();
resetBall();
mode = 'playing';
BGM.start();
}
/* ---------------- 入力 ---------------- */
function clampPaddle(x) { return Math.max(0, Math.min(W - PADDLE.w, x)); }
function launch() {
if (!ball.stuck) return;
ball.stuck = false;
const ang = -Math.PI / 2 + (Math.random() * 2 - 1) * 0.35;
ball.vx = ball.speed * Math.cos(ang);
ball.vy = ball.speed * Math.sin(ang);
Sfx.launch();
}
function doAction() {
if (mode === 'start') { Sfx.start(); newGame(); }
else if (mode === 'playing') { launch(); }
else if (mode === 'gameover' || mode === 'win') { Sfx.start(); newGame(); }
else if (mode === 'quit') { mode = 'start'; }
}
window.addEventListener('keydown', (e) => {
if ([' ', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault();
const k = e.key;
if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = true;
if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = true;
if (e.repeat) return;
if (k === 'm' || k === 'M') { A.ensure(); A.toggleMute(); return; }
if (k === ' ' || k === 'Enter') { doAction(); return; }
if ((k === 'r' || k === 'R') && (mode === 'gameover' || mode === 'win')) { Sfx.start(); newGame(); return; }
if (k === 'Escape' && (mode === 'gameover' || mode === 'win')) { BGM.stop(); mode = 'quit'; return; }
});
window.addEventListener('keyup', (e) => {
const k = e.key;
if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = false;
if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = false;
});
function pointerX(e) {
const r = canvas.getBoundingClientRect();
return (e.clientX - r.left) * (W / r.width);
}
canvas.addEventListener('mousemove', (e) => {
mouse.x = pointerX(e);
if (mode === 'playing') paddleX = clampPaddle(mouse.x - PADDLE.w / 2);
});
canvas.addEventListener('click', () => doAction());
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (e.touches.length) mouse.x = pointerX(e.touches[0]);
doAction();
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (e.touches.length) {
mouse.x = pointerX(e.touches[0]);
if (mode === 'playing') paddleX = clampPaddle(mouse.x - PADDLE.w / 2);
}
}, { passive: false });
/* ---------------- 更新処理 ---------------- */
function spawnParticles(b) {
const [c1] = ROW_COLORS[b.row];
for (let i = 0; i < 12; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 60 + Math.random() * 220;
particles.push({
x: b.x + BRICK.w / 2, y: b.y + BRICK.h / 2,
vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 60,
life: 0.5 + Math.random() * 0.3, t: 0,
size: 2 + Math.random() * 3, color: c1
});
}
}
function update(dt) {
// キーによるラケット移動
let dir = 0;
if (keys.left) dir -= 1;
if (keys.right) dir += 1;
if (dir) paddleX = clampPaddle(paddleX + dir * PADDLE.speed * dt);
// パーティクル
for (const p of particles) {
p.t += dt;
p.vy += 500 * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
}
particles = particles.filter(p => p.t < p.life);
if (mode !== 'playing') return;
if (ball.stuck) {
ball.x = paddleX + PADDLE.w / 2;
ball.y = PADDLE.y - BALL_R - 1;
return;
}
// 高速ボールのトンネル防止のためサブステップ
const dist = Math.hypot(ball.vx, ball.vy) * dt;
const steps = Math.min(5, Math.max(1, Math.ceil(dist / (BALL_R * 0.8))));
const sdt = dt / steps;
for (let s = 0; s < steps && mode === 'playing'; s++) {
ball.x += ball.vx * sdt;
ball.y += ball.vy * sdt;
// 壁(左右・上)
if (ball.x < BALL_R) { ball.x = BALL_R; ball.vx = Math.abs(ball.vx); Sfx.wall(); }
else if (ball.x > W - BALL_R) { ball.x = W - BALL_R; ball.vx = -Math.abs(ball.vx); Sfx.wall(); }
if (ball.y < BALL_R) { ball.y = BALL_R; ball.vy = Math.abs(ball.vy); Sfx.wall(); }
// ラケット(ボールが下向きで、ラケットの上面付近に到達した場合のみ)
if (ball.vy > 0 &&
ball.y + BALL_R >= PADDLE.y && ball.y + BALL_R <= PADDLE.y + PADDLE.h + 12 &&
ball.x >= paddleX - BALL_R && ball.x <= paddleX + PADDLE.w + BALL_R) {
const off = Math.max(-1, Math.min(1, (ball.x - (paddleX + PADDLE.w / 2)) / (PADDLE.w / 2)));
const ang = off * (Math.PI / 3); // 最大 60°
ball.speed = Math.min(MAX_SPEED, ball.speed + SPEED_UP);
ball.vx = ball.speed * Math.sin(ang);
ball.vy = -ball.speed * Math.cos(ang);
ball.y = PADDLE.y - BALL_R - 0.5;
Sfx.paddle();
}
// ブロック
for (const b of bricks) {
if (b.hit) continue;
const nx = Math.max(b.x, Math.min(ball.x, b.x + BRICK.w));
const ny = Math.max(b.y, Math.min(ball.y, b.y + BRICK.h));
const dx = ball.x - nx, dy = ball.y - ny;
if (dx * dx + dy * dy > BALL_R * BALL_R) continue;
b.hit = true;
score += 10;
Sfx.brick(b.row);
spawnParticles(b);
// 入った方向を判定して反射(境界押し出し)
const cx = b.x + BRICK.w / 2, cy = b.y + BRICK.h / 2;
if (Math.abs(ball.x - cx) / (BRICK.w / 2) > Math.abs(ball.y - cy) / (BRICK.h / 2)) {
ball.vx = ball.x < cx ? -Math.abs(ball.vx) : Math.abs(ball.vx);
ball.x = ball.x < cx ? b.x - BALL_R - 0.5 : b.x + BRICK.w + BALL_R + 0.5;
} else {
ball.vy = ball.y < cy ? -Math.abs(ball.vy) : Math.abs(ball.vy);
ball.y = ball.y < cy ? b.y - BALL_R - 0.5 : b.y + BRICK.h + BALL_R + 0.5;
}
break; // フレームごとのブロック当たりは 1 回
}
// 水平運動に張り付くのを防ぐ
const sp = Math.hypot(ball.vx, ball.vy);
if (sp > 0 && Math.abs(ball.vy) < sp * 0.22) {
const sign = ball.vy < 0 ? -1 : 1;
ball.vy = sign * sp * 0.22;
ball.vx = Math.sign(ball.vx || 1) * Math.sqrt(Math.max(0, sp * sp - ball.vy * ball.vy));
}
// 落下(ラケットを逃した)
if (ball.y - BALL_R > H) {
lives -= 1;
Sfx.miss();
if (lives <= 0) {
mode = 'gameover';
BGM.stop();
Sfx.gameover();
} else {
resetBall();
}
break;
}
}
// トレール
trail.push({ x: ball.x, y: ball.y });
if (trail.length > 14) trail.shift();
// クリア判定
if (mode === 'playing' && bricks.every(b => b.hit)) {
score += 100; // クリアボーナス
mode = 'win';
BGM.stop();
Sfx.win();
}
}
/* ---------------- 描画 ---------------- */
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#101736');
g.addColorStop(0.6, '#0d1226');
g.addColorStop(1, '#0a0e1e');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
}
function drawBricks() {
for (const b of bricks) {
if (b.hit) continue;
const [c1, c2] = ROW_COLORS[b.row];
const g = ctx.createLinearGradient(0, b.y, 0, b.y + BRICK.h);
g.addColorStop(0, c2);
g.addColorStop(0.45, c1);
g.addColorStop(1, c1);
ctx.fillStyle = g;
roundRect(b.x, b.y, BRICK.w, BRICK.h, 4);
ctx.fill();
// 上面のハイライト
ctx.fillStyle = 'rgba(255,255,255,0.25)';
roundRect(b.x + 2, b.y + 2, BRICK.w - 4, 4, 2);
ctx.fill();
}
}
function drawPaddle() {
const g = ctx.createLinearGradient(0, PADDLE.y, 0, PADDLE.y + PADDLE.h);
g.addColorStop(0, '#c8f4ff');
g.addColorStop(1, '#38bdf8');
ctx.fillStyle = g;
roundRect(paddleX, PADDLE.y, PADDLE.w, PADDLE.h, 7);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.5)';
roundRect(paddleX + 6, PADDLE.y + 2, PADDLE.w - 12, 3, 2);
ctx.fill();
}
function drawBall() {
// トレール
for (let i = 0; i < trail.length; i++) {
const p = trail[i];
const a = (i + 1) / trail.length;
ctx.globalAlpha = a * 0.18;
ctx.fillStyle = '#7dd3fc';
ctx.beginPath();
ctx.arc(p.x, p.y, BALL_R * (0.4 + a * 0.5), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.save();
ctx.shadowColor = '#7dd3fc';
ctx.shadowBlur = 14;
const g = ctx.createRadialGradient(ball.x - 3, ball.y - 3, 1, ball.x, ball.y, BALL_R);
g.addColorStop(0, '#ffffff');
g.addColorStop(1, '#bae6fd');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawParticles() {
for (const p of particles) {
ctx.globalAlpha = Math.max(0, 1 - p.t / p.life);
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
ctx.textBaseline = 'top';
// スコア
ctx.textAlign = 'left';
ctx.font = '600 18px "Segoe UI", "Hiragino Kaku Gothic ProN", sans-serif';
ctx.fillStyle = '#c7d2fe';
ctx.fillText('SCORE ' + score, 16, 14);
// ラケット残数(ラケットアイコンで表示)
ctx.textAlign = 'right';
ctx.fillText('ラケット', W - 96, 16);
for (let i = 0; i < MAX_LIVES; i++) {
const x = W - 88 + i * 16;
ctx.fillStyle = i < lives ? '#7dd3fc' : 'rgba(148,163,184,0.35)';
roundRect(x, 16, 12, 8, 3);
ctx.fill();
}
// ミュート表示
if (A.muted) {
ctx.textAlign = 'left';
ctx.font = '12px "Segoe UI", sans-serif';
ctx.fillStyle = '#f87171';
ctx.fillText('MUTED(M で解除)', 16, 40);
}
}
function blinkText(text, x, y, font, color, strong) {
const a = 0.55 + 0.45 * Math.sin(performance.now() / 220);
ctx.globalAlpha = a;
ctx.font = font;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (strong) { ctx.save(); ctx.shadowColor = color; ctx.shadowBlur = 18; ctx.fillText(text, x, y); ctx.restore(); }
else ctx.fillText(text, x, y);
ctx.globalAlpha = 1;
}
function overlay(title, titleColor, lines, strong) {
ctx.fillStyle = 'rgba(6,9,22,0.72)';
ctx.fillRect(0, 0, W, H);
blinkText(title, W / 2, 208, 'bold 54px "Segoe UI", "Hiragino Kaku Gothic ProN", sans-serif', titleColor, true);
let y = 292;
for (const [t, c, f] of lines) {
ctx.font = f;
ctx.fillStyle = c;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(t, W / 2, y);
y += f.includes('16px') ? 30 : 38;
}
}
const FONT_JP = '"Segoe UI", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif';
function drawScreens() {
if (mode === 'start') {
overlay('ブロックくずし', '#7dd3fc', [
['すべてのブロックを破壊してラケットを守れ!', '#dbe4ff', '16px ' + FONT_JP],
['← → キー / A・D / マウスの左右移動:ラケット', '#aab3d6', '14px ' + FONT_JP],
['ラケットの当たる位置でボールの跳ね返り方向が変わる', '#aab3d6', '14px ' + FONT_JP],
['ボールを逃すとラケットが減る(全 5 本)', '#aab3d6', '14px ' + FONT_JP],
['Space / クリックで発球 ・ M でミュート', '#aab3d6', '14px ' + FONT_JP],
]);
blinkText('Space キーまたはクリックでスタート', W / 2, 470, 'bold 22px ' + FONT_JP, '#feca57', true);
} else if (mode === 'gameover') {
overlay('GAME OVER', '#ff5964', [
['スコア ' + score, '#ffffff', 'bold 26px ' + FONT_JP],
['R キー / クリック:もう一度プレイ', '#dbe4ff', '16px ' + FONT_JP],
['Esc キー:ゲーム終了', '#aab3d6', '16px ' + FONT_JP],
]);
} else if (mode === 'win') {
overlay('CLEAR!', '#2ed573', [
['ブロックをすべて破壊した!', '#dbe4ff', '16px ' + FONT_JP],
['スコア ' + score, '#ffffff', 'bold 26px ' + FONT_JP],
['R キー / クリック:もう一度プレイ', '#dbe4ff', '16px ' + FONT_JP],
['Esc キー:ゲーム終了', '#aab3d6', '16px ' + FONT_JP],
]);
} else if (mode === 'quit') {
overlay('ごプレイありがとうございました', '#a5b4fc', [
['また遊んでください!', '#dbe4ff', '16px ' + FONT_JP],
]);
blinkText('Space キーまたはクリックでタイトルへ', W / 2, 420, 'bold 20px ' + FONT_JP, '#feca57', true);
}
}
/* ---------------- メインループ ---------------- */
function frame(t) {
const dt = Math.min(0.033, (t - lastTime) / 1000 || 0.016);
lastTime = t;
update(dt);
drawBackground();
if (mode === 'start' || mode === 'quit') {
// タイトル背景としてブロックも薄く描く
ctx.globalAlpha = 0.35;
drawBricks();
ctx.globalAlpha = 1;
} else {
drawBricks();
}
drawParticles();
if (mode === 'playing' || mode === 'gameover' || mode === 'win') {
drawPaddle();
drawBall();
}
drawHUD();
drawScreens();
requestAnimationFrame(frame);
}
buildBricks();
resetBall();
requestAnimationFrame(frame);
</script>
</body>
</html>