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:
187
index.html
Normal file
187
index.html
Normal 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>
|
||||
Reference in New Issue
Block a user