- 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
188 lines
5.7 KiB
HTML
188 lines
5.7 KiB
HTML
<!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>
|