GPT-6.1 Terra
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 13 min 57 s
- Cost
- $0.332
- Output tokens
- 66,354
- Spent thinking
- 46,792 tokens
- Speed
- 79.6 tok/s
- Console errors
- 0
- First token after
- 4.3 s
- First visible text after
- 11 min 26 s
- Prompt tokens
- 508
- Code size
- 57.7 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6.1-terra- Reasoning sent
{"effort":"max"}- Max output
- 128,000 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1669 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout — Playfield Arcade</title>
<style>
:root {
color-scheme: dark;
--bg: #090e17;
--panel: #111a27;
--border: #293748;
--muted: #8291a3;
--text: #edf5f6;
--mint: #81e9db;
--violet: #b9a4ff;
}
* { box-sizing: border-box; }
html { min-height: 100%; background: var(--bg); }
body {
margin: 0;
min-height: 100vh;
overflow-x: hidden;
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(ellipse at 13% 0%, rgba(39, 111, 120, .19), transparent 34%),
radial-gradient(ellipse at 90% 15%, rgba(94, 69, 143, .14), transparent 32%),
linear-gradient(160deg, #0c131e 0%, #090e17 55%, #080d15 100%);
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(170, 205, 218, .035) 1px, transparent 1px),
linear-gradient(90deg, rgba(170, 205, 218, .035) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: linear-gradient(to bottom, black, transparent 85%);
opacity: .42;
}
button { font: inherit; }
[hidden] { display: none !important; }
.page {
position: relative;
width: calc(100% - 64px);
max-width: 1140px;
margin: 0 auto;
}
.topbar {
height: 65px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(150, 177, 195, .13);
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border: 1px solid rgba(148, 236, 226, .35);
border-radius: 9px;
background: linear-gradient(145deg, #294c52, #182b38);
box-shadow: inset 0 1px rgba(255,255,255,.07), 0 0 22px rgba(79, 218, 200, .08);
}
.brand-mark svg { width: 23px; height: 23px; }
.brand-name {
font-size: 13px;
font-weight: 850;
letter-spacing: .12em;
line-height: 1;
}
.brand-name span { color: var(--mint); }
.brand-sub {
margin-top: 5px;
color: #6f8191;
font-size: 8px;
font-weight: 700;
letter-spacing: .16em;
line-height: 1;
}
.top-meta {
display: flex;
align-items: center;
gap: 10px;
color: #8294a4;
font-size: 9px;
font-weight: 750;
letter-spacing: .13em;
white-space: nowrap;
}
.live-dot {
width: 7px;
height: 7px;
display: inline-block;
flex: 0 0 auto;
border-radius: 50%;
background: #80ebd7;
box-shadow: 0 0 10px rgba(128, 235, 215, .8);
animation: breathe 2s ease-in-out infinite;
}
.top-divider { width: 1px; height: 15px; background: #334151; margin: 0 4px; }
.top-volume { color: #596c7e; }
@keyframes breathe { 50% { opacity: .55; box-shadow: 0 0 4px rgba(128,235,215,.5); } }
.intro {
display: flex;
justify-content: space-between;
align-items: end;
gap: 20px;
padding: 20px 0 18px;
}
.eyebrow {
display: flex;
align-items: center;
gap: 8px;
color: #8ae4d9;
font-size: 9px;
line-height: 1;
font-weight: 800;
letter-spacing: .16em;
}
.eyebrow-line { width: 19px; height: 2px; border-radius: 2px; background: #74d9ce; }
.eyebrow .slash { color: #51697a; }
.title-line { display: flex; align-items: center; flex-wrap: wrap; gap: 13px; margin-top: 7px; }
h1 {
margin: 0;
font-size: clamp(30px, 3.3vw, 39px);
line-height: 1;
font-weight: 850;
letter-spacing: -.055em;
}
h1 span { color: var(--mint); }
.classic-tag {
padding: 5px 7px;
border: 1px solid #344454;
border-radius: 5px;
color: #9aabba;
background: rgba(29, 43, 57, .68);
font-size: 8px;
font-weight: 800;
letter-spacing: .12em;
white-space: nowrap;
}
.intro p {
margin: 7px 0 0;
color: #8e9cab;
font-size: 12px;
line-height: 1.3;
}
.intro-note { display: flex; align-items: center; gap: 10px; padding-bottom: 3px; }
.intro-note-icon {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border: 1px solid #344455;
border-radius: 50%;
color: #95e6db;
font-size: 17px;
}
.intro-note span, .intro-note strong { display: block; white-space: nowrap; }
.intro-note span { color: #728395; font-size: 8px; font-weight: 750; letter-spacing: .13em; }
.intro-note strong { margin-top: 4px; color: #bdcbd2; font-size: 10px; letter-spacing: .08em; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 258px;
gap: 16px;
align-items: stretch;
}
.game-frame, .guide {
min-width: 0;
border: 1px solid var(--border);
border-radius: 13px;
background: #111a27;
box-shadow: 0 18px 55px rgba(0, 0, 0, .24), inset 0 1px rgba(255,255,255,.035);
}
.game-frame { overflow: hidden; }
.hud {
min-height: 65px;
display: flex;
align-items: center;
padding: 0 19px;
background: linear-gradient(110deg, #182331, #131c29 70%);
}
.metric { flex: 0 0 auto; }
.metric-score { min-width: 96px; }
.metric-lives { min-width: 91px; }
.metric-bricks { min-width: 71px; }
.metric-label {
margin-bottom: 6px;
color: #7d8e9f;
font-size: 9px;
line-height: 1;
font-weight: 800;
letter-spacing: .13em;
}
.metric-value {
display: block;
color: #f2f7f8;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 18px;
line-height: 1;
font-weight: 750;
letter-spacing: .045em;
}
.metric-total { color: #637487; font-size: 12px; letter-spacing: 0; }
.hud-separator { width: 1px; height: 30px; margin: 0 18px 0 0; background: #314050; }
.hearts { display: flex; align-items: center; gap: 4px; height: 18px; }
.heart { width: 15px; height: 15px; fill: #ff8e98; filter: drop-shadow(0 0 5px rgba(255, 123, 142, .28)); }
.heart.lost { fill: #435160; filter: none; opacity: .55; }
.hud-actions { display: flex; align-items: center; gap: 9px; margin-left: auto; }
.level-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border: 1px solid #344758;
border-radius: 7px;
color: #b2c3ce;
background: #1a2837;
font-size: 9px;
line-height: 1;
font-weight: 800;
letter-spacing: .1em;
white-space: nowrap;
}
.level-diamond { color: var(--mint); font-size: 12px; line-height: 0; }
.icon-button {
width: 33px;
height: 33px;
display: grid;
place-items: center;
padding: 0;
border: 1px solid #344758;
border-radius: 7px;
background: #1a2837;
color: #9eafbb;
cursor: pointer;
transition: color .18s, border-color .18s, background .18s;
}
.icon-button:hover { color: #fff; border-color: #6e9b9d; background: #233746; }
.icon-button svg { width: 16px; height: 16px; }
button:focus-visible { outline: 2px solid #9cf4e7; outline-offset: 3px; }
.canvas-wrap { position: relative; overflow: hidden; background: #0b1420; touch-action: none; }
canvas { display: block; width: 100%; height: auto; cursor: pointer; }
.canvas-wrap.playing canvas { cursor: crosshair; }
.start-prompt {
position: absolute;
z-index: 2;
left: 50%;
bottom: 17%;
width: min(302px, calc(100% - 32px));
transform: translateX(-50%);
padding: 11px 14px 12px;
text-align: left;
border: 1px solid rgba(132, 216, 208, .34);
border-radius: 9px;
color: #f4fbfa;
background: rgba(15, 27, 40, .95);
box-shadow: 0 10px 35px rgba(0, 0, 0, .38), inset 0 1px rgba(255,255,255,.045);
backdrop-filter: blur(7px);
cursor: pointer;
transition: border-color .2s, background .2s;
}
.start-prompt:hover { border-color: #8ae7da; background: rgba(22, 43, 54, .98); }
.prompt-kicker {
display: flex;
align-items: center;
gap: 6px;
color: #88e4d8;
font-size: 8px;
line-height: 1;
font-weight: 850;
letter-spacing: .14em;
}
.prompt-kicker .live-dot { width: 5px; height: 5px; }
.prompt-kicker .dim { color: #6c8290; }
.prompt-main {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-top: 8px;
font-size: 13px;
line-height: 1.15;
font-weight: 750;
letter-spacing: -.01em;
}
.prompt-main svg { width: 15px; height: 15px; flex: 0 0 auto; color: #96e6db; }
.effect-badge, .power-toast {
position: absolute;
z-index: 1;
bottom: 14px;
display: flex;
align-items: center;
gap: 7px;
padding: 7px 9px;
border: 1px solid rgba(189, 166, 255, .32);
border-radius: 6px;
color: #d6cbff;
background: rgba(24, 26, 48, .92);
font-size: 9px;
line-height: 1;
font-weight: 850;
letter-spacing: .08em;
white-space: nowrap;
pointer-events: none;
}
.effect-badge { left: 14px; }
.effect-badge i { width: 6px; height: 6px; border-radius: 50%; background: #c4adff; box-shadow: 0 0 8px #a88cf5; }
.effect-badge.is-wide { border-color: rgba(128, 233, 219, .35); color: #a3f2e6; background: rgba(19, 43, 46, .94); }
.effect-badge.is-wide i { background: #81e9db; box-shadow: 0 0 8px #81e9db; }
.power-toast {
right: 14px;
opacity: 0;
transform: translateY(5px);
transition: opacity .2s, transform .2s;
}
.power-toast.show { opacity: 1; transform: translateY(0); }
.power-toast.wide { color: #a3f2e6; border-color: rgba(128, 233, 219, .35); background: rgba(19, 43, 46, .94); }
.power-toast.danger { color: #ffc1c5; border-color: rgba(255, 144, 152, .35); background: rgba(48, 26, 37, .94); }
.toast-star { font-size: 12px; }
.end-overlay {
position: absolute;
z-index: 5;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
background: rgba(6, 12, 21, .79);
backdrop-filter: blur(5px);
}
.end-card {
width: min(350px, 100%);
padding: 22px 28px 25px;
text-align: center;
border: 1px solid #3b5361;
border-radius: 13px;
background: linear-gradient(145deg, #1b2b39, #111c2a);
box-shadow: 0 20px 55px rgba(0,0,0,.45);
}
.end-icon {
width: 38px;
height: 38px;
display: grid;
place-items: center;
margin: 0 auto 11px;
border: 1px solid #4d827e;
border-radius: 11px;
color: var(--mint);
background: rgba(80, 189, 176, .12);
}
.end-icon svg { width: 20px; height: 20px; }
.end-eyebrow { color: #8ce6da; font-size: 9px; font-weight: 850; letter-spacing: .16em; }
.end-card h2 { margin: 7px 0 5px; font-size: 28px; line-height: 1.05; letter-spacing: -.045em; }
.end-card p { margin: 0; color: #a4b2bd; font-size: 12px; line-height: 1.45; }
.final-score { margin: 16px 0; }
.final-score span { display: block; color: #7f92a1; font-size: 9px; font-weight: 800; letter-spacing: .13em; }
.final-score strong { display: block; margin-top: 5px; font: 750 23px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .05em; }
.play-again {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
min-height: 39px;
padding: 0 20px;
border: 0;
border-radius: 7px;
color: #102a2c;
background: #91eadc;
font-size: 11px;
font-weight: 850;
letter-spacing: .08em;
cursor: pointer;
}
.play-again:hover { background: #b2fff2; }
.play-again svg { width: 15px; height: 15px; }
.guide { display: flex; flex-direction: column; padding: 19px; }
.guide-header { display: flex; align-items: center; gap: 10px; }
.guide-header-icon {
width: 35px;
height: 35px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: 1px solid #385052;
border-radius: 9px;
color: #93e9dd;
background: #1a3438;
}
.guide-header-icon svg { width: 17px; height: 17px; }
.guide-header h2 { margin: 0; font-size: 12px; line-height: 1; font-weight: 850; letter-spacing: .06em; }
.guide-header p { margin: 6px 0 0; color: #7f90a0; font-size: 10px; line-height: 1; }
.guide-number { margin-left: auto; color: #536879; font: 700 10px/1 ui-monospace, monospace; }
.guide-rule { height: 1px; margin: 17px 0 16px; background: #2a3949; }
.guide-section + .guide-section { margin-top: 20px; }
.guide-label {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
color: #778a9b;
font-size: 9px;
line-height: 1;
font-weight: 850;
letter-spacing: .13em;
}
.guide-label span { color: #4e6272; font-family: ui-monospace, monospace; letter-spacing: 0; }
.mission-copy { margin: 0; color: #e1e9ec; font-size: 14px; font-weight: 750; line-height: 1.45; letter-spacing: -.02em; }
.mission-copy span { color: #8e9eac; font-weight: 500; }
.info-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.info-row + .info-row { margin-top: 10px; }
.info-icon {
width: 36px;
height: 36px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: 1px solid #314253;
border-radius: 8px;
color: #a8b9c5;
background: #192534;
}
.info-icon svg { width: 19px; height: 19px; }
.key-icon { display: flex; gap: 3px; }
.key-icon kbd {
width: 13px;
height: 17px;
display: grid;
place-items: center;
border: 1px solid #647687;
border-radius: 3px;
font: 700 10px/1 system-ui, sans-serif;
}
.info-icon.wide { color: #91eee0; border-color: #315b5a; background: #1a383b; }
.info-icon.multi { color: #cbbcff; border-color: #514b70; background: #302c46; }
.info-text { min-width: 0; }
.info-text strong, .info-text small { display: block; }
.info-text strong { color: #dce6ea; font-size: 11px; line-height: 1.2; font-weight: 750; }
.info-text small { margin-top: 4px; color: #7d90a0; font-size: 10px; line-height: 1.2; }
.brick-tip {
display: flex;
align-items: center;
gap: 10px;
margin-top: auto;
padding: 11px;
border: 1px solid #314153;
border-radius: 9px;
background: #182534;
}
.brick-tip svg { width: 42px; height: 27px; flex: 0 0 auto; }
.brick-tip strong, .brick-tip span { display: block; }
.brick-tip strong { color: #cdd9e1; font-size: 9px; line-height: 1.2; font-weight: 850; letter-spacing: .05em; }
.brick-tip span { margin-top: 4px; color: #8698a8; font-size: 10px; line-height: 1.2; }
.guide-foot {
display: flex;
justify-content: space-between;
margin-top: 14px;
color: #5e7283;
font-size: 8px;
line-height: 1;
font-weight: 800;
letter-spacing: .1em;
}
footer {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 15px 1px 20px;
color: #596c7d;
font-size: 9px;
font-weight: 750;
letter-spacing: .1em;
}
footer span:last-child { color: #6d817f; }
@media (max-width: 900px) {
.page { width: calc(100% - 36px); }
.layout { grid-template-columns: minmax(0, 1fr) 235px; gap: 13px; }
.guide { padding: 16px; }
.hud { padding: 0 14px; }
.hud-separator { margin-right: 11px; }
.metric-score { min-width: 82px; }
.metric-lives { min-width: 78px; }
.metric-bricks { min-width: 62px; }
}
@media (max-width: 700px) {
.page { width: calc(100% - 28px); }
.topbar { height: 59px; }
.top-volume, .top-divider { display: none; }
.top-meta { font-size: 8px; letter-spacing: .08em; }
.intro { padding: 18px 0 16px; }
.intro-note { display: none; }
.intro p { font-size: 11px; }
.layout { grid-template-columns: minmax(0, 1fr); gap: 12px; }
.hud { min-height: 60px; padding: 0 10px; }
.metric-score { min-width: 61px; }
.metric-lives { min-width: 60px; }
.metric-bricks { min-width: 52px; }
.metric-label { font-size: 8px; margin-bottom: 5px; }
.metric-value { font-size: 15px; }
.metric-total { font-size: 10px; }
.hud-separator { height: 27px; margin-right: 8px; }
.hearts { gap: 2px; height: 15px; }
.heart { width: 13px; height: 13px; }
.hud-actions { gap: 5px; }
.level-chip { padding: 7px 6px; font-size: 8px; gap: 4px; }
.icon-button { width: 30px; height: 30px; }
.start-prompt { padding: 9px 12px 10px; bottom: 16%; width: min(285px, calc(100% - 30px)); }
.prompt-main { margin-top: 6px; font-size: 12px; }
.guide {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 17px 14px;
padding: 16px;
}
.guide-header, .guide-objective, .brick-tip, .guide-foot { grid-column: 1 / -1; }
.guide-rule { display: none; }
.guide-section + .guide-section { margin-top: 0; }
.guide-objective { padding-bottom: 13px; border-bottom: 1px solid #2a3949; }
.mission-copy { font-size: 13px; }
.brick-tip { margin-top: 0; }
.guide-foot { margin-top: 0; }
footer { font-size: 8px; }
.end-card { padding: 13px 18px 16px; }
.end-icon { display: none; }
.end-card h2 { font-size: 23px; }
.final-score { margin: 10px 0; }
.final-score strong { font-size: 20px; }
.play-again { min-height: 35px; }
}
@media (max-width: 390px) {
.classic-tag { font-size: 7px; padding: 5px; }
.title-line { gap: 8px; }
.top-meta { font-size: 7px; }
.effect-badge, .power-toast { font-size: 8px; padding: 6px; bottom: 9px; }
.effect-badge { left: 9px; }
.power-toast { right: 9px; }
.info-text strong { font-size: 10px; }
.info-text small { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.live-dot { animation: none; }
}
</style>
</head>
<body>
<div class="page">
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="7" height="4" rx="1.2" fill="#a7f5e9"/>
<rect x="12" y="4" width="9" height="4" rx="1.2" fill="#a7f5e9" opacity=".65"/>
<rect x="3" y="10" width="11" height="4" rx="1.2" fill="#a7f5e9" opacity=".65"/>
<rect x="16" y="10" width="5" height="4" rx="1.2" fill="#a7f5e9"/>
<rect x="7" y="19" width="10" height="2" rx="1" fill="#a7f5e9"/>
<circle cx="12" cy="16.5" r="1.5" fill="#e8fffa"/>
</svg>
</div>
<div>
<div class="brand-name">PLAYFIELD<span>.</span></div>
<div class="brand-sub">THE ARCADE COLLECTION</div>
</div>
</div>
<div class="top-meta">
<span class="live-dot"></span><span id="modeText">DEMO IN PROGRESS</span>
<span class="top-divider"></span><span class="top-volume">VOLUME 01</span>
</div>
</header>
<section class="intro">
<div>
<div class="eyebrow"><span class="eyebrow-line"></span> THE CLASSICS <span class="slash">/</span> GAME 01</div>
<div class="title-line"><h1>Breakout<span>.</span></h1><span class="classic-tag">ARCADE CLASSIC</span></div>
<p>Clear the wall. Keep the ball in play.</p>
</div>
<div class="intro-note" aria-hidden="true">
<div class="intro-note-icon">✳</div>
<div><span>YOUR OBJECTIVE</span><strong>EVERY LAST BRICK.</strong></div>
</div>
</section>
<main class="layout">
<section class="game-frame" aria-label="Breakout game">
<div class="hud">
<div class="metric metric-score">
<div class="metric-label">SCORE</div>
<strong class="metric-value" id="score">000000</strong>
</div>
<div class="hud-separator"></div>
<div class="metric metric-lives">
<div class="metric-label">LIVES</div>
<div class="hearts" id="hearts" aria-label="3 lives remaining">
<svg class="heart" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.1-4.3-9.1-8.4C1.1 8.8 3.2 5 6.9 5c2.1 0 3.9 1.1 5.1 2.7C13.2 6.1 15 5 17.1 5c3.7 0 5.8 3.8 4 7.6C19.1 16.7 12 21 12 21Z"/></svg>
<svg class="heart" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.1-4.3-9.1-8.4C1.1 8.8 3.2 5 6.9 5c2.1 0 3.9 1.1 5.1 2.7C13.2 6.1 15 5 17.1 5c3.7 0 5.8 3.8 4 7.6C19.1 16.7 12 21 12 21Z"/></svg>
<svg class="heart" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.1-4.3-9.1-8.4C1.1 8.8 3.2 5 6.9 5c2.1 0 3.9 1.1 5.1 2.7C13.2 6.1 15 5 17.1 5c3.7 0 5.8 3.8 4 7.6C19.1 16.7 12 21 12 21Z"/></svg>
</div>
</div>
<div class="hud-separator"></div>
<div class="metric metric-bricks">
<div class="metric-label">BRICKS</div>
<strong class="metric-value"><span id="brickCount">60</span><span class="metric-total"> / <span id="brickTotal">60</span></span></strong>
</div>
<div class="hud-actions">
<div class="level-chip"><span class="level-diamond">✦</span> LEVEL 01</div>
<button class="icon-button" id="restartTop" data-restart type="button" title="Restart game" aria-label="Restart game">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a8 8 0 1 1 2.4 5.7"/><path d="M4 5v6h6"/></svg>
</button>
</div>
</div>
<div class="canvas-wrap" id="field">
<canvas id="game" width="840" height="500" aria-label="Breakout playfield. Move the paddle with your mouse or the left and right arrow keys."></canvas>
<button class="start-prompt" id="startPrompt" type="button">
<span class="prompt-kicker"><span class="live-dot"></span> DEMO MODE <span class="dim">/ AI IS PLAYING</span></span>
<span class="prompt-main">Click or press a key to start
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10h11M11 5l5 5-5 5"/></svg>
</span>
</button>
<div class="effect-badge" id="effectBadge" hidden><i></i><span id="effectText"></span></div>
<div class="power-toast" id="powerToast"><span class="toast-star">✦</span><span id="toastText"></span></div>
<div class="end-overlay" id="endOverlay" hidden aria-live="polite">
<div class="end-card">
<div class="end-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><path d="m12 2.5 2.5 5.6 6.1.6-4.6 4.1 1.3 6-5.3-3.1-5.3 3.1 1.3-6-4.6-4.1 6.1-.6L12 2.5Z"/></svg>
</div>
<div class="end-eyebrow" id="endEyebrow">GAME OVER</div>
<h2 id="endTitle">Out of lives.</h2>
<p id="endCopy">That wall put up a fight. Ready for another go?</p>
<div class="final-score"><span>FINAL SCORE</span><strong id="finalScore">000000</strong></div>
<button class="play-again" id="playAgain" data-restart type="button">PLAY AGAIN
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a8 8 0 1 1 2.4 5.7"/><path d="M4 5v6h6"/></svg>
</button>
</div>
</div>
</div>
</section>
<aside class="guide" aria-label="Game guide">
<div class="guide-header">
<div class="guide-header-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 10.5v5M12 7.5h.01"/></svg>
</div>
<div><h2>THE GAME PLAN</h2><p>A quick little refresher</p></div>
<span class="guide-number">01</span>
</div>
<div class="guide-rule"></div>
<div class="guide-section guide-objective">
<div class="guide-label">THE MISSION <span>01</span></div>
<p class="mission-copy">Break every brick.<br><span>Keep the ball in play.</span></p>
</div>
<div class="guide-section">
<div class="guide-label">CONTROLS <span>02</span></div>
<div class="info-row">
<div class="info-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><rect x="6.5" y="3" width="11" height="18" rx="5.5"/><path d="M12 3v5"/></svg>
</div>
<div class="info-text"><strong>Mouse</strong><small>Move to steer</small></div>
</div>
<div class="info-row">
<div class="info-icon key-icon" aria-hidden="true"><kbd>←</kbd><kbd>→</kbd></div>
<div class="info-text"><strong>Arrow keys</strong><small>Move left & right</small></div>
</div>
</div>
<div class="guide-section">
<div class="guide-label">POWER-UPS <span>03</span></div>
<div class="info-row">
<div class="info-icon wide" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16M7 9l-3 3 3 3M17 9l3 3-3 3"/></svg>
</div>
<div class="info-text"><strong>Wider paddle</strong><small>Extra reach for 10 sec</small></div>
</div>
<div class="info-row">
<div class="info-icon multi" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="7" r="2.6"/><circle cx="7.5" cy="15.5" r="2.6"/><circle cx="16.5" cy="15.5" r="2.6"/></svg>
</div>
<div class="info-text"><strong>Multi-ball</strong><small>Three balls in play</small></div>
</div>
</div>
<div class="brick-tip">
<svg viewBox="0 0 42 27" aria-hidden="true">
<rect x="1" y="3" width="40" height="21" rx="4" fill="#858fbd" stroke="#b8c4e9"/>
<path d="M5 6h32" stroke="#d9e0ff" opacity=".45" stroke-linecap="round"/>
<circle cx="7" cy="10" r="1.2" fill="#3a4767"/><circle cx="11" cy="10" r="1.2" fill="#3a4767"/>
<path d="m23 4-4 6 4 3-3 5 3 5m-3-10-5 2" fill="none" stroke="#35415b" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div><strong>TOUGH BRICKS</strong><span>Two dots. Two hits.</span></div>
</div>
<div class="guide-foot"><span>ONE LEVEL. ALL SKILL.</span><span>✳</span></div>
</aside>
</main>
<footer><span>MADE FOR A LITTLE BREAK</span><span>PLAYFIELD ARCADE / 01</span></footer>
</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const byId = id => document.getElementById(id);
const ui = {
score: byId("score"),
hearts: Array.from(document.querySelectorAll("#hearts .heart")),
heartsContainer: byId("hearts"),
brickCount: byId("brickCount"),
brickTotal: byId("brickTotal"),
mode: byId("modeText"),
field: byId("field"),
prompt: byId("startPrompt"),
badge: byId("effectBadge"),
badgeText: byId("effectText"),
toast: byId("powerToast"),
toastText: byId("toastText"),
end: byId("endOverlay"),
endEyebrow: byId("endEyebrow"),
endTitle: byId("endTitle"),
endCopy: byId("endCopy"),
finalScore: byId("finalScore")
};
const W = 840;
const ROWS = 6;
const STEP = 1 / 120;
const palette = ["#fa8293", "#f7ab79", "#efcf78", "#74d5c4", "#79b7f2", "#ad9af1"];
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
const formatted = n => String(Math.floor(n)).padStart(6, "0");
let H = 500, columns = 10, brickHeight = 27, ballRadius = 7;
let normalWidth = 122, wideWidth = 186, baseSpeed = 340;
let mobile = false, pixelRatio = 1;
let state = "demo", bricks = [], balls = [], drops = [], particles = [], floaters = [];
let paddle, score = 0, lives = 3, remaining = 0, totalBricks = 0;
let wideTime = 0, serveTimer = 0, toastTimer = 0, clock = 0;
let pendingDemoReset = false, pointerX = W / 2, pointerControl = false, touchTracking = false;
let lastBadge = "";
const keys = { left: false, right: false };
function configureCanvas() {
pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(W * pixelRatio);
canvas.height = Math.round(H * pixelRatio);
ctx.setTransform(canvas.width / W, 0, 0, canvas.height / H, 0, 0);
}
function configureWorld() {
mobile = window.innerWidth <= 700;
H = mobile ? 620 : 500;
columns = mobile ? 8 : 10;
brickHeight = mobile ? 31 : 27;
ballRadius = mobile ? 9 : 7;
normalWidth = mobile ? 150 : 122;
wideWidth = mobile ? 222 : 186;
baseSpeed = mobile ? 380 : 340;
configureCanvas();
}
function blend(a, b, amount) {
const aa = parseInt(a.slice(1), 16);
const bb = parseInt(b.slice(1), 16);
const parts = [16, 8, 0].map(shift => {
const v = Math.round(((aa >> shift) & 255) * (1 - amount) + ((bb >> shift) & 255) * amount);
return v.toString(16).padStart(2, "0");
});
return "#" + parts.join("");
}
function roundedRect(x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function powerFor(row, col) {
if ((row === 0 && col === 2) ||
(row === 1 && col === 3) ||
(row === 3 && col === columns - 3) ||
(row === 5 && col === 2)) return "wide";
if ((row === 0 && col === columns - 2) ||
(row === 2 && col === 1) ||
(row === 4 && col === 4) ||
(row === 5 && col === Math.floor(columns * .65))) return "multi";
return null;
}
function buildBricks(isDemo) {
const margin = 38, gap = 8, top = 54;
const width = (W - margin * 2 - gap * (columns - 1)) / columns;
const result = [];
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < columns; col++) {
let tough = (row + col * 2) % 5 === 0;
if (row === 5 && col === Math.floor(columns * .65)) tough = false;
let hp = tough ? 2 : 1;
// The demo starts with one already-damaged brick to show the armor mechanic.
if (isDemo && row === 2 && col === 4) hp = 1;
const base = tough ? blend(palette[row], "#344253", .22) : palette[row];
result.push({
x: margin + col * (width + gap),
y: top + row * (brickHeight + 8),
w: width, h: brickHeight, row, col,
hp, maxHp: tough ? 2 : 1,
power: powerFor(row, col),
color: palette[row],
light: blend(base, "#ffffff", .17),
dark: blend(base, "#1b2634", .10),
flash: 0
});
}
}
return result;
}
function makeBall(x, y, vx, vy, tint = 0) {
return { x, y, vx, vy, r: ballRadius, tint, trail: [{ x, y }], trailTimer: 0 };
}
function launchBall() {
const horizontal = baseSpeed * .30 * (lives % 2 === 0 ? -1 : 1);
const vertical = -Math.sqrt(baseSpeed * baseSpeed - horizontal * horizontal);
balls.push(makeBall(paddle.x + paddle.w / 2, paddle.y - ballRadius - 1, horizontal, vertical));
}
function syncHud() {
ui.score.textContent = formatted(score);
ui.brickCount.textContent = remaining;
ui.brickTotal.textContent = totalBricks;
ui.hearts.forEach((heart, index) => heart.classList.toggle("lost", index >= lives));
ui.heartsContainer.setAttribute("aria-label", lives + (lives === 1 ? " life remaining" : " lives remaining"));
}
function syncBadge() {
let text = "";
let isWide = false;
if (wideTime > 0) {
text = "WIDE PADDLE · " + Math.ceil(wideTime) + "s";
isWide = true;
} else if (balls.length > 1) {
text = "MULTI-BALL · " + balls.length + " IN PLAY";
}
if (text !== lastBadge) {
ui.badge.hidden = !text;
ui.badgeText.textContent = text;
lastBadge = text;
}
ui.badge.classList.toggle("is-wide", isWide);
}
function showToast(text, kind) {
ui.toastText.textContent = text;
ui.toast.className = "power-toast show " + (kind || "");
toastTimer = 1.7;
}
function resetGame(mode) {
state = mode;
score = 0;
lives = 3;
wideTime = 0;
serveTimer = 0;
toastTimer = 0;
clock = 0;
pendingDemoReset = false;
pointerX = W / 2;
pointerControl = false;
touchTracking = false;
keys.left = false;
keys.right = false;
particles = [];
floaters = [];
drops = [];
balls = [];
paddle = { x: (W - normalWidth) / 2, y: H - 40, w: normalWidth, h: 14 };
bricks = buildBricks(mode === "demo");
totalBricks = bricks.length;
remaining = totalBricks;
if (mode === "demo") {
const target = bricks.find(b => b.row === 5 && b.col === Math.floor(columns * .65));
balls.push(makeBall(target.x + target.w / 2 - 12, H - 145, 46, -baseSpeed));
} else {
launchBall();
}
ui.prompt.hidden = mode !== "demo";
ui.end.hidden = true;
ui.field.classList.toggle("playing", mode === "playing");
ui.mode.textContent = mode === "demo" ? "DEMO IN PROGRESS" : "PLAYER IN CONTROL";
ui.toast.className = "power-toast";
lastBadge = "";
ui.badge.hidden = true;
syncHud();
syncBadge();
}
function finishGame(won) {
state = won ? "victory" : "gameover";
keys.left = keys.right = false;
ui.prompt.hidden = true;
ui.end.hidden = false;
ui.field.classList.remove("playing");
ui.endEyebrow.textContent = won ? "LEVEL COMPLETE" : "GAME OVER";
ui.endTitle.textContent = won ? "Wall cleared!" : "Out of lives.";
ui.endCopy.textContent = won ? "Every last brick. Nicely done." : "That wall put up a fight. Ready for another go?";
ui.finalScore.textContent = formatted(score);
ui.mode.textContent = won ? "LEVEL COMPLETE" : "GAME OVER";
}
function burst(x, y, color, count, energy = 1) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (35 + Math.random() * 150) * energy;
const life = .35 + Math.random() * .45;
particles.push({
x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 25 * energy,
size: 2 + Math.random() * 4, life, maxLife: life, color,
rotation: Math.random() * Math.PI, spin: (Math.random() - .5) * 8
});
}
if (particles.length > 260) particles.splice(0, particles.length - 260);
}
function addFloater(x, y, text, color) {
floaters.push({ x, y, text, color, life: .8, maxLife: .8 });
}
function hitBrick(brick, x, y) {
brick.hp--;
brick.flash = .16;
if (brick.hp > 0) {
score += 25;
burst(x, y, brick.color, 6, .55);
addFloater(x, y, "+25", "#e4eef5");
} else {
remaining--;
const points = brick.maxHp === 2 ? 175 : 100;
score += points;
burst(x, y, brick.color, 15, 1);
addFloater(brick.x + brick.w / 2, brick.y, "+" + points, "#edf7f5");
if (brick.power) {
drops.push({
x: brick.x + brick.w / 2,
y: brick.y + brick.h / 2,
type: brick.power,
speed: mobile ? 165 : 130,
age: 0
});
}
}
syncHud();
if (remaining === 0) {
if (state === "demo") pendingDemoReset = true;
else finishGame(true);
}
}
function changePaddleWidth(width) {
const center = paddle.x + paddle.w / 2;
paddle.w = width;
paddle.x = clamp(center - width / 2, 0, W - width);
}
function catchPower(drop) {
const color = drop.type === "wide" ? "#81e9db" : "#b9a4ff";
burst(drop.x, paddle.y, color, 17, .8);
score += 50;
syncHud();
if (drop.type === "wide") {
wideTime = 10;
changePaddleWidth(wideWidth);
showToast("WIDE PADDLE +10s", "wide");
} else {
if (balls.length === 0) {
balls.push(makeBall(paddle.x + paddle.w / 2, paddle.y - ballRadius - 1, 0, -baseSpeed));
serveTimer = 0;
}
const source = balls[0];
while (balls.length < 3) {
const angle = balls.length === 1 ? -.48 : .48;
const cosine = Math.cos(angle), sine = Math.sin(angle);
let vx = source.vx * cosine - source.vy * sine;
let vy = source.vx * sine + source.vy * cosine;
const speed = Math.hypot(vx, vy);
if (Math.abs(vy) < speed * .23) {
vy = (vy < 0 ? -1 : 1) * speed * .23;
vx = (vx < 0 ? -1 : 1) * Math.sqrt(speed * speed - vy * vy);
}
balls.push(makeBall(source.x, source.y, vx, vy, balls.length));
}
showToast("MULTI-BALL ×3", "multi");
}
syncBadge();
}
// Continuous circle-versus-rectangle collision, including rounded corner sweeps.
// This checks the entire path in each step rather than only the final position.
function sweepCircleRect(x, y, vx, vy, radius, rect, maxTime) {
const left = rect.x, right = rect.x + rect.w;
const top = rect.y, bottom = rect.y + rect.h;
const endX = x + vx * maxTime, endY = y + vy * maxTime;
if (Math.max(x, endX) + radius < left || Math.min(x, endX) - radius > right ||
Math.max(y, endY) + radius < top || Math.min(y, endY) - radius > bottom) return null;
let best = null;
function add(t, nx, ny, penetration = 0) {
if (t < -1e-7 || t > maxTime + 1e-7 || vx * nx + vy * ny >= -1e-7) return;
t = Math.max(0, t);
if (!best || t < best.t) best = { t, nx, ny, penetration };
}
const nearX = clamp(x, left, right), nearY = clamp(y, top, bottom);
const dx = x - nearX, dy = y - nearY;
const distanceSq = dx * dx + dy * dy;
if (distanceSq < radius * radius - 1e-6) {
if (distanceSq > 1e-9) {
const distance = Math.sqrt(distanceSq);
add(0, dx / distance, dy / distance, radius - distance);
} else {
const sides = [
{ d: x - left, nx: -1, ny: 0 },
{ d: right - x, nx: 1, ny: 0 },
{ d: y - top, nx: 0, ny: -1 },
{ d: bottom - y, nx: 0, ny: 1 }
];
sides.sort((a, b) => a.d - b.d);
add(0, sides[0].nx, sides[0].ny, radius + sides[0].d);
}
}
if (vx > 1e-8) {
const t = (left - radius - x) / vx, yy = y + vy * t;
if (yy >= top && yy <= bottom) add(t, -1, 0);
} else if (vx < -1e-8) {
const t = (right + radius - x) / vx, yy = y + vy * t;
if (yy >= top && yy <= bottom) add(t, 1, 0);
}
if (vy > 1e-8) {
const t = (top - radius - y) / vy, xx = x + vx * t;
if (xx >= left && xx <= right) add(t, 0, -1);
} else if (vy < -1e-8) {
const t = (bottom + radius - y) / vy, xx = x + vx * t;
if (xx >= left && xx <= right) add(t, 0, 1);
}
const a = vx * vx + vy * vy;
if (a > 1e-8) {
const corners = [
{ x: left, y: top, sx: -1, sy: -1 },
{ x: right, y: top, sx: 1, sy: -1 },
{ x: left, y: bottom, sx: -1, sy: 1 },
{ x: right, y: bottom, sx: 1, sy: 1 }
];
for (const corner of corners) {
const cx = x - corner.x, cy = y - corner.y;
const b = cx * vx + cy * vy;
const c = cx * cx + cy * cy - radius * radius;
const discriminant = b * b - a * c;
if (discriminant < 0) continue;
const t = (-b - Math.sqrt(discriminant)) / a;
if (t < -1e-7 || t > maxTime + 1e-7) continue;
const hitX = x + vx * t, hitY = y + vy * t;
const inX = corner.sx < 0 ? hitX <= left + 1e-6 : hitX >= right - 1e-6;
const inY = corner.sy < 0 ? hitY <= top + 1e-6 : hitY >= bottom - 1e-6;
if (inX && inY) add(t, (hitX - corner.x) / radius, (hitY - corner.y) / radius);
}
}
return best;
}
function moveBall(ball, dt, paddleStart, paddleVelocity) {
if (ball.y > H + ball.r) return false;
let timeLeft = dt, elapsed = 0, iterations = 0;
while (timeLeft > 1e-7 && iterations++ < 12) {
let hit = null;
function offer(candidate) {
if (!candidate || candidate.t < -1e-7 || candidate.t > timeLeft + 1e-7) return;
candidate.t = Math.max(0, candidate.t);
if (!hit || candidate.t < hit.t) hit = candidate;
}
if (ball.vx < 0) offer({ t: (ball.r - ball.x) / ball.vx, nx: 1, ny: 0, type: "wall" });
if (ball.vx > 0) offer({ t: (W - ball.r - ball.x) / ball.vx, nx: -1, ny: 0, type: "wall" });
if (ball.vy < 0) offer({ t: (ball.r - ball.y) / ball.vy, nx: 0, ny: 1, type: "wall" });
if (ball.vy > 0) offer({ t: (H + ball.r - ball.y) / ball.vy, type: "loss" });
for (const brick of bricks) {
if (brick.hp <= 0) continue;
const collision = sweepCircleRect(ball.x, ball.y, ball.vx, ball.vy, ball.r, brick, timeLeft);
if (collision) {
collision.type = "brick";
collision.brick = brick;
offer(collision);
}
}
if (ball.vy > 0 && ball.y <= paddle.y + paddle.h + ball.r) {
const paddleNow = paddleStart + paddleVelocity * elapsed;
const collision = sweepCircleRect(
ball.x - paddleNow, ball.y, ball.vx - paddleVelocity, ball.vy, ball.r,
{ x: 0, y: paddle.y, w: paddle.w, h: paddle.h }, timeLeft
);
if (collision) {
collision.type = "paddle";
offer(collision);
}
}
if (!hit) {
ball.x += ball.vx * timeLeft;
ball.y += ball.vy * timeLeft;
timeLeft = 0;
break;
}
ball.x += ball.vx * hit.t;
ball.y += ball.vy * hit.t;
elapsed += hit.t;
timeLeft -= hit.t;
if (hit.type === "loss") return false;
if (hit.type === "paddle") {
const paddleCenter = paddleStart + paddleVelocity * elapsed + paddle.w / 2;
const position = clamp((ball.x - paddleCenter) / (paddle.w / 2), -1, 1);
const angle = position * Math.PI * .38;
const speed = Math.min(baseSpeed * 1.42, Math.max(baseSpeed, Math.hypot(ball.vx, ball.vy) * 1.012));
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.cos(angle) * speed;
ball.y = Math.min(ball.y, paddle.y - ball.r - .04);
ball.x = clamp(ball.x, ball.r, W - ball.r);
burst(ball.x, paddle.y, "#a4f9ed", 4, .3);
} else {
if (hit.type === "brick") {
hitBrick(hit.brick, ball.x - hit.nx * ball.r, ball.y - hit.ny * ball.r);
if (pendingDemoReset || (state !== "demo" && state !== "playing")) return true;
}
const dot = ball.vx * hit.nx + ball.vy * hit.ny;
if (dot < 0) {
ball.vx -= 2 * dot * hit.nx;
ball.vy -= 2 * dot * hit.ny;
}
const push = (hit.penetration || 0) + .025;
ball.x += hit.nx * push;
ball.y += hit.ny * push;
}
if (hit.t < 1e-7) {
const tiny = Math.min(timeLeft, .00001);
timeLeft -= tiny;
elapsed += tiny;
}
}
ball.trailTimer += dt;
if (ball.trailTimer >= .018) {
ball.trailTimer = 0;
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > 10) ball.trail.shift();
}
return true;
}
function reflectedX(x, radius) {
const span = W - radius * 2, period = span * 2;
let n = ((x - radius) % period + period) % period;
if (n > span) n = period - n;
return radius + n;
}
function demoTarget() {
let incoming = null, soonest = Infinity, intercept = W / 2;
for (const ball of balls) {
if (ball.vy <= 0 || ball.y > paddle.y - ball.r + 1) continue;
const t = (paddle.y - ball.r - ball.y) / ball.vy;
if (t >= 0 && t < soonest) {
soonest = t;
incoming = ball;
intercept = reflectedX(ball.x + ball.vx * t, ball.r);
}
}
const aimStrength = clamp((clock - 2.7) / 4, 0, 1) * paddle.w * .22;
let target = incoming ? intercept - Math.sin(clock * .85) * aimStrength :
(balls.length ? balls[0].x : paddle.x + paddle.w / 2);
const nearbyDrop = drops.find(drop => drop.y > paddle.y - 135);
if (nearbyDrop) {
if (!incoming || soonest > .9) target = nearbyDrop.x;
else if (Math.abs(intercept - nearbyDrop.x) < paddle.w - 18) {
target = (intercept + nearbyDrop.x) / 2;
}
}
return clamp(target, paddle.w / 2, W - paddle.w / 2);
}
function updateDrops(dt, paddleStart, paddleVelocity) {
const kept = [];
for (const drop of drops) {
const half = 13;
const timeToPaddle = (paddle.y - (drop.y + half)) / drop.speed;
if (timeToPaddle >= 0 && timeToPaddle <= dt) {
const paddleAtCatch = paddleStart + paddleVelocity * timeToPaddle;
if (drop.x + half >= paddleAtCatch && drop.x - half <= paddleAtCatch + paddle.w) {
catchPower(drop);
continue;
}
}
drop.y += drop.speed * dt;
drop.age += dt;
if (drop.y - half <= H + 4) kept.push(drop);
}
drops = kept;
}
function updateEffects(dt) {
for (const brick of bricks) brick.flash = Math.max(0, brick.flash - dt);
for (const p of particles) {
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 150 * dt;
p.vx *= .992;
p.rotation += p.spin * dt;
p.life -= dt;
}
particles = particles.filter(p => p.life > 0);
for (const f of floaters) {
f.y -= 20 * dt;
f.life -= dt;
}
floaters = floaters.filter(f => f.life > 0);
if (toastTimer > 0) {
toastTimer -= dt;
if (toastTimer <= 0) ui.toast.classList.remove("show");
}
}
function update(dt) {
if (state !== "demo" && state !== "playing") return;
const activeState = state;
clock += dt;
if (wideTime > 0) {
wideTime = Math.max(0, wideTime - dt);
if (wideTime === 0) changePaddleWidth(normalWidth);
}
if (serveTimer > 0) {
serveTimer -= dt;
if (serveTimer <= 0) {
serveTimer = 0;
launchBall();
}
}
const paddleStart = paddle.x;
if (state === "demo") {
const desired = demoTarget() - paddle.w / 2;
paddle.x += clamp(desired - paddle.x, -900 * dt, 900 * dt);
} else {
const direction = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
if (direction !== 0) {
paddle.x += direction * 650 * dt;
pointerX = paddle.x + paddle.w / 2;
pointerControl = false;
} else if (pointerControl) {
const desired = pointerX - paddle.w / 2;
paddle.x += clamp(desired - paddle.x, -2400 * dt, 2400 * dt);
}
}
paddle.x = clamp(paddle.x, 0, W - paddle.w);
const paddleVelocity = (paddle.x - paddleStart) / dt;
const hadBalls = balls.length > 0;
const survivors = [];
for (const ball of balls) {
if (moveBall(ball, dt, paddleStart, paddleVelocity)) survivors.push(ball);
if (pendingDemoReset || state !== activeState) break;
}
balls = survivors;
if (pendingDemoReset) {
resetGame("demo");
return;
}
if (state !== activeState) return;
updateDrops(dt, paddleStart, paddleVelocity);
if (hadBalls && balls.length === 0) {
lives--;
syncHud();
if (lives <= 0) {
if (state === "demo") {
resetGame("demo");
return;
}
finishGame(false);
return;
}
serveTimer = .7;
showToast(lives === 1 ? "1 LIFE LEFT" : lives + " LIVES LEFT", "danger");
}
updateEffects(dt);
syncBadge();
}
function drawBackground() {
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
ctx.setLineDash([]);
const background = ctx.createLinearGradient(0, 0, 0, H);
background.addColorStop(0, "#111e2c");
background.addColorStop(.54, "#0c1725");
background.addColorStop(1, "#0a1320");
ctx.fillStyle = background;
ctx.fillRect(0, 0, W, H);
const glow = ctx.createRadialGradient(W * .5, 30, 10, W * .5, 130, W * .62);
glow.addColorStop(0, "rgba(74, 125, 135, .14)");
glow.addColorStop(1, "rgba(30, 50, 72, 0)");
ctx.fillStyle = glow;
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "rgba(154, 188, 204, .045)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x <= W; x += 42) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
for (let y = 0; y <= H; y += 42) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
ctx.stroke();
ctx.fillStyle = "rgba(160, 196, 205, .14)";
for (let i = 0; i < 27; i++) {
const x = (i * 149 + 73) % W;
const y = 285 + ((i * 67 + 19) % Math.max(1, H - 300));
ctx.fillRect(x, y, 2, 2);
}
ctx.setLineDash([3, 8]);
ctx.strokeStyle = "rgba(144, 195, 197, .12)";
ctx.beginPath();
ctx.moveTo(W / 2, Math.max(290, H * .6));
ctx.lineTo(W / 2, H - 65);
ctx.stroke();
ctx.setLineDash([]);
ctx.font = "700 10px ui-monospace, SFMono-Regular, Menlo, monospace";
ctx.fillStyle = "#708899";
ctx.textAlign = "left";
ctx.fillText("01 / THE BRICK FIELD", 38, 29);
ctx.textAlign = "right";
ctx.fillText("CLEAR THEM ALL", W - 38, 29);
ctx.strokeStyle = "rgba(151, 186, 199, .12)";
ctx.beginPath();
ctx.moveTo(38, 38);
ctx.lineTo(W - 38, 38);
ctx.stroke();
ctx.font = "700 9px ui-monospace, SFMono-Regular, Menlo, monospace";
ctx.fillStyle = "rgba(142, 166, 180, .48)";
ctx.textAlign = "left";
ctx.fillText("← MOVE →", 23, H - 12);
ctx.textAlign = "right";
ctx.fillText("DON'T DROP IT", W - 23, H - 12);
}
function drawSymbol(type, x, y, scale, color) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 1.7;
ctx.lineCap = "round";
ctx.lineJoin = "round";
if (type === "wide") {
ctx.beginPath();
ctx.moveTo(-7, 0); ctx.lineTo(7, 0);
ctx.moveTo(-4, -3); ctx.lineTo(-7, 0); ctx.lineTo(-4, 3);
ctx.moveTo(4, -3); ctx.lineTo(7, 0); ctx.lineTo(4, 3);
ctx.stroke();
} else {
ctx.beginPath();
ctx.arc(0, -3.5, 2.25, 0, Math.PI * 2);
ctx.arc(-4.1, 3.2, 2.25, 0, Math.PI * 2);
ctx.arc(4.1, 3.2, 2.25, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawBrick(brick) {
if (brick.hp <= 0) return;
ctx.save();
ctx.shadowColor = brick.color;
ctx.shadowBlur = 6;
const gradient = ctx.createLinearGradient(0, brick.y, 0, brick.y + brick.h);
gradient.addColorStop(0, brick.light);
gradient.addColorStop(1, brick.dark);
roundedRect(brick.x, brick.y, brick.w, brick.h, 5);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(255,255,255,.25)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "rgba(255,255,255,.25)";
roundedRect(brick.x + 5, brick.y + 3, brick.w - 10, 2, 1);
ctx.fill();
ctx.fillStyle = "rgba(20,30,44,.12)";
roundedRect(brick.x + 5, brick.y + brick.h - 4, brick.w - 10, 1.5, .7);
ctx.fill();
if (brick.maxHp === 2) {
ctx.fillStyle = "rgba(20,31,48,.47)";
ctx.beginPath();
ctx.arc(brick.x + 7, brick.y + 8, 1.6, 0, Math.PI * 2);
ctx.arc(brick.x + 12, brick.y + 8, 1.6, 0, Math.PI * 2);
ctx.fill();
}
if (brick.power) {
const cx = brick.x + brick.w / 2, cy = brick.y + brick.h / 2 + .5;
ctx.fillStyle = "rgba(16,29,43,.32)";
ctx.beginPath();
ctx.arc(cx, cy, 9.5, 0, Math.PI * 2);
ctx.fill();
drawSymbol(brick.power, cx, cy, .83, "#f5fffd");
}
if (brick.maxHp === 2 && brick.hp === 1) {
const x = brick.x + brick.w * .52, y = brick.y;
ctx.strokeStyle = "rgba(27,36,51,.88)";
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(x + 3, y + 2);
ctx.lineTo(x - 3, y + 9);
ctx.lineTo(x + 2, y + 12);
ctx.lineTo(x - 2, y + 18);
ctx.lineTo(x + 3, y + brick.h - 2);
ctx.moveTo(x - 2, y + 12);
ctx.lineTo(x - 8, y + 15);
ctx.stroke();
}
if (brick.flash > 0) {
roundedRect(brick.x, brick.y, brick.w, brick.h, 5);
ctx.fillStyle = "rgba(255,255,255," + Math.min(.5, brick.flash * 2.5) + ")";
ctx.fill();
}
ctx.restore();
}
function drawDrop(drop) {
const color = drop.type === "wide" ? "#8cefe0" : "#c8b7ff";
ctx.save();
ctx.strokeStyle = drop.type === "wide" ? "rgba(140,239,224,.3)" : "rgba(200,183,255,.3)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(drop.x, drop.y - 24);
ctx.lineTo(drop.x, drop.y - 15);
ctx.stroke();
ctx.translate(drop.x, drop.y);
ctx.rotate(Math.sin(drop.age * 5) * .1);
ctx.shadowColor = color;
ctx.shadowBlur = 16;
roundedRect(-13, -13, 26, 26, 6);
ctx.fillStyle = "#1b2938";
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.stroke();
drawSymbol(drop.type, 0, 0, 1.05, color);
ctx.restore();
}
function drawParticles() {
for (const p of particles) {
ctx.save();
ctx.globalAlpha = clamp(p.life / p.maxLife, 0, 1);
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * .7);
ctx.restore();
}
ctx.save();
ctx.textAlign = "center";
ctx.font = "800 11px ui-monospace, SFMono-Regular, Menlo, monospace";
for (const f of floaters) {
ctx.globalAlpha = clamp(f.life / f.maxLife, 0, 1);
ctx.fillStyle = f.color;
ctx.fillText(f.text, f.x, f.y);
}
ctx.restore();
}
function drawBall(ball) {
ctx.save();
const trailColor = ball.tint === 1 ? "190,170,255" : ball.tint === 2 ? "154,195,255" : "134,247,230";
ctx.lineCap = "round";
for (let i = 1; i < ball.trail.length; i++) {
const alpha = .05 + (i / ball.trail.length) * .34;
ctx.strokeStyle = "rgba(" + trailColor + "," + alpha + ")";
ctx.lineWidth = 1 + (i / ball.trail.length) * 4;
ctx.beginPath();
ctx.moveTo(ball.trail[i - 1].x, ball.trail[i - 1].y);
ctx.lineTo(ball.trail[i].x, ball.trail[i].y);
ctx.stroke();
}
if (ball.trail.length) {
const last = ball.trail[ball.trail.length - 1];
ctx.strokeStyle = "rgba(" + trailColor + ",.42)";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(last.x, last.y);
ctx.lineTo(ball.x, ball.y);
ctx.stroke();
}
ctx.shadowColor = "rgb(" + trailColor + ")";
ctx.shadowBlur = 18;
ctx.fillStyle = ball.tint === 1 ? "#e6dcff" : ball.tint === 2 ? "#daeaff" : "#d4fff8";
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(ball.x - ball.r * .2, ball.y - ball.r * .2, ball.r * .53, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawPaddle() {
ctx.save();
ctx.shadowColor = "#6be8d6";
ctx.shadowBlur = 19;
const gradient = ctx.createLinearGradient(0, paddle.y, 0, paddle.y + paddle.h);
gradient.addColorStop(0, "#c6fff6");
gradient.addColorStop(.4, "#82efdf");
gradient.addColorStop(1, "#40bdaf");
roundedRect(paddle.x, paddle.y, paddle.w, paddle.h, 7);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(229,255,251,.7)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "rgba(19,99,96,.32)";
for (let i = -1; i <= 1; i++) {
roundedRect(paddle.x + paddle.w / 2 + i * 7 - 1, paddle.y + 4, 2, 6, 1);
ctx.fill();
}
ctx.restore();
}
function render() {
drawBackground();
for (const brick of bricks) drawBrick(brick);
for (const drop of drops) drawDrop(drop);
drawParticles();
for (const ball of balls) drawBall(ball);
if (serveTimer > 0 && balls.length === 0) {
drawBall({ x: paddle.x + paddle.w / 2, y: paddle.y - ballRadius - 1, r: ballRadius, tint: 0, trail: [] });
}
drawPaddle();
}
function setPointer(clientX) {
const rect = canvas.getBoundingClientRect();
if (!rect.width) return;
pointerX = clamp((clientX - rect.left) / rect.width * W, 0, W);
pointerControl = true;
}
document.addEventListener("pointerdown", event => {
const target = event.target;
if (target && typeof target.closest === "function" && target.closest("[data-restart]")) return;
if (state === "demo") resetGame("playing");
if (state === "playing" && target && typeof target.closest === "function" && target.closest(".canvas-wrap")) {
touchTracking = true;
setPointer(event.clientX);
}
});
window.addEventListener("pointermove", event => {
if (state !== "playing") return;
if (event.pointerType === "touch" && !touchTracking) return;
setPointer(event.clientX);
});
window.addEventListener("pointerup", () => { touchTracking = false; });
window.addEventListener("pointercancel", () => { touchTracking = false; });
document.addEventListener("keydown", event => {
const arrow = event.key === "ArrowLeft" || event.key === "ArrowRight";
if (arrow) event.preventDefault();
if (state === "demo") {
resetGame("playing");
if (event.key === " ") event.preventDefault();
} else if (state === "gameover" || state === "victory") {
if (event.key === "Enter" || event.key === " " || event.key.toLowerCase() === "r") {
event.preventDefault();
resetGame("playing");
}
return;
}
if (state === "playing") {
if (event.key === "ArrowLeft") keys.left = true;
if (event.key === "ArrowRight") keys.right = true;
}
});
document.addEventListener("keyup", event => {
if (event.key === "ArrowLeft") keys.left = false;
if (event.key === "ArrowRight") keys.right = false;
});
window.addEventListener("blur", () => { keys.left = false; keys.right = false; touchTracking = false; });
ui.prompt.addEventListener("click", () => { if (state === "demo") resetGame("playing"); });
byId("restartTop").addEventListener("click", () => resetGame("playing"));
byId("playAgain").addEventListener("click", () => resetGame("playing"));
configureWorld();
resetGame("demo");
render();
window.addEventListener("resize", () => {
const newMobile = window.innerWidth <= 700;
const newRatio = Math.min(window.devicePixelRatio || 1, 2);
if (newMobile !== mobile) {
const mode = state === "demo" ? "demo" : "playing";
configureWorld();
resetGame(mode);
} else if (newRatio !== pixelRatio) {
configureCanvas();
}
});
let previous = performance.now();
let accumulator = 0;
function frame(now) {
const delta = clamp((now - previous) / 1000, 0, .05);
previous = now;
accumulator += delta;
while (accumulator >= STEP) {
update(STEP);
accumulator -= STEP;
}
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (46,792 tokens)
**Planning game development**
I need to create a complete HTML canvas with inline CSS and JS for a game. It should be polished and visually appealing, starting with a "Click or press a key to start" overlay. The game needs to be responsive down to 375 pixels without scrolling, and I'll prepare screenshots at 1280x800. I'll implement things like continuous collision detection and multi-ball features. Additionally, I want to establish a compelling visual theme with navy accents and modern UI elements.**Designing game layout**
I'm considering creating a page layout with a main game board on the left (the canvas) and an information column on the right. I want the screenshot to capture gameplay well. The background could be a dark navy color with a subtle grid or glow. The logo would read "A / ARCADE" and the game label "01 / BREAKOUT" along with a single-player indicator. The main section could feature bold headings and level indicators, while ensuring ample space for the gameplay area and sidebar instructions.**Structuring game interface**
I’m planning to display the game bricks in the canvas with around 12 columns and 7 rows at the top 250 pixels area, placing the paddle at the bottom. I want to make sure I have at least 6 distinct rows. The sidebar will indicate "FIRST / ONLY LEVEL." Above the canvas, there will be space for the score and lives, with an eye-catching header showing things like "Score 000000" and hearts for lives. The right sidebar will contain controls and power-up visuals, along with a "HOW TO PLAY" section that avoids using giant text.**Designing game start overlay**
I’m thinking about placing an overlay card near the bottom or centered in the lower half of the canvas, while the game demo is running. I don’t want it to obscure too many bricks or the ball. The overlay should be semi-translucent and feature a pulsing dot with a "READY TO PLAY?" title and a message saying "Click or press a key to start." I want to ensure that click events remain active even when the pointer is over the card or canvas. It's important to handle clicks carefully to ensure the game resets correctly when started.**Designing game end overlay**
I’m focused on creating a game end overlay that has a "PLAY AGAIN" button centered on it. I want the canvas to have functional design, with logical dimensions around 800 width and 540 height, while also considering devicePixelRatio for mobile adaptability. Visual quality is key, so I’ll use a gradient background with a subtle grid and some decorative elements like flight lines. The bricks need to look polished with pastel colors, and I want to add clear distinctions like hatches or cracks to show damage.**Planning brick layout**
I'm considering using 9 columns with wider 75px bricks, and then placing icons on some of them. The design would have 9 columns and 6 rows, making a total of 54 bricks, or potentially 7 columns if I go for 10 rows. Given a width of 800, each brick would be around 74px, and they'd all need to be visible. I want to make sure the visuals are vivid and eye-catching, aiming for around 20% toughness in the design.**Designing arcade interface**
I’m thinking about the product title "BREAK / OUT" and how to structure the arcade interface. A coherent layout is important! For the upper title header, I’m considering using a max width of 1160 for the masthead, centered with margin auto. I’ll add some padding on top, around 25, and display elements like the logo symbol and "ARCADE" in a smaller font. This keeps everything organized and visually appealing!**Planning canvas dimensions**
I'm working on a canvas with dimensions 840x500 and thinking about the layout. I'll add a border at the top, and I want to include a section title like "DESTROY ALL BRICKS" with a few small bars. It might be nice to draw a grid background and some labels, such as "◉ 01 / TARGETS" in the top-left.
I’ll sketch the bricks from 42 to 252, position the ball at around x420, y390, and the paddle at y468. I’m also considering an overlay situated around y330, where the ball should still be partially visible. After 3 seconds, I’ll take a screenshot, probably catching the ball somewhere in the frame.**Clarifying design details**
The user wants something behind a "Click or press a key to start" message. I’m considering an overlay at the bottom of the screen that doesn’t obstruct things. Maybe a pill overlay around 69% on the Y-axis and a small "AUTOPLAY IS ON" text above it, clickable, with a slight shadow. I want to create a clear demo, adding a badge like "● DEMO MODE" in the top left and using a dark design board with a radial gradient background. I'll outline the bricks and gaps to ensure the layout looks good.**Planning the brick layout**
I'm considering a layout of 10 columns and 6 rows for the bricks, with each brick being 70 units wide and 25 units high. I’ll use a colorful palette: salmon pink, coral, golden, turquoise, blue, and purple for each row. But I'm aiming for a visually richer uniform rainbow effect across the breakout bricks. The board should highlight its full structure, perhaps using gradients for shine and shadows on the rows. I’ll implement a rounded rectangle helper for modern browsers and focus on bright, attractive colors with dark outlines for visibility.**Considering color mixing for brightness**
I need to derive the hex colors for the brick break effect, making sure to mix them effectively. For the glow, I think a subtle shadow with a blur effect would enhance the visual appeal. I want to ensure the colors blend well to create that glowing look, so it’s balancing the mix to keep it vibrant yet soft. I’ll experiment with different shades to find the perfect blend for the bricks and their glow!**Analyzing paddle collision**
I'm looking into the sides of paddle collision to ensure that if the ball overlaps, I only choose the top side. This helps in avoiding any weird direction changes. It’s important that the ball never passes through the paddle when moving downwards. The ball's velocity will change to move upwards depending on the contact point. I'll compute the intersection ray, comparing the ball's position and paddle's dimensions carefully to maintain accurate collision detection.**Calculating physics for game elements**
I need to compute the ball's world position while updating paddle movement for each physics tick. I'll calculate the start and end positions for each substep, and ensure the ball uses a moving Axis-Aligned Bounding Box (AABB) for more accurate collision detection. Managing the difficulty setting is crucial. I might not need a moving target, ideally using an expanded AABB for better handling. I should implement continuous rect calculations for the paddle's static position, including bounce angles and collision detection.**Considering demo ball mechanics**
I'm focusing on the demo ball's velocity, aiming for it to break bricks and to automatically restart on victory or loss. The demo will incorporate AI, and players won't have a score until they click. Importantly, I want to avoid any overlay that interrupts the game.
Regarding AI, I'll need to select the target ball based on its downward movement and calculate various parameters like time to the paddle and future positions using some math on its coordinates and velocity.**Considering panel design**
I'm thinking about designing a support card with specific instructions. The side height might need to be around 570 to ensure everything is legible. I want to focus on making the content clear and easy to read, especially the header styles. I’m also considering adding a "TIPS" bar at the bottom for extra guidance. It’s all about creating an accessible and user-friendly interface that provides helpful information right at hand!**Considering demo performance**
I need to keep an eye on how the demo resets quickly after a victory, which might be an issue if it ends altogether.
For the particle updates, I’m thinking about setting properties like gravity, lifetime, and colors, creating small rectangular sparks.
For balls with trails, I’ll record each frame instead of fixed steps to better represent movement and draw glowing trails in light blue.
Regarding multi-ball features, I need to ensure the correct number of balls spawns based on the current count while maintaining stable direction.**Deciding ball spawning**
I’m considering how to handle ball spawning when catching. I should pick an existing ball and clone it at the same x and y position, giving it a slight rotation of ±25° based on its velocity.
I also need to be mindful to avoid having spawned balls overlap with bricks exactly at that point. It’s tricky, but ensuring a smooth gameplay experience is key! I’ll need to think through this carefully to avoid any potential issues.**Designing Screen Effects**
I’m creating a canvas with a deep gradient background, transitioning from #101927 to #090f1c, and adding a radial glow at the top. I plan to overlay a subtle dot grid and include small text at the top saying "TARGETS / 60". In the top-right corner, I'll show "01 — FIELD". I want to draw on the canvas as well, with "DESTROY THE BRICKS" in small text at the top, along with "TARGET FIELD" and "⬢ 60 BRICKS" in the header area.**Evaluating canvas dimensions**
I'm thinking about the playable area, ideally making it good for mobile at 375px wide. If I set W to 840 and H to 640, that's a 1.31 aspect ratio. But during runtime, I want it to adapt properly based on the CSS width. If the width is 600 or less, I'll use W of 840 and H of 680. Also, considering whether I should use 8 columns or stick to 10, since the user has specified 6 rows.**Adjusting canvas dimensions**
I'm focusing on setting the height to 650 for mobile, with a paddle width of 150, making it more accessible. I need to ensure the HUD adapts well. For orientation changes or window resizing, I'll handle it in the `resize` event listener to adjust dimensions dynamically. It’s crucial to set the canvas width and height dynamically in the HTML to ensure proper rendering. I’m also considering using 10 columns for desktop and 8 for mobile, with unique icon placements in the bottom row.**Adjusting power pattern for columns**
I'm thinking about the power pattern. For 10 columns, I have it hard-coded, but for 8 columns, I'll need to adjust the bottom column using `floor(0.65 * cols)` and set the initial ball's x position accordingly. It's essential that I adapt these values based on whether we're in mobile mode or not. I'll define the `const cols` first, depending on that mobile condition. This should help ensure everything aligns correctly.**Designing game elements**
I’m detailing a linear gradient fill with adjusted hex values and a subtle stroke for a light shine effect. If the challenge is high, I’ll add small white markers. For health mechanics, I’ll create visual cues like cracks for specific HP scenarios. Also, I should ensure that the damage appears visually when the ball hits the bricks. It’s essential to communicate the toughness of bricks in the initial screenshots, perhaps showing one already cracked. Adapting the gameplay experience is important here!**Setting up demo parameters**
I'm thinking about how to set up an initial demo with the ball hitting a tough target first and then using power shots. I want to ensure clarity with a cracked target visible at the start. For the demo, I’ll check if it's a demo mode and adjust HP to 1 for the tough target. I'll also start the score at zero and update it throughout the demo with a clear HUD showing the score and remaining bricks.**Considering coding for performance**
I’m focusing on using robust continuous physics in my code to avoid errors, and there are no external requests. I need to think about the `CanvasRenderingContext2D` shadow gradient and `roundRect` compatibility. The latest Chrome supports these, but I want consistency with helper paths. Ensuring no performance issues is important, especially with device pixel ratios up to 2, or even higher on mobile. I plan to use `requestAnimationFrame` with a fixed delta time of 1/120 and a catch-up limit of 5. Debugging mentally is essential since I can’t run the tool directly.**Evaluating UI badge design**
I'm considering how to implement the badge for a wide timer. It might be best to position it at the top right or bottom right of the screen. I could show a small label in the HUD, like "WIDE 10s". It’s crucial to display feedback in a way that remains clear and meaningful, especially during multi-ball scenarios to avoid having too many updates on the DOM at once. I'll think about using textContent updates for efficiency.**Identifying expiration bug**
I need to address the `wideTimer` expiration bug that results from applying power in `updateDrops` after the balls have moved. This affects the paddle width and changes the starting position for the next tick. In the `updateDrops(dt)` function, I'll ensure that the drops are correctly detected for collision with the paddle. The logic needs to check if a drop crosses the paddle's bottom and verify the positions accurately to prevent through-paddle issues. I'll draw a custom icon with a size of 23 as part of the design.**Analyzing ball physics**
I'm thinking about how a ball might overlap if it's moving outward without bouncing — it could actually be physically inside a collision. For my target, I would need to ensure it doesn't pass through the closest face, which seems logical if it's deep enough or moving fast enough.
After some calculations, I’ll update the ball’s position based on time and velocity. When dealing with bricks, I need to reflect the ball and adjust its position if there's penetration. This involves various conditions depending on whether it interacts with a paddle or walls.**Implementing AI Targeting Logic**
I'm working on a strategy for an AI that needs to intercept a ball effectively. First, I need to predict where the ball will land based on its speed and distance from the paddle. If the ball will reach the paddle in less than 0.8 seconds and is nearby, I should aim for a midpoint to adjust the bounce angle.
When the ball is farther away, I should prioritize positioning myself to intercept it without losing track of it. I'm also considering how to adjust angles dynamically to enhance gameplay without losing consistent performance over time.**Targeting the Ball's Drop Position**
I'm focusing on how to set my target for intercepting the ball effectively when it's coming down. If the drop is close, I want to check if the ball is incoming or if its travel time is significant. I’ll adjust my target based on the ball’s position and paddle width.
I figured out that I can catch the ball at various points, which creates additional copies that move toward the bricks nearby. The icons indicate the different powers available, and I'm considering adding a few more for variety. Overall, it feels like a solid approach!**Implementing canvas labels**
I’m working on the canvas labels using `ctx.font='600 10px ...'` for text like `ctx.fillText('BRICK FIELD', 38,29)`. On the right, I’ll add `ctx.fillText('01 / 01', 802,29)`, which substitutes that area nicely. I also want to include a dynamic element that says "60 BRICKS TO GO" on the right, right next to the scoreboard that’s already set up. This approach should provide a clear and functional layout.**Planning layout details**
I'm outlining the layout for a page with a dark background and a centered design. The top bar is at x70 with a cyan logo. I might use "Breakout" as the header text. The game frame will be 860x575, with a sidebar of 250. I’m considering six colored stripes with ten blocks each. The canvas heights need adjustments, especially for the footer and header, which will help finalize the overall structure. I feel like I'm making good progress!**Testing paddle velocity logic**
I’m reflecting on the importance of `paddleVel` in relation to instantaneous pointer movements, especially when teleporting 400 pixels per update. I need to ensure that the collision detection is robust, particularly with high velocities and small deltas. If the ball’s position is inside the rectangular boundary, I’ll convert its position and velocities correctly after a hit, using the paddle's center to calculate the angle. I should also manage the player pointer’s cap to maintain responsiveness while avoiding erratic movements.**Analyzing brick collision logic**
When bricks collide, I need to check if the brick is alive first; if not, I continue. After a hit, I should emit particles at the contact point, which is calculated relative to the ball's position. If the ball hits from the bottom, I can determine the contact point accurately. To prevent the ball from getting stuck at zero angle after a reflection, I’ll need to apply a check for horizontal collisions. I should also ensure emitted clones maintain appropriate vertical speed.**Analyzing event handling for canvas interactions**
I’m starting with `touchTracking=false` and establishing pointer movement only when touched inside the canvas. The pointerup event is set to reset things. I’ll also use `touch-action:none` on the `.canvas-wrap` element to allow page scrolling outside. In the keydown section, I’m checking for arrow key presses and supporting game state changes like resetting if the game is over or victorious. I’ll also handle resizing for mobile or desktop setups. It looks like a clear plan!**Refining game initialization and animation**
I need to make sure the `resize` callback is added after the initial configuration or reset; otherwise, it can throw off the racing functionality.
In the frame function, I’m keeping track of the previous performance time and using an accumulator to maintain time between updates. I’ll also ensure that repeated resets do not affect the accumulator. Plus, there’s an important note about automating a three-second demo mode with a simple AI that runs behind a prompt saying "Click or press a key to start." So, making those adjustments will be essential!**Evaluating game design elements**
I’m thinking about using generic `.start-prompt` and `<span>` for CSS sizing. There’s a concern about flickering due to the outer div rounding. The ball speed at 340 px/sec with 60 bricks should make gameplay last around 100 seconds. It’s important to have all the interesting elements visible early, like pre-crack and power icons. Users can win by clearing the level. I’ll ensure they can access power-ups like a wider paddle or multi-ball effectively.**Exploring code optimizations**
I'm noticing a subtle bug with `paddleVel` in relation to the collision when calculating `paddleStart` versus the actual `paddle.x`. I also see `changePaddleWidth` is fine inside `updateDrops` after the ball.
I plan to implement `sweepCircleRect` for collision detection, using default undefined for `penetration`. It looks like I need to finalize code in one code fence, ensuring all styles and IDs are correct. The output is projected at around 22k characters, which fits within our token limit, but we'll need to manage carefully. Plus, no emojis, just Unicode for arrows!**Addressing color blending issues**
I’m working with `parseInt(mix target)` in JavaScript, particularly when the base is an RGB string. The function `mixColor` is currently returning `rgb(...)`, which complicates things when trying to blend hex colors again.
To solve this, I want to ensure that `blend(a, b, t)` always returns a hex output. I think I can adjust the blend function to parse the hex color correctly. Also, I'm planning to implement a new gradient for each brick, and everything related to `ctx.lineDashOffset` looks good!