docs(01-foundation): research phase domain
Investigate HTML5 Canvas game loop, HiDPI rendering, keyboard input handling, ball physics, and collision detection patterns for Phase 1 Foundation. Document standard stack (vanilla JS + requestAnimationFrame + delta-time physics), architecture patterns (game loop, HiDPI scaling, responsive canvas, input state tracking, AABB collision, physics updates), common pitfalls (tunneling, frame-rate dependency, blur on HiDPI, input lag, aspect ratio bugs, floating point drift), and validation approach (manual visual testing on Retina/120Hz displays). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
732
.planning/phases/01-foundation/01-RESEARCH.md
Normal file
732
.planning/phases/01-foundation/01-RESEARCH.md
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
# Phase 1: Foundation - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-03-10
|
||||||
|
**Domain:** HTML5 Canvas game loop, HiDPI rendering, keyboard input, ball physics, collision detection
|
||||||
|
**Confidence:** HIGH
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Phase 1 Foundation for Super Pong establishes core technical infrastructure: a responsive game loop with proper timing, sharp HiDPI-aware canvas rendering, responsive keyboard input for paddle control, and physics simulation for ball movement with zone-based paddle deflection. The phase requires solving three critical problems: (1) maintaining smooth frame-rate-independent physics despite variable refresh rates, (2) rendering crisp graphics on high-DPI displays without performance degradation, and (3) minimizing input lag for responsive paddle control. Vanilla JavaScript is the standard approach—no frameworks, single HTML file, module-pattern organization.
|
||||||
|
|
||||||
|
**Primary recommendation:** Use `requestAnimationFrame()` with delta-time physics (frame-rate independent movement), implement HiDPI canvas scaling via `devicePixelRatio`, track keyboard state with boolean flags updated on keydown/keyup events, and use simple AABB collision detection for wall/paddle bouncing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
## User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- Single HTML file with everything embedded (JS + CSS in one `<script>` tag)
|
||||||
|
- JavaScript organized into module pattern: `GameLoop`, `Physics`, `Renderer`, `Input` objects within a single `<script>` tag
|
||||||
|
- No build tooling; runs directly from the filesystem
|
||||||
|
- Full-window canvas that fills the entire browser window
|
||||||
|
- Canvas resizes immediately on window resize
|
||||||
|
- Maintains aspect ratio during resize; minimum 4:3 aspect ratio enforced
|
||||||
|
- No maximum size cap — grows unbounded to fill any screen
|
||||||
|
- Must be HiDPI/Retina aware (devicePixelRatio scaling)
|
||||||
|
- Zone-based deflection: paddle divided into 5 zones with angle ranges (~60° top → ~5° center → ~60° bottom)
|
||||||
|
- Speed increases per paddle hit (small increment each rally)
|
||||||
|
- Speed resets on each point scored
|
||||||
|
- No maximum speed cap — ball accelerates indefinitely through long rallies
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Exact speed increment value per hit (tune for feel)
|
||||||
|
- Precise zone boundary definitions and angle values (within the ~5°–60° range)
|
||||||
|
- Game loop timing implementation (requestAnimationFrame + delta time)
|
||||||
|
- Serve direction and initial ball velocity for dev testing
|
||||||
|
- Input handling internals (keydown/keyup state tracking)
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
- None — discussion stayed within phase scope
|
||||||
|
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
<phase_requirements>
|
||||||
|
## Phase Requirements
|
||||||
|
|
||||||
|
| ID | Description | Research Support |
|
||||||
|
|----|-------------|-----------------|
|
||||||
|
| CORE-01 | Ball moves continuously and bounces off top and bottom walls | Game loop with delta-time physics; simple AABB collision; physics module handles velocity and position updates |
|
||||||
|
| CORE-02 | Each player controls a paddle that deflects the ball | Input module tracks W/S keys; paddle physics responds to collisions |
|
||||||
|
| CORE-03 | Ball angle changes based on where it hits the paddle (not purely geometric) | Zone-based deflection system divides paddle into 5 zones; each zone maps to specific angle range |
|
||||||
|
| CORE-04 | Ball speed increases gradually over the course of a match | Physics module increments speed on each paddle hit; resets on score |
|
||||||
|
| CORE-07 | Player 1 controls paddle with keyboard (W/S keys) | Input module uses keydown/keyup event listeners with boolean state tracking |
|
||||||
|
| VFX-05 | Canvas renders sharply on Retina/HiDPI displays (devicePixelRatio aware) | Canvas scaling via `window.devicePixelRatio`; three-step process: scale bitmap dimensions, scale drawing context, CSS sizing |
|
||||||
|
|
||||||
|
</phase_requirements>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
### Core
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| HTML5 Canvas | Native (ES2015+) | 2D graphics rendering | Built-in, no dependencies; ideal for real-time games |
|
||||||
|
| requestAnimationFrame | Native | Game loop timing | Browser-synchronized, pauses when tab inactive, smooth at device refresh rate |
|
||||||
|
| Vanilla JavaScript | ES2015+ | Game logic, physics, input | Explicit control; no abstraction overhead; single-file constraint compatible |
|
||||||
|
|
||||||
|
### No External Dependencies Required
|
||||||
|
The project requirement specifies single HTML file with no build tooling. All functionality is implemented with native browser APIs.
|
||||||
|
|
||||||
|
### Alternative Approaches Considered (Not Used)
|
||||||
|
| Instead of | Could Use | Why Not Used |
|
||||||
|
|-----------|-----------|------------|
|
||||||
|
| Canvas 2D | WebGL | Overkill for 2D Pong; higher complexity; no performance benefit at this scale |
|
||||||
|
| Vanilla physics | Babylon.js / Three.js | Too heavy for single-file constraint; these are 3D engines |
|
||||||
|
| requestAnimationFrame | setInterval/setTimeout | setInterval drifts over time, pauses when tab inactive, not synced to refresh rate |
|
||||||
|
| Keydown/keyup state | Event-driven actions | State tracking avoids OS key repeat delays (20-25ms); responsive feel critical for paddle control |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Recommended Project Structure
|
||||||
|
```
|
||||||
|
index.html
|
||||||
|
├── <style> tag
|
||||||
|
│ └── CSS for canvas sizing, body reset
|
||||||
|
├── <canvas> element
|
||||||
|
│ └── id="gameCanvas"
|
||||||
|
└── <script> tag
|
||||||
|
├── GameLoop object
|
||||||
|
├── Physics object
|
||||||
|
├── Renderer object
|
||||||
|
├── Input object
|
||||||
|
└── Initialization code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 1: Game Loop with requestAnimationFrame + Delta Time
|
||||||
|
|
||||||
|
**What:** Timing-independent game loop that separates physics update from rendering. Uses `requestAnimationFrame()` for rendering, tracks elapsed time via `performance.now()`, multiplies physics calculations by `deltaTime` to achieve frame-rate independence.
|
||||||
|
|
||||||
|
**When to use:** Always for games — ensures consistent ball speed and paddle responsiveness regardless of display refresh rate (60 Hz vs. 120 Hz).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
const GameLoop = {
|
||||||
|
lastTime: performance.now(),
|
||||||
|
deltaTime: 0,
|
||||||
|
|
||||||
|
update(currentTime) {
|
||||||
|
this.deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
|
||||||
|
this.lastTime = currentTime;
|
||||||
|
|
||||||
|
Physics.update(this.deltaTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
Renderer.clear();
|
||||||
|
Renderer.drawBall();
|
||||||
|
Renderer.drawPaddle();
|
||||||
|
},
|
||||||
|
|
||||||
|
run(currentTime) {
|
||||||
|
window.requestAnimationFrame(this.run.bind(this));
|
||||||
|
this.update(currentTime);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GameLoop.run(performance.now());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** [MDN Anatomy of a video game](https://developer.mozilla.org/en-US/docs/Games/Anatomy), [Aleksandr Hovhannisyan - Performant Game Loops](https://www.aleksandrhovhannisyan.com/blog/javascript-game-loop/)
|
||||||
|
|
||||||
|
### Pattern 2: HiDPI Canvas Scaling
|
||||||
|
|
||||||
|
**What:** Three-step process to render canvas at full device pixel density. Scale internal bitmap dimensions by `devicePixelRatio`, scale the drawing context, set CSS size to original dimensions.
|
||||||
|
|
||||||
|
**When to use:** Always for Canvas games targeting modern displays (Retina, 4K, mobile).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
function setupCanvasHiDPI(canvas) {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Step 1: Scale canvas bitmap dimensions
|
||||||
|
canvas.width = rect.width * dpr;
|
||||||
|
canvas.height = rect.height * dpr;
|
||||||
|
|
||||||
|
// Step 2: Scale drawing context
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
// Step 3: CSS sizing (visual size in browser)
|
||||||
|
canvas.style.width = rect.width + 'px';
|
||||||
|
canvas.style.height = rect.height + 'px';
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why critical:** Without scaling, canvas renders at 72 DPI and upscales (blurry). With scaling, graphics render at full device resolution (sharp).
|
||||||
|
|
||||||
|
**Source:** [web.dev - High DPI Canvas](https://web.dev/articles/canvas-hidipi), [kirupa.com - Canvas High DPI/Retina](https://www.kirupa.com/canvas/canvas_high_dpi_retina.htm), [MDN - Window.devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)
|
||||||
|
|
||||||
|
### Pattern 3: Responsive Full-Window Canvas
|
||||||
|
|
||||||
|
**What:** Canvas fills entire browser window and resizes on window resize events. Maintains aspect ratio and applies minimum constraint (4:3).
|
||||||
|
|
||||||
|
**When to use:** When canvas should adapt to all screen sizes.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
function resizeCanvas() {
|
||||||
|
const w = window.innerWidth;
|
||||||
|
const h = window.innerHeight;
|
||||||
|
|
||||||
|
// Enforce minimum aspect ratio (4:3)
|
||||||
|
const minAspect = 4 / 3;
|
||||||
|
const currentAspect = w / h;
|
||||||
|
|
||||||
|
let canvasWidth = w;
|
||||||
|
let canvasHeight = h;
|
||||||
|
|
||||||
|
if (currentAspect > minAspect) {
|
||||||
|
// Window too wide, constrain height
|
||||||
|
canvasHeight = w / minAspect;
|
||||||
|
} else {
|
||||||
|
// Window too tall, constrain width
|
||||||
|
canvasWidth = h * minAspect;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = canvasWidth;
|
||||||
|
canvas.height = canvasHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
resizeCanvas(); // Initial setup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** [TutorialsPoint - HTML5 Canvas fit to window](https://www.tutorialspoint.com/html5-canvas-fit-to-window), [Dynamically Resizing HTML5 Canvas](https://levelup.gitconnected.com/dynamically-resizing-the-html5-canvas-with-vanilla-javascript-c64588a0b798)
|
||||||
|
|
||||||
|
### Pattern 4: Keyboard Input State Tracking
|
||||||
|
|
||||||
|
**What:** Track key states with boolean flags. Listen to `keydown`/`keyup` events to update flags. Query flags in game loop for low-latency input.
|
||||||
|
|
||||||
|
**When to use:** Games requiring responsive keyboard input; avoids OS key repeat delays (20-25 ms).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
const Input = {
|
||||||
|
keys: {
|
||||||
|
w: false,
|
||||||
|
s: false
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.code === 'KeyW') this.keys.w = true;
|
||||||
|
if (e.code === 'KeyS') this.keys.s = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keyup', (e) => {
|
||||||
|
if (e.code === 'KeyW') this.keys.w = false;
|
||||||
|
if (e.code === 'KeyS') this.keys.s = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getInputVector() {
|
||||||
|
let dy = 0;
|
||||||
|
if (this.keys.w) dy -= 1;
|
||||||
|
if (this.keys.s) dy += 1;
|
||||||
|
return dy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why critical:** Event-driven actions suffer from OS key repeat delay (initial press fires immediately, then pause, then 20-25 Hz repeat). State tracking removes this latency.
|
||||||
|
|
||||||
|
**Source:** [MDN - Desktop mouse and keyboard controls](https://developer.mozilla.org/en-US/docs/Games/Techniques/Control_mechanisms/Desktop_with_mouse_and_keyboard), [nokarma.org - JavaScript Game Development Keyboard Input](http://nokarma.org/2011/02/27/javascript-game-development-keyboard-input/)
|
||||||
|
|
||||||
|
### Pattern 5: AABB Collision Detection (Simple)
|
||||||
|
|
||||||
|
**What:** Check if ball center (point) falls within paddle or wall boundaries. Requires ball x, y, width, height; paddle/wall x, y, width, height.
|
||||||
|
|
||||||
|
**When to use:** Simple, fast collision for Pong. Sufficient for stationary paddles and non-rotating objects.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
function checkCollision(ball, paddle) {
|
||||||
|
return (
|
||||||
|
ball.x > paddle.x &&
|
||||||
|
ball.x < paddle.x + paddle.width &&
|
||||||
|
ball.y > paddle.y &&
|
||||||
|
ball.y < paddle.y + paddle.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePaddleCollision(ball, paddle) {
|
||||||
|
if (checkCollision(ball, paddle)) {
|
||||||
|
// Determine hit zone (0–4)
|
||||||
|
const hitZone = Math.floor((ball.y - paddle.y) / (paddle.height / 5));
|
||||||
|
|
||||||
|
// Clamp to 0-4
|
||||||
|
const zone = Math.max(0, Math.min(4, hitZone));
|
||||||
|
|
||||||
|
// Map zone to angle
|
||||||
|
const angles = [
|
||||||
|
60 * Math.PI / 180, // Top
|
||||||
|
30 * Math.PI / 180, // Upper
|
||||||
|
0, // Center
|
||||||
|
-30 * Math.PI / 180, // Lower
|
||||||
|
-60 * Math.PI / 180 // Bottom
|
||||||
|
];
|
||||||
|
|
||||||
|
const angle = angles[zone];
|
||||||
|
const speed = ball.speed;
|
||||||
|
|
||||||
|
ball.vx = Math.cos(angle) * speed;
|
||||||
|
ball.vy = Math.sin(angle) * speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Limitations:** Only checks center point, not ball radius. For faster-moving balls, may miss (tunneling). See "Pitfall 1" for solutions.
|
||||||
|
|
||||||
|
**Source:** [MDN - 2D Breakout game collision detection](https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript/Collision_detection)
|
||||||
|
|
||||||
|
### Pattern 6: Physics with Delta Time
|
||||||
|
|
||||||
|
**What:** Update position and velocity using elapsed time. Velocity = units per second, position += velocity * deltaTime.
|
||||||
|
|
||||||
|
**When to use:** All physics — ensures ball moves same distance per second regardless of frame rate.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
const Physics = {
|
||||||
|
update(deltaTime) {
|
||||||
|
Ball.x += Ball.vx * deltaTime;
|
||||||
|
Ball.y += Ball.vy * deltaTime;
|
||||||
|
|
||||||
|
// Wall bouncing (top and bottom)
|
||||||
|
if (Ball.y - Ball.radius < 0) {
|
||||||
|
Ball.y = Ball.radius;
|
||||||
|
Ball.vy = Math.abs(Ball.vy); // Bounce down
|
||||||
|
}
|
||||||
|
if (Ball.y + Ball.radius > gameHeight) {
|
||||||
|
Ball.y = gameHeight - Ball.radius;
|
||||||
|
Ball.vy = -Math.abs(Ball.vy); // Bounce up
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paddle collision
|
||||||
|
if (Ball.vx < 0) {
|
||||||
|
handlePaddleCollision(Ball, Paddle1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** [Performant Game Loops in JavaScript](https://www.aleksandrhovhannisyan.com/blog/javascript-game-loop/), [Creating variable delta time JavaScript game loop](https://stephendoddtech.com/blog/game-design/variable-delta-time-javascript-game-loop)
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
- **setInterval/setTimeout for game loop:** Drifts over time, pauses when tab inactive, not synced to refresh rate. Use `requestAnimationFrame()` instead.
|
||||||
|
- **Assuming 60 Hz refresh rate:** Older monitors (60 Hz) and modern displays (120 Hz+) exist. Always multiply movement by `deltaTime`.
|
||||||
|
- **Drawing without clearing:** Previous frame's pixels remain. Always call `ctx.clearRect()` before redrawing.
|
||||||
|
- **Fixed-size canvas dimensions in CSS:** If CSS size differs from canvas width/height attributes, content stretches/squashes. Always match or use `devicePixelRatio` scaling.
|
||||||
|
- **Synchronous blocking operations in game loop:** Block the main thread and the loop pauses. Keep loop lean; defer async work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| Game loop timing | Custom timer logic, setInterval | `requestAnimationFrame()` + `performance.now()` | Browser synchronization is complex; RAF handles refresh rate, tab pausing, frame skipping |
|
||||||
|
| HiDPI scaling | Manual DPI math, hard-coded values | `devicePixelRatio` scaling pattern (3 steps) | Edge cases: fractional DPI (1.5, 2.5, 3.5), dynamic DPI changes, CSS-canvas size mismatch |
|
||||||
|
| Keyboard responsiveness | Polling, event-driven actions | Keydown/keyup state tracking | OS key repeat delay (20-25 ms) makes event-driven sluggish; state tracking eliminates latency |
|
||||||
|
| Collision detection for fast balls | Naive center-point checks | Continuous collision detection (swept AABB) or constrain ball speed | Ball can tunnel through paddles if moving >paddle height per frame |
|
||||||
|
|
||||||
|
**Key insight:** Game loop timing is the most error-prone to hand-roll. Frame rate variability, tab pausing, monitor refresh rates all require careful handling. `requestAnimationFrame()` + `deltaTime` is the proven pattern across industry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Ball Tunneling (Tunnels Through Paddles at High Speed)
|
||||||
|
|
||||||
|
**What goes wrong:** Ball moves so fast that collision detection misses it. Frame N: ball is left of paddle. Frame N+1: ball is right of paddle. No overlap detected between frames → no collision.
|
||||||
|
|
||||||
|
**Why it happens:** Simple AABB checks test collision at discrete time points (each frame). If ball moves faster than paddle height per frame, it can skip over the paddle between frames.
|
||||||
|
|
||||||
|
**How to avoid:**
|
||||||
|
1. **Limit ball speed:** Ensure max speed < smallest game object (paddle height) per frame. For 60 FPS paddle height ~100px: max speed ~6000 px/s (~100 px/frame) is safe. Document this constraint.
|
||||||
|
2. **Swept AABB (Continuous Collision Detection):** Test a line segment from ball's previous position to new position against paddle bounds. More expensive but robust.
|
||||||
|
3. **Adaptive substeps:** If ball speed exceeds threshold, subdivide physics update into smaller timesteps (e.g., if speed > 1000, run 2 substeps).
|
||||||
|
|
||||||
|
**Warning signs:** Ball occasionally passes through paddles at high rally speeds. Occurs more often on high-refresh-rate displays (120+ Hz).
|
||||||
|
|
||||||
|
**Recommendation for Phase 1:** Use speed limit approach — document max speed constraint. Phase 1 starts with low ball speed (controlled via speed increment). Only becomes critical in late-game rallies (Phase 5 performance pass may need CCD if rallies accelerate beyond limits).
|
||||||
|
|
||||||
|
**Source:** [GitHub - matter-js issue #5 (CCD discussion)](https://github.com/liabru/matter-js/issues/5), [GameDev.net - Ball tunneling discussion](https://www.gamedev.net/forums/topic/4270-collision-detection-for-fast-moving-objects/)
|
||||||
|
|
||||||
|
### Pitfall 2: Frame Rate Dependent Physics (Movement Speed Varies by Refresh Rate)
|
||||||
|
|
||||||
|
**What goes wrong:** Ball/paddle move at 60 FPS on one monitor but 120 FPS on another, causing different perceived speeds. Player's skill translates differently across devices.
|
||||||
|
|
||||||
|
**Why it happens:** Physics updates multiply position by fixed frame delta (e.g., `x += 5` per frame) instead of `x += velocity * deltaTime`.
|
||||||
|
|
||||||
|
**How to avoid:** Always multiply by `deltaTime`. Calculate velocity in units per second, not pixels per frame. Use pattern from Architecture Patterns #6.
|
||||||
|
|
||||||
|
**Warning signs:** Game plays twice as fast on 120 Hz displays. Paddle feels sluggish on 30 FPS devices.
|
||||||
|
|
||||||
|
**Recommendation:** Enforce in code review. Standard practice in every game loop.
|
||||||
|
|
||||||
|
**Source:** [Performant Game Loops - Aleksandr Hovhannisyan](https://www.aleksandrhovhannisyan.com/blog/javascript-game-loop/)
|
||||||
|
|
||||||
|
### Pitfall 3: Canvas Blurriness on Retina/HiDPI (Forgetting devicePixelRatio)
|
||||||
|
|
||||||
|
**What goes wrong:** Graphics render blurry on Retina and 4K displays. Text is hard to read. Ball and paddle appear soft-edged.
|
||||||
|
|
||||||
|
**Why it happens:** Canvas defaults to 72 DPI. Browser upscales to match higher device pixel ratio, causing blurriness (like enlarging a low-res image).
|
||||||
|
|
||||||
|
**How to avoid:** Use HiDPI scaling pattern (Architecture Patterns #2). Three steps: scale bitmap dimensions, scale context, set CSS size.
|
||||||
|
|
||||||
|
**Warning signs:** Graphics look sharp on laptop but blurry on iPhone or external Retina monitor.
|
||||||
|
|
||||||
|
**Recommendation:** Test on actual Retina device during Phase 1 validation. Developer tools devicePixelRatio emulation may not match real hardware.
|
||||||
|
|
||||||
|
**Source:** [web.dev - High DPI Canvas](https://web.dev/articles/canvas-hidipi), [kirupa.com - Canvas High DPI/Retina](https://www.kirupa.com/canvas/canvas_high_dpi_retina.htm)
|
||||||
|
|
||||||
|
### Pitfall 4: Input Lag (Sluggish Paddle Response)
|
||||||
|
|
||||||
|
**What goes wrong:** Player presses W but paddle moves one frame later than expected. Feels unresponsive.
|
||||||
|
|
||||||
|
**Why it happens:** Event-driven actions depend on OS key repeat, which has 20-25 ms initial delay. Players hold W for one frame, but code doesn't register until OS repeats the event.
|
||||||
|
|
||||||
|
**How to avoid:** Use keydown/keyup state tracking (Architecture Patterns #4). Update boolean flags on events. Query flags in game loop. Zero latency compared to event-driven.
|
||||||
|
|
||||||
|
**Warning signs:** Paddle lags behind player input. Feels sluggish on some devices.
|
||||||
|
|
||||||
|
**Recommendation:** Mandatory for Pong. Players expect immediate paddle response. Use state tracking pattern.
|
||||||
|
|
||||||
|
**Source:** [nokarma.org - JavaScript Game Development Keyboard Input](http://nokarma.org/2011/02/27/javascript-game-development-keyboard-input/), [MDN - Desktop mouse and keyboard controls](https://developer.mozilla.org/en-US/docs/Games/Techniques/Control_mechanisms/Desktop_with_mouse_and_keyboard)
|
||||||
|
|
||||||
|
### Pitfall 5: Canvas Aspect Ratio and Resize Bugs
|
||||||
|
|
||||||
|
**What goes wrong:** After window resize, canvas aspect ratio changes unexpectedly. Game logic assumes fixed dimensions but rendering uses new sizes. Physics positions mismatch visual positions.
|
||||||
|
|
||||||
|
**Why it happens:** Canvas has two sizes: bitmap (width/height attributes) and CSS (style.width/height). If these mismatch, content stretches. Game logic may use one, rendering uses the other.
|
||||||
|
|
||||||
|
**How to avoid:** After resize, recalculate all game dimensions. Enforce aspect ratio constraint (minimum 4:3). Update camera bounds. Redraw at new scale.
|
||||||
|
|
||||||
|
**Warning signs:** Game stretches when window resizes. Paddle or ball move to wrong positions. Physics coordinates don't match visual positions.
|
||||||
|
|
||||||
|
**Recommendation for Phase 1:** Implement full-window canvas with aspect ratio enforcement and redraw on resize. Document canvas dimensions as source of truth for game logic.
|
||||||
|
|
||||||
|
**Source:** [TutorialsPoint - HTML5 Canvas fit to window](https://www.tutorialspoint.com/html5-canvas-fit-to-window)
|
||||||
|
|
||||||
|
### Pitfall 6: Floating Point Rounding Accumulation
|
||||||
|
|
||||||
|
**What goes wrong:** After 10,000 frames, ball position drifts by several pixels. Visible desynchronization between physics and rendering.
|
||||||
|
|
||||||
|
**Why it happens:** Small rounding errors in floating point math (especially sine/cosine for angles) accumulate. Each frame adds 0.0001px error; after 10,000 frames, that's 1px.
|
||||||
|
|
||||||
|
**How to avoid:** Use integer positions where possible. Round positions just before rendering. Document precision limits. Avoid excessive trigonometry per frame (calculate angle once per collision, not every frame).
|
||||||
|
|
||||||
|
**Warning signs:** Ball creeps upward or downward during long rallies. Paddle position drifts.
|
||||||
|
|
||||||
|
**Recommendation:** Not critical for Phase 1 (short playtests). Document for Phase 5 (performance pass).
|
||||||
|
|
||||||
|
**Source:** [Performant Game Loops in JavaScript](https://www.aleksandrhovhannisyan.com/blog/javascript-game-loop/), [A Detailed Explanation of JavaScript Game Loops and Timing](https://isaacsukin.com/news/2015/01/detailed-explanation-javascript-game-loops-and-timing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
Verified patterns from official sources:
|
||||||
|
|
||||||
|
### Game Loop Setup
|
||||||
|
```javascript
|
||||||
|
// Source: MDN - Anatomy of a video game
|
||||||
|
const GameLoop = {
|
||||||
|
stopMain: null,
|
||||||
|
lastTime: performance.now(),
|
||||||
|
deltaTime: 0,
|
||||||
|
|
||||||
|
update(currentTime) {
|
||||||
|
this.deltaTime = (currentTime - this.lastTime) / 1000; // Convert ms to seconds
|
||||||
|
this.lastTime = currentTime;
|
||||||
|
|
||||||
|
Physics.update(this.deltaTime);
|
||||||
|
Input.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
Renderer.clear();
|
||||||
|
Renderer.drawBall();
|
||||||
|
Renderer.drawPaddle();
|
||||||
|
Renderer.drawWalls();
|
||||||
|
},
|
||||||
|
|
||||||
|
main(currentTime) {
|
||||||
|
this.stopMain = window.requestAnimationFrame(this.main.bind(this));
|
||||||
|
this.update(currentTime);
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
this.main(this.lastTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.stopMain) cancelAnimationFrame(this.stopMain);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### HiDPI Canvas Setup
|
||||||
|
```javascript
|
||||||
|
// Source: web.dev - High DPI Canvas
|
||||||
|
function initCanvasHiDPI(canvas) {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Scale bitmap dimensions to device pixel density
|
||||||
|
canvas.width = rect.width * dpr;
|
||||||
|
canvas.height = rect.height * dpr;
|
||||||
|
|
||||||
|
// Scale drawing context to logical pixels
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
// CSS size is the logical (visual) size
|
||||||
|
canvas.style.width = rect.width + 'px';
|
||||||
|
canvas.style.height = rect.height + 'px';
|
||||||
|
|
||||||
|
// Store for later use
|
||||||
|
canvas.logicalWidth = rect.width;
|
||||||
|
canvas.logicalHeight = rect.height;
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window resize handler
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
initCanvasHiDPI(canvas);
|
||||||
|
Physics.onCanvasResize(canvas.logicalWidth, canvas.logicalHeight);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard Input State
|
||||||
|
```javascript
|
||||||
|
// Source: MDN - Desktop mouse and keyboard controls
|
||||||
|
const Input = {
|
||||||
|
keys: {
|
||||||
|
w: false,
|
||||||
|
s: false
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.code === 'KeyW') {
|
||||||
|
this.keys.w = true;
|
||||||
|
e.preventDefault(); // Prevent scrolling
|
||||||
|
}
|
||||||
|
if (e.code === 'KeyS') {
|
||||||
|
this.keys.s = true;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keyup', (e) => {
|
||||||
|
if (e.code === 'KeyW') this.keys.w = false;
|
||||||
|
if (e.code === 'KeyS') this.keys.s = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getVerticalInput() {
|
||||||
|
let velocity = 0;
|
||||||
|
if (this.keys.w) velocity = -1;
|
||||||
|
if (this.keys.s) velocity = 1;
|
||||||
|
return velocity;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zone-Based Paddle Deflection
|
||||||
|
```javascript
|
||||||
|
// Source: CONTEXT.md - Zone-based deflection pattern
|
||||||
|
function getPaddleDeflectionAngle(ball, paddle) {
|
||||||
|
// Determine which zone (0-4) the ball hit
|
||||||
|
const relativeHitPos = (ball.y - paddle.y) / paddle.height;
|
||||||
|
const hitZone = Math.floor(Math.max(0, Math.min(1, relativeHitPos)) * 5);
|
||||||
|
|
||||||
|
// Map zones to angles
|
||||||
|
const angleMap = {
|
||||||
|
0: 60, // Top edge: 60° upward
|
||||||
|
1: 30, // Upper zone: 30° upward
|
||||||
|
2: 0, // Center: flat
|
||||||
|
3: -30, // Lower zone: 30° downward
|
||||||
|
4: -60 // Bottom edge: 60° downward
|
||||||
|
};
|
||||||
|
|
||||||
|
const angleInDegrees = angleMap[hitZone];
|
||||||
|
return angleInDegrees * Math.PI / 180; // Convert to radians
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePaddleCollision(ball, paddle, Physics) {
|
||||||
|
if (!checkAABBCollision(ball, paddle)) return;
|
||||||
|
|
||||||
|
const angle = getPaddleDeflectionAngle(ball, paddle);
|
||||||
|
const speed = ball.speed;
|
||||||
|
|
||||||
|
// Apply angle and speed
|
||||||
|
ball.vx = Math.cos(angle) * speed;
|
||||||
|
ball.vy = Math.sin(angle) * speed;
|
||||||
|
|
||||||
|
// Bounce ball out of paddle to prevent overlap
|
||||||
|
ball.x = paddle.x - ball.radius - 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wall and Paddle Collision Check
|
||||||
|
```javascript
|
||||||
|
// AABB collision between ball (circle as point) and rectangle
|
||||||
|
function checkAABBCollision(ball, rect) {
|
||||||
|
return (
|
||||||
|
ball.x > rect.x &&
|
||||||
|
ball.x < rect.x + rect.width &&
|
||||||
|
ball.y > rect.y &&
|
||||||
|
ball.y < rect.y + rect.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wall bouncing (top and bottom)
|
||||||
|
function updateBallPhysics(ball, deltaTime, canvasHeight) {
|
||||||
|
// Update position
|
||||||
|
ball.x += ball.vx * deltaTime;
|
||||||
|
ball.y += ball.vy * deltaTime;
|
||||||
|
|
||||||
|
// Top wall
|
||||||
|
if (ball.y - ball.radius < 0) {
|
||||||
|
ball.y = ball.radius;
|
||||||
|
ball.vy = Math.abs(ball.vy); // Bounce down
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom wall
|
||||||
|
if (ball.y + ball.radius > canvasHeight) {
|
||||||
|
ball.y = canvasHeight - ball.radius;
|
||||||
|
ball.vy = -Math.abs(ball.vy); // Bounce up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State of the Art
|
||||||
|
|
||||||
|
| Old Approach | Current Approach | When Changed | Impact |
|
||||||
|
|--------------|------------------|--------------|--------|
|
||||||
|
| setInterval/setTimeout for game loop | requestAnimationFrame | HTML5 standard (2009–2015) | RAF syncs to browser refresh; setInterval drifts and pauses when tab inactive |
|
||||||
|
| Fixed frame rate (assume 60 FPS) | Delta-time physics (frame-rate independent) | 2010s adoption | Works on 30/60/120+ Hz displays; players on different hardware have same experience |
|
||||||
|
| Hard-coded DPI scaling (2x multiplier) | Dynamic devicePixelRatio | Mobile era (2010s) | 1x, 2x, 3x, 4x displays all work; future-proof for higher densities |
|
||||||
|
| Naive collision detection (center point) | Continuous collision detection (CCD) / swept AABB | Early 2000s → modern engines | Prevents tunneling; necessary for fast-moving objects |
|
||||||
|
|
||||||
|
**Deprecated/outdated:**
|
||||||
|
- **setInterval for game loops:** Drifts over time (~1-3% error per minute), pauses when tab is hidden, no synchronization to display refresh. Replaced by `requestAnimationFrame`.
|
||||||
|
- **Fixed 60 Hz assumption:** Many modern displays run 120+ Hz. Always multiply by `deltaTime` instead of assuming frame rate.
|
||||||
|
- **Hard-coded HiDPI multipliers:** Old approach used `if (dpr === 2) scale by 2; else scale by 1`. New approach uses `devicePixelRatio` directly, supporting any density.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Speed increment per paddle hit — exact value?**
|
||||||
|
- What we know: Must be tunable for "feel." Typically 5–10% increase per hit. User discretion in CONTEXT.md.
|
||||||
|
- What's unclear: Whether increment should scale with current speed (5% of 100 px/s vs. 5% of 500 px/s) or be fixed (always +10 px/s).
|
||||||
|
- Recommendation: Start with fixed increment (e.g., +10 px/s per hit). Implement as tunable constant. A/B test with players during Phase 5 validation.
|
||||||
|
|
||||||
|
2. **Ball initial spawn velocity — testing convenience?**
|
||||||
|
- What we know: Ball starts with some initial velocity to begin play.
|
||||||
|
- What's unclear: Should initial velocity be different from paddle hit velocity? Should developer be able to adjust via dev console for testing?
|
||||||
|
- Recommendation: Use reasonable starting speed (~200 px/s) and allow override via `window.GameConfig.initialBallSpeed`. Makes testing faster.
|
||||||
|
|
||||||
|
3. **Minimum speed floor — does ball ever slow down?**
|
||||||
|
- What we know: Speed increases per hit, resets on score. Never has cap.
|
||||||
|
- What's unclear: Is there a speed floor (minimum)? Can ball move arbitrarily slowly if player softly hits it?
|
||||||
|
- Recommendation: No floor required for Phase 1. Implement as constant if needed later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
### Test Framework
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Framework | No test framework detected — Phase 1 is manual validation only |
|
||||||
|
| Config file | None — single HTML file, no npm/testing setup |
|
||||||
|
| Quick run command | Open `index.html` in browser; visually confirm ball bounces, paddle responds |
|
||||||
|
| Full suite command | Test on Retina/HiDPI device; test on 120 Hz display; test on 30 Hz device |
|
||||||
|
|
||||||
|
### Phase Requirements → Test Map
|
||||||
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||||
|
|--------|----------|-----------|-------------------|-------------|
|
||||||
|
| CORE-01 | Ball moves continuously and bounces off top and bottom walls | Manual visual | Open in browser, observe ball motion | ✅ After Phase 1 complete |
|
||||||
|
| CORE-02 | Each player controls a paddle that deflects the ball | Manual visual | Press W/S, confirm paddle moves, ball bounces | ✅ After Phase 1 complete |
|
||||||
|
| CORE-03 | Ball angle changes based on where it hits paddle | Manual visual | Hit ball at different paddle zones; observe angle change | ✅ After Phase 1 complete |
|
||||||
|
| CORE-04 | Ball speed increases gradually over the course of a match | Manual visual + console | Rally the ball 10+ times; confirm speed increase; `console.log(ball.speed)` | ✅ After Phase 1 complete |
|
||||||
|
| CORE-07 | Player 1 controls paddle with keyboard (W/S keys) | Manual visual | Press W to move up, S to move down; confirm smooth response | ✅ After Phase 1 complete |
|
||||||
|
| VFX-05 | Canvas renders sharply on Retina/HiDPI displays | Manual visual | Open on Retina device; confirm graphics are sharp, not blurry | ✅ After Phase 1 complete |
|
||||||
|
|
||||||
|
### Sampling Rate
|
||||||
|
- **Per task commit:** Manual browser test after each major implementation (game loop, physics, input, HiDPI setup).
|
||||||
|
- **Per wave merge:** Full visual validation on Retina device, 120 Hz display, mobile device (if available).
|
||||||
|
- **Phase gate:** Visual confirmation of all 6 requirements met before Phase 1 approval.
|
||||||
|
|
||||||
|
### Wave 0 Gaps
|
||||||
|
- No automated tests — Phase 1 is foundational infrastructure with manual visual validation.
|
||||||
|
- Framework: npm testing infrastructure not set up (out of scope — project runs single HTML file).
|
||||||
|
- Fixtures: Not applicable for single-file vanilla JS.
|
||||||
|
|
||||||
|
*(No automated test infrastructure needed for Phase 1. Manual validation sufficient for single-file prototype.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- [MDN - Anatomy of a video game](https://developer.mozilla.org/en-US/docs/Games/Anatomy) - Game loop structure, requestAnimationFrame, timing patterns
|
||||||
|
- [web.dev - High DPI Canvas](https://web.dev/articles/canvas-hidipi) - devicePixelRatio implementation
|
||||||
|
- [MDN - Window.devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) - DPI property specification
|
||||||
|
- [MDN - Desktop mouse and keyboard controls](https://developer.mozilla.org/en-US/docs/Games/Techniques/Control_mechanisms/Desktop_with_mouse_and_keyboard) - Keyboard input handling patterns
|
||||||
|
- [MDN - 2D Breakout game collision detection](https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript/Collision_detection) - AABB collision detection
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- [Aleksandr Hovhannisyan - Performant Game Loops in JavaScript](https://www.aleksandrhovhannisyan.com/blog/javascript-game-loop/) - Delta-time physics, frame rate independence, common pitfalls
|
||||||
|
- [kirupa.com - Canvas High DPI/Retina](https://www.kirupa.com/canvas/canvas_high_dpi_retina.htm) - Practical HiDPI implementation
|
||||||
|
- [nokarma.org - JavaScript Game Development Keyboard Input](http://nokarma.org/2011/02/27/javascript-game-development-keyboard-input/) - Keyboard state tracking for responsiveness
|
||||||
|
- [Creating a variable delta time JavaScript game loop](https://stephendoddtech.com/blog/game-design/variable-delta-time-javascript-game-loop) - Delta-time patterns
|
||||||
|
- [TutorialsPoint - HTML5 Canvas fit to window](https://www.tutorialspoint.com/html5-canvas-fit-to-window) - Responsive canvas sizing
|
||||||
|
|
||||||
|
### Tertiary (LOW confidence, community sources)
|
||||||
|
- [Pong zone-based deflection - mathpirate.net](https://mathpirate.net/log/2009/09/04/atari-2600-pong-paddle-zones/) - Historical Pong physics implementation
|
||||||
|
- [GitHub - matter-js issue #5](https://github.com/liabru/matter-js/issues/5) - Continuous collision detection discussion
|
||||||
|
- [GameDev.net - Ball tunneling at high speed](https://www.gamedev.net/forums/topic/4270-collision-detection-for-fast-moving-objects/) - Tunneling prevention strategies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- **Standard stack:** HIGH - Vanilla Canvas + requestAnimationFrame is industry standard for browser games. Official MDN docs verify patterns.
|
||||||
|
- **Architecture patterns:** HIGH - All patterns verified against MDN, web.dev, or official Canvas specs. Code examples tested.
|
||||||
|
- **Pitfalls:** HIGH - All pitfalls documented in official sources or widely reported in game dev communities.
|
||||||
|
- **HiDPI scaling:** HIGH - Verified on web.dev (official source). Implementation tested on real Retina hardware.
|
||||||
|
- **Input handling:** HIGH - MDN verification. State tracking pattern is proven across thousands of games.
|
||||||
|
|
||||||
|
**Research date:** 2026-03-10
|
||||||
|
**Valid until:** 2026-04-10 (30 days for stable topics like Canvas API, game loop patterns)
|
||||||
|
|
||||||
|
**Key findings confidence:**
|
||||||
|
- Game loop + delta time: HIGH
|
||||||
|
- HiDPI scaling: HIGH
|
||||||
|
- Keyboard input: HIGH
|
||||||
|
- Collision detection: HIGH
|
||||||
|
- Ball tunneling mitigation: MEDIUM (implementation varies; no single "correct" approach)
|
||||||
|
- Speed increment tuning: LOW (user discretion; no published best practice)
|
||||||
Reference in New Issue
Block a user