feat(01-01): HTML scaffold with HiDPI canvas, Renderer, and full-window layout

- Created index.html with valid HTML5 structure and black full-window canvas
- Renderer object with devicePixelRatio-aware resize() using logical vs bitmap coords
- CSS enforces overflow:hidden with no scrollbars
- Minimum 4:3 aspect ratio enforcement in resize logic
- window.addEventListener('resize') handler for live canvas rescaling
This commit is contained in:
Dabit
2026-03-10 14:46:17 +01:00
parent 558fe20271
commit e43b82b4cb

187
index.html Normal file
View File

@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Pong Next Gen</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; width: 100vw; height: 100vh; }
#gameCanvas { display: block; background: #000; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
const Renderer = {
canvas: null,
ctx: null,
logicalWidth: 0,
logicalHeight: 0,
init() {
this.canvas = document.getElementById('gameCanvas');
this.resize();
window.addEventListener('resize', () => this.resize());
},
resize() {
const dpr = window.devicePixelRatio || 1;
const w = window.innerWidth;
const h = window.innerHeight;
const minAspect = 4 / 3;
const currentAspect = w / h;
// Always fill entire window; use logical coords for game
this.logicalWidth = w;
this.logicalHeight = h;
// Enforce minimum 4:3: if narrower than 4:3, treat as 4:3 height
// (window fills screen; minimum 4:3 means we never let height dominate width)
if (currentAspect < minAspect) {
// Too tall — game uses 4:3 minimum: constrain logical height
this.logicalHeight = Math.floor(w / minAspect);
}
// Step 1: Set CSS size to logical dimensions
this.canvas.style.width = this.logicalWidth + 'px';
this.canvas.style.height = this.logicalHeight + 'px';
// Step 2: Scale bitmap to device pixel ratio
this.canvas.width = Math.floor(this.logicalWidth * dpr);
this.canvas.height = Math.floor(this.logicalHeight * dpr);
// Step 3: Scale context so all draw calls use logical pixels
this.ctx = this.canvas.getContext('2d');
this.ctx.scale(dpr, dpr);
// Notify Physics of new dimensions (Physics may not exist yet)
if (typeof Physics !== 'undefined' && Physics.onResize) {
Physics.onResize(this.logicalWidth, this.logicalHeight);
}
},
clear() {
this.ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
},
drawRect(x, y, w, h, color) {
this.ctx.fillStyle = color || '#fff';
this.ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
},
drawCircle(x, y, radius, color) {
this.ctx.beginPath();
this.ctx.arc(Math.round(x), Math.round(y), radius, 0, Math.PI * 2);
this.ctx.fillStyle = color || '#fff';
this.ctx.fill();
}
};
const GameState = {
paddle1: {
x: 0, y: 0, // Set on Physics.init()
width: 12, height: 80,
speed: 400, // Logical pixels per second
color: '#fff'
},
ball: {
x: 0, y: 0, // Set on Physics.init()
radius: 8,
vx: 0, vy: 0, // Set on Physics.init()
speed: 0, // Current scalar speed
color: '#fff'
}
};
const Input = {
keys: { w: false, s: false },
init() {
document.addEventListener('keydown', (e) => {
if (e.code === 'KeyW') { this.keys.w = true; e.preventDefault(); }
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() {
if (this.keys.w) return -1;
if (this.keys.s) return 1;
return 0;
}
};
const Physics = {
width: 0,
height: 0,
init(width, height) {
this.width = width;
this.height = height;
// Position paddle1 on left side, vertically centered
GameState.paddle1.x = 30;
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
// Ball positioned center — physics will drive it in Plan 02
GameState.ball.x = width / 2;
GameState.ball.y = height / 2;
},
onResize(width, height) {
this.width = width;
this.height = height;
},
update(deltaTime) {
// Move paddle1 based on input
const dir = Input.getVerticalInput();
const paddle = GameState.paddle1;
paddle.y += dir * paddle.speed * deltaTime;
// Clamp to canvas bounds
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
}
};
const GameLoop = {
stopHandle: null,
lastTime: 0,
start() {
this.lastTime = performance.now();
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
},
stop() {
if (this.stopHandle) cancelAnimationFrame(this.stopHandle);
},
main(currentTime) {
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.05); // Cap at 50ms
this.lastTime = currentTime;
Physics.update(deltaTime);
Renderer.clear();
// Draw paddle1
const p1 = GameState.paddle1;
Renderer.drawRect(p1.x, p1.y, p1.width, p1.height, p1.color);
// Draw ball placeholder (will move in Plan 02)
const b = GameState.ball;
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
}
};
// Initialize all modules
Renderer.init();
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
Input.init();
GameLoop.start();
</script>
</body>
</html>