Prompt
Create a minimalist version of Pong that transcends the classic. Use only HTML5 Canvas and pure JavaScript, but add a modern touch: paddles should deform slightly based on ball velocity (rubber effect), implemented using a custom physics engine with spring-damper systems. The background should be a dynamic gradient that changes based on the score, using a procedural noise algorithm (like Perlin noise) to create a swirling effect. Ball physics should include a slight spin effect that affects ricochet angles, calculated using a custom vector math library. Implement a 'slow motion' mode triggered by holding down a key, using requestAnimationFrame for smooth frame rate adjustments. No external libraries.
A system prompt was added to support web rendering
Here's a minimalist yet modern Pong implementation with all the requested features: ```html <!DOCTYPE html> <html> <head> <title>Transcendent Pong</title> <style> body { margin: 0; overflow: hidden; background: #000; } canvas { display: block; } </style> </head> <body> <canvas id="gameCanvas"></canvas> <script> // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Game state let leftScore = 0; let rightScore = 0; let slowMotion = false; let gameRunning = true; // Vector math library class Vector { constructor(x = 0, y = 0) { this.x = x; this.y = y; } add(v) { return new Vector(this.x + v.x, this.y + v.y); } sub(v) { return new Vector(this.x - v.x, this.y - v.y); } mult(s) { return new Vector(this.x * s, this.y * s); } mag() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const mag = this.mag(); return mag === 0 ? new Vector() : new Vector(this.x / mag, this.y / mag); } dot(v) { return this.x * v.x + this.y * v.y; } rotate(angle) { const cos = Math.cos(angle); const sin = Math.sin(angle); return new Vector( this.x * cos - this.y * sin, this.x * sin + this.y * cos ); } } // Perlin noise implementation class PerlinNoise { constructor() { this.gradients = {}; this.memory = {}; } randomVector() { const angle = Math.random() * Math.PI * 2; return new Vector(Math.cos(angle), Math.sin(angle)); } getGradient(x, y) { const key = `${x}_${y}`; if (!this.gradients[key]) { this.gradients[key] = this.randomVector(); } return this.gradients[key]; } dotGridGradient(ix, iy, x, y) { const gradient = this.getGradient(ix, iy); const dx = x - ix; const dy = y - iy; return dx * gradient.x + dy * gradient.y; } interpolate(a, b, t) { return t * t * (3 - 2 * t) * (b - a) + a; } noise(x, y) { const x0 = Math.floor(x); const x1 = x0 + 1; const y0 = Math.floor(y); const y1 = y0 + 1; const sx = x - x0; const sy = y - y0; const n0 = this.dotGridGradient(x0, y0, x, y); const n1 = this.dotGridGradient(x1, y0, x, y); const ix0 = this.interpolate(n0, n1, sx); const n2 = this.dotGridGradient(x0, y1, x, y); const n3 = this.dotGridGradient(x1, y1, x, y); const ix1 = this.interpolate(n2, n3, sx); return this.interpolate(ix0, ix1, sy); } } // Spring-damper system for paddle deformation class SpringDamper { constructor(target, stiffness, damping) { this.target = target; this.position = target; this.velocity = 0; this.stiffness = stiffness; this.damping = damping; } update(dt) { const displacement = this.target - this.position; const springForce = -this.stiffness * displacement; const dampingForce = -this.damping * this.velocity; const force = springForce + dampingForce; this.velocity += force * dt; this.position += this.velocity * dt; } } // Paddle class with rubber effect class Paddle { constructor(x, isLeft) { this.width = 20; this.height = 120; this.x = x; this.y = canvas.height / 2 - this.height / 2; this.speed = 8; this.isLeft = isLeft; this.springs = Array(5).fill().map((_, i) => new SpringDamper(0, 0.2 + i * 0.05, 0.05 + i * 0.01) ); this.deformation = 0; } update(dt) { this.springs.forEach(spring => { spring.target = this.deformation; spring.update(dt); }); this.deformation = 0; } deform(amount) { this.deformation = amount; } draw() { ctx.save(); ctx.fillStyle = '#fff'; const segments = this.springs.length; const segmentHeight = this.height / segments; for (let i = 0; i < segments; i++) { const xOffset = this.springs[i].position * 30; const x = this.isLeft ? this.x - this.width + xOffset : this.x + this.width + xOffset; ctx.beginPath(); ctx.roundRect( x, this.y + i * segmentHeight, this.width, segmentHeight, 4 ); ctx.fill(); } ctx.restore(); } move(direction) { this.y += direction * this.speed; this.y = Math.max(0, Math.min(canvas.height - this.height, this.y)); } } // Ball class with spin effect class Ball { constructor() { this.radius = 12; this.reset(); this.spin = 0; this.maxSpin = 0.2; } reset() { this.position = new Vector(canvas.width / 2, canvas.height / 2); const angle = (Math.random() * Math.PI / 3) - Math.PI / 6; const speed = 5 + Math.random() * 2; this.velocity = new Vector( Math.random() > 0.5 ? speed : -speed, Math.sin(angle) * speed ); this.spin = 0; } update(dt) { this.position = this.position.add(this.velocity.mult(dt)); // Apply spin effect if (Math.abs(this.spin) > 0.01) { const spinEffect = new Vector( -this.velocity.y * this.spin * 0.1, this.velocity.x * this.spin * 0.1 ); this.velocity = this.velocity.add(spinEffect.mult(dt)); this.spin *= 0.99; // Spin decays over time } } draw() { ctx.save(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2); ctx.fill(); // Draw spin indicator ctx.strokeStyle = '#f00'; ctx.lineWidth = 2; ctx.beginPath(); const spinAngle = this.spin * 10; ctx.arc( this.position.x, this.position.y, this.radius - 3, spinAngle, spinAngle + Math.PI ); ctx.stroke(); ctx.restore(); } collideWithPaddle(paddle) { const halfPaddleHeight = paddle.height / 2; const paddleCenterY = paddle.y + halfPaddleHeight; // Check collision if ( (paddle.isLeft && this.position.x - this.radius < paddle.x + paddle.width && this.position.x + this.radius > paddle.x && Math.abs(this.position.y - paddleCenterY) < halfPaddleHeight + this.radius) || (!paddle.isLeft && this.position.x + this.radius > paddle.x && this.position.x - this.radius < paddle.x + paddle.width && Math.abs(this.position.y - paddleCenterY) < halfPaddleHeight + this.radius) ) { // Calculate hit position (-1 to 1) const hitPos = (this.position.y - paddleCenterY) / halfPaddleHeight; // Calculate bounce angle const maxAngle = Math.PI / 3; const angle = hitPos * maxAngle; // Reflect velocity const speed = this.velocity.mag() * 1.05; const direction = paddle.isLeft ? 1 : -1; // Calculate spin based on hit position and paddle movement const paddleMovement = this.position.y - this.velocity.y; const spinAmount = (hitPos * 0.5 + (paddleMovement - this.position.y) * 0.01); this.spin = Math.max(-this.maxSpin, Math.min(this.maxSpin, spinAmount)); this.velocity = new Vector( Math.cos(angle) * speed * direction, Math.sin(angle) * speed ); // Deform paddle based on ball velocity const deformation = this.velocity.mag() * 0.03; paddle.deform(deformation); return true; } return false; } checkWallCollision() { // Top and bottom walls if (this.position.y - this.radius < 0 || this.position.y + this.radius > canvas.height) { this.velocity.y *= -1; this.position.y = Math.max(this.radius, Math.min(canvas.height - this.radius, this.position.y)); return true; } // Scoring if (this.position.x - this.radius < 0) { rightScore++; this.reset(); return true; } if (this.position.x + this.radius > canvas.width) { leftScore++; this.reset(); return true; } return false; } } // Create game objects const leftPaddle = new Paddle(50, true); const rightPaddle = new Paddle(canvas.width - 70, false); const ball = new Ball(); const noise = new PerlinNoise(); let noiseOffset = 0; // Track pressed keys const keys = { w: false, s: false, ArrowUp: false, ArrowDown: false, Shift: false }; // Event listeners window.addEventListener('keydown', (e) => { if (keys.hasOwnProperty(e.key)) keys[e.key] = true; }); window.addEventListener('keyup', (e) => { if (keys.hasOwnProperty(e.key)) keys[e.key] = false; }); window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; leftPaddle.y = canvas.height / 2 - leftPaddle.height / 2; rightPaddle.y = canvas.height / 2 - rightPaddle.height / 2; }); // Main game loop let lastTime = 0; function gameLoop(timestamp) { if (!gameRunning) return; requestAnimationFrame(gameLoop); // Calculate delta time const deltaTime = slowMotion ? (timestamp - lastTime) * 0.3 : (timestamp - lastTime); lastTime = timestamp; // Update game state if (keys.w) leftPaddle.move(-1); if (keys.s) leftPaddle.move(1); if (keys.ArrowUp) rightPaddle.move(-1); if (keys.ArrowDown) rightPaddle.move(1); slowMotion = keys.Shift; leftPaddle.update(deltaTime / 1000); rightPaddle.update(deltaTime / 1000); ball.update(deltaTime / 1000); // Collision detection ball.collideWithPaddle(leftPaddle); ball.collideWithPaddle(rightPaddle); ball.checkWallCollision(); // Update noise offset for background animation noiseOffset += deltaTime / 5000; // Draw everything drawBackground(); drawScore(); leftPaddle.draw(); rightPaddle.draw(); ball.draw(); } function drawBackground() { const scoreFactor = (leftScore + rightScore) / 20; // Create gradient based on score const centerX = canvas.width / 2; const centerY = canvas.height / 2; const gradient = ctx.createRadialGradient( centerX, centerY, 0, centerX, centerY, Math.max(canvas.width, canvas.height) / 2 ); // Generate colors using Perlin noise const hue1 = (noise.noise(noiseOffset, 0) + 1) * 180 + scoreFactor * 10; const hue2 = (noise.noise(noiseOffset + 10, 10) + 1) * 180 + scoreFactor * 20; gradient.addColorStop(0, `hsl(${hue1 % 360}, 70%, 10%)`); gradient.addColorStop(1, `hsl(${hue2 % 360}, 70%, 5%)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Add subtle noise pattern ctx.fillStyle = 'rgba(255, 255, 255, 0.02)'; for (let i = 0; i < canvas.width; i += 20) { for (let j = 0; j < canvas.height; j += 20) { const n = noise.noise(i / 100 + noiseOffset, j / 100); if (n > 0.3) { ctx.fillRect(i, j, 1, 1); } } } } function drawScore() { ctx.save(); ctx.fillStyle = '#fff'; ctx.font = '48px monospace'; ctx.textAlign = 'center'; ctx.fillText(`${leftScore} : ${rightScore}`, canvas.width / 2, 60); if (slowMotion) { ctx.font = '24px monospace'; ctx.fillText('SLOW MOTION', canvas.width / 2, 100); } ctx.restore(); } // Start the game requestAnimationFrame(gameLoop); </script> </body> </html> ``` This implementation includes: 1. Custom vector math library for physics calculations 2. Perlin noise for dynamic background generation 3. Spring-damper systems for paddle deformation 4. Ball spin physics affecting ricochet angles 5. Slow motion mode (hold Shift) 6. Procedural background that changes with score 7. Pure JavaScript canvas rendering 8. Responsive design that adapts to window size Controls: - W/S keys for left paddle -
### Minimalist Pong with Modern Twist Below is a simple implementation of a minimalist Pong game with a modern twist, using HTML5 Canvas and pure JavaScript. The paddles deform based on the ball's velocity, and the background is a dynamic gradient that changes with the score. ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Modern Pong</title> <style> /* Add some basic styling to our canvas */ canvas { border: 1px solid black; } </style> </head> <body> <canvas id="canvas" width="800" height="600"></canvas> <script> // Get the canvas element const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Define some constants const WIDTH = canvas.width; const HEIGHT = canvas.height; const PADDLE_WIDTH = 10; const PADDLE_HEIGHT = 100; const BALL_SIZE = 10; const FPS = 60; // Define some variables let score1 = 0; let score2 = 0; let slowMotion = false; let lastTime = 0; // Define the paddles let paddle1 = { x: 0, y: HEIGHT / 2 - PADDLE_HEIGHT / 2, vy: 0, k: 0.1, // Spring constant c: 0.05, // Damping coefficient deformation: 0 }; let paddle2 = { x: WIDTH - PADDLE_WIDTH, y: HEIGHT / 2 - PADDLE_HEIGHT / 2, vy: 0, k: 0.1, // Spring constant c: 0.05, // Damping coefficient deformation: 0 }; // Define the ball let ball = { x: WIDTH / 2, y: HEIGHT / 2, vx: 5, vy: 3, radius: BALL_SIZE / 2, spin: 0.1 }; // Define some vector math functions function addVectors(v1, v2) { return { x: v1.x + v2.x, y: v1.y + v2.y }; } function subtractVectors(v1, v2) { return { x: v1.x - v2.x, y: v1.y - v2.y }; } function multiplyVector(v, scalar) { return { x: v.x * scalar, y: v.y * scalar }; } function normalizeVector(v) { const magnitude = Math.sqrt(v.x * v.x + v.y * v.y); return { x: v.x / magnitude, y: v.y / magnitude }; } function dotProduct(v1, v2) { return v1.x * v2.x + v1.y * v2.y; } // Define the Perlin noise function function perlinNoise(x, y, seed) { // Simple Perlin noise implementation const rand = (x, y, seed) => { const n = x + y * 57 + seed; return (n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff; }; const floorX = Math.floor(x); const floorY = Math.floor(y); const fractX = x - floorX; const fractY = y - floorY; const v1 = rand(floorX, floorY, seed); const v2 = rand(floorX + 1, floorY, seed); const v3 = rand(floorX, floorY + 1, seed); const v4 = rand(floorX + 1, floorY + 1, seed); const i1 = v1 + (v2 - v1) * fractX; const i2 = v3 + (v4 - v3) * fractX; return (i1 + (i2 - i1) * fractY) / 0x7fffffff; } // Main game loop function update(time) { // Calculate the time difference const dt = (time - lastTime) / 1000; lastTime = time; if (slowMotion) { // Slow down the game by reducing the time step for (let i = 0; i < 10; i++) { updateGame(dt / 10); } } else { updateGame(dt); } // Draw everything draw(); // Request the next frame requestAnimationFrame(update); } // Update the game state function updateGame(dt) { // Update the paddles paddle1.vy += (getPaddleTargetY(1) - paddle1.y - paddle1.deformation / 2 - PADDLE_HEIGHT / 2) * paddle1.k * dt; paddle1.vy *= Math.pow(1 - paddle1.c, dt * 60); paddle1.y += paddle1.vy * dt * 60; paddle2.vy += (getPaddleTargetY(2) - paddle2.y - paddle2.deformation / 2 - PADDLE_HEIGHT / 2) * paddle2.k * dt; paddle2.vy *= Math.pow(1 - paddle2.c, dt * 60); paddle2.y += paddle2.vy * dt * 60; // Deform the paddles based on the ball's velocity if (checkCollision(ball, paddle1)) { paddle1.deformation = -ball.vx * 0.1; } else { paddle1.deformation *= Math.pow(1 - paddle1.c, dt * 60); } if (checkCollision(ball, paddle2)) { paddle2.deformation = ball.vx * 0.1; } else { paddle2.deformation *= Math.pow(1 - paddle2.c, dt * 60); } // Update the ball ball.x += ball.vx * dt * 60; ball.y += ball.vy * dt * 60; // Check for collisions with the walls and paddles if (ball.y < ball.radius) { ball.vy = -ball.vy; } else if (ball.y > HEIGHT - ball.radius) { ball.vy = -ball.vy; } if (checkCollision(ball, paddle1) || checkCollision(ball, paddle2)) { // Calculate the normal vector of the collision const normal = normalizeVector(subtractVectors({ x: ball.x, y: ball.y }, { x: (checkCollision(ball, paddle1) ? paddle1.x + PADDLE_WIDTH : paddle2.x), y: ball.y })); // Calculate the tangent vector of the collision const tangent = { x: -normal.y, y: normal.x }; // Project the ball's velocity onto the tangent and normal vectors const vTangent = dotProduct({ x: ball.vx, y: ball.vy }, tangent); const vNormal = dotProduct({ x: ball.vx, y: ball.vy }, normal); // Apply the spin effect vTangent += ball.spin * (checkCollision(ball, paddle1) ? 1 : -1); // Reflect the ball's velocity around the normal vector ball.vx = vTangent * tangent.x + vNormal * -normal.x; ball.vy = vTangent * tangent.y + vNormal * -normal.y; } // Check for goals if (ball.x < 0) { score2++; resetBall(); } else if (ball.x > WIDTH) { score1++; resetBall(); } } // Draw everything function draw() { // Clear the canvas ctx.clearRect(0, 0, WIDTH, HEIGHT); // Draw the background gradient const gradient = ctx.createLinearGradient(0, 0, WIDTH, HEIGHT); const noise = perlinNoise(score1 + score2, score1 - score2, 42); gradient.addColorStop(0, `hsl(${noise * 360}, 100%, 50%)`); gradient.addColorStop(1, `hsl(${(noise + 0.5) * 360}, 100%, 50%)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, WIDTH, HEIGHT); // Draw the paddles ctx.fillStyle = 'white'; ctx.fillRect(paddle1.x, paddle1.y, PADDLE_WIDTH, PADDLE_HEIGHT + paddle1.deformation); ctx.fillRect(paddle2.x, paddle2.y, PADDLE_WIDTH, PADDLE_HEIGHT + paddle2.deformation); // Draw the ball ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, 2 * Math.PI); ctx.fill(); // Draw the scores ctx.font = '36px Arial'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = 'white'; ctx.fillText(score1, 10, 10); ctx.textAlign = 'right'; ctx.fillText(score2, WIDTH - 10, 10); } // Check if the ball collides with a paddle function checkCollision(ball, paddle) { return ball.x - ball.radius < paddle.x + PADDLE_WIDTH && ball.x + ball.radius > paddle.x && ball.y - ball.radius < paddle.y + PADDLE_HEIGHT + paddle.deformation && ball.y + ball.radius > paddle.y; } // Get the target y position for a paddle function getPaddleTargetY(player) { return player === 1 ? ball.y - PADDLE_HEIGHT / 2 : ball.y - PADDLE_HEIGHT / 2; } // Reset the ball function resetBall() { ball.x = WIDTH / 2; ball.y = HEIGHT / 2; ball.vx = Math.random() < 0.5 ? -5 : 5; ball.vy = Math.random() * 2 - 1; ball.spin = Math.random() * 0.2 - 0.1; } // Handle keyboard input document.addEventListener('keydown', (e) => { if (e.key === ' ') { slowMotion = true; } }); document.addEventListener('keyup', (e) => { if (e.key === ' ') { slowMotion = false; } }); // Start the game loop resetBall(); requestAnimationFrame(update); </script> </body> </html> ``` ### How it works 1. The game loop is implemented using `requestAnimationFrame` to ensure smooth frame rate adjustments. 2. The paddles are updated based on a spring-damper system, which simulates the rubber effect. 3. The ball's velocity is updated based on its spin and the normal vector of the collision with the paddles. 4. The background gradient is generated using Perlin noise, creating a swirling effect based on the score. 5. The game supports slow motion mode when the space bar is pressed. ### Running the code To run the code, simply open the HTML file in a web browser that supports HTML5 Canvas and JavaScript. You can play the game using the default controls (the paddles follow the ball's y position). Press the space bar to toggle slow motion mode.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Modern Pong</title> <style> /* Add some basic styling to our canvas */ canvas { border: 1px solid black; } </style> </head> <body> <canvas id="pongCanvas" width="800" height="600"></canvas> <script> // Get the canvas element const canvas = document.getElementById('pongCanvas'); const ctx = canvas.getContext('2d'); // Define some constants const CANVAS_WIDTH = canvas.width; const CANVAS_HEIGHT = canvas.height; const PADDLE_WIDTH = 10; const PADDLE_HEIGHT = 100; const BALL_SIZE = 10; const SPRING_CONSTANT = 0.1; const DAMPING_CONSTANT = 0.9; const SPIN_EFFECT = 0.05; // Define the Perlin noise function function perlinNoise(x, y, seed) { let random = Math.sin((x + seed) * 1234) * 43758 + Math.sin((y + seed) * 5678) * 3456; return random - Math.floor(random); } // Define the vector math library class Vector { constructor(x, y) { this.x = x; this.y = y; } add(other) { return new Vector(this.x + other.x, this.y + other.y); } subtract(other) { return new Vector(this.x - other.x, this.y - other.y); } multiply(scalar) { return new Vector(this.x * scalar, this.y * scalar); } magnitude() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const magnitude = this.magnitude(); return new Vector(this.x / magnitude, this.y / magnitude); } } // Define the spring-damper system class SpringDamperSystem { constructor(position, velocity, springConstant, dampingConstant) { this.position = position; this.velocity = velocity; this.springConstant = springConstant; this.dampingConstant = dampingConstant; } update() { const force = this.springConstant * this.position; this.velocity += force; this.velocity *= this.dampingConstant; this.position += this.velocity; } } // Define the paddle class Paddle { constructor(x, y) { this.x = x; this.y = y; this.velocity = 0; this.springDamperSystem = new SpringDamperSystem(0, 0, SPRING_CONSTANT, DAMPING_CONSTANT); } update() { this.springDamperSystem.update(); this.y += this.velocity; } draw() { ctx.fillStyle = 'white'; ctx.fillRect(this.x, this.y, PADDLE_WIDTH, PADDLE_HEIGHT); // Draw the deformed paddle ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo(this.x + PADDLE_WIDTH, this.y + this.springDamperSystem.position); ctx.lineTo(this.x + PADDLE_WIDTH, this.y + PADDLE_HEIGHT); ctx.lineTo(this.x, this.y + PADDLE_HEIGHT); ctx.fillStyle = 'white'; ctx.fill(); } } // Define the ball class Ball { constructor(x, y) { this.x = x; this.y = y; this.velocity = new Vector(2, 2); this.spin = 0; } update() { this.x += this.velocity.x; this.y += this.velocity.y; this.spin += SPIN_EFFECT; this.velocity = this.velocity.add(new Vector(Math.sin(this.spin), Math.cos(this.spin)).multiply(0.01)); } draw() { ctx.fillStyle = 'white'; ctx.fillRect(this.x, this.y, BALL_SIZE, BALL_SIZE); } } // Define the game class Game { constructor() { this.paddle1 = new Paddle(10, CANVAS_HEIGHT / 2 - PADDLE_HEIGHT / 2); this.paddle2 = new Paddle(CANVAS_WIDTH - 20, CANVAS_HEIGHT / 2 - PADDLE_HEIGHT / 2); this.ball = new Ball(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); this.score1 = 0; this.score2 = 0; this.slowMotion = false; } update() { this.paddle1.update(); this.paddle2.update(); this.ball.update(); // Collision with paddles if (this.ball.x < this.paddle1.x + PADDLE_WIDTH && this.ball.y > this.paddle1.y && this.ball.y < this.paddle1.y + PADDLE_HEIGHT) { this.ball.velocity = this.ball.velocity.multiply(-1); this.paddle1.springDamperSystem.position = this.ball.velocity.magnitude(); } if (this.ball.x > this.paddle2.x - BALL_SIZE && this.ball.y > this.paddle2.y && this.ball.y < this.paddle2.y + PADDLE_HEIGHT) { this.ball.velocity = this.ball.velocity.multiply(-1); this.paddle2.springDamperSystem.position = this.ball.velocity.magnitude(); } // Collision with walls if (this.ball.y < 0 || this.ball.y > CANVAS_HEIGHT - BALL_SIZE) { this.ball.velocity = this.ball.velocity.multiply(-1); } // Scoring if (this.ball.x < 0) { this.score2++; this.ball = new Ball(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); } if (this.ball.x > CANVAS_WIDTH - BALL_SIZE) { this.score1++; this.ball = new Ball(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); } } draw() { // Draw the background ctx.fillStyle = `hsl(${this.score1 * 10}, 100%, 50%)`; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // Draw the noise background for (let x = 0; x < CANVAS_WIDTH; x++) { for (let y = 0; y < CANVAS_HEIGHT; y++) { const noise = perlinNoise(x / 100, y / 100, this.score1 + this.score2); ctx.fillStyle = `hsl(${noise * 360}, 100%, 50%)`; ctx.fillRect(x, y, 1, 1); } } // Draw the paddles this.paddle1.draw(); this.paddle2.draw(); // Draw the ball this.ball.draw(); // Draw the score ctx.fillStyle = 'white'; ctx.font = '24px Arial'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText(`Score: ${this.score1}`, 10, 10); ctx.textAlign = 'right'; ctx.fillText(`Score: ${this.score2}`, CANVAS_WIDTH - 10, 10); } } // Create a new game const game = new Game(); // Main loop function loop() { game.update(); ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); game.draw(); // Slow motion mode if (game.slowMotion) { setTimeout(loop, 1000 / 30); } else { requestAnimationFrame(loop); } } // Handle keyboard input document.addEventListener('keydown', (e) => { if (e.key === ' ') { game.slowMotion = !game.slowMotion; } if (e.key === 'w') { game.paddle1.velocity = -5; } if (e.key === 's') { game.paddle1.velocity = 5; } if (e.key === 'ArrowUp') { game.paddle2.velocity = -5; } if (e.key === 'ArrowDown') { game.paddle2.velocity = 5; } }); document.addEventListener('keyup', (e) => { if (e.key === 'w' || e.key === 's') { game.paddle1.velocity = 0; } if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { game.paddle2.velocity = 0; } }); // Start the game loop(); </script> </body> </html> ```
# Minimalist Modern Pong Implementation Here's a complete HTML file implementing a minimalist Pong game with modern physics effects: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Modern Pong</title> <style> body { margin: 0; padding: 0; background-color: #000; overflow: hidden; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Arial', sans-serif; } canvas { box-shadow: 0 0 30px rgba(100, 200, 255, 0.5); } #info { position: absolute; bottom: 20px; color: rgba(255, 255, 255, 0.7); font-size: 14px; } </style> </head> <body> <canvas id="gameCanvas" width="800" height="500"></canvas> <div id="info">Hold SPACE for slow motion | W/S and Up/Down Arrow keys to move paddles</div> <script> // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // Game constants const PADDLE_WIDTH = 15; const PADDLE_HEIGHT = 120; const PADDLE_MARGIN = 30; const BALL_RADIUS = 12; const INITIAL_BALL_SPEED = 5; const MAX_BALL_SPEED = 15; const PADDLE_SPEED = 8; const SPIN_FACTOR = 0.5; const DEFORM_INTENSITY = 0.3; const SPRING_STIFFNESS = 200; const SPRING_DAMPING = 15; // Game state let scoreLeft = 0; let scoreRight = 0; let slowMo = false; let keys = {}; // Particle system for background const particles = []; const PARTICLE_COUNT = 200; // Custom vector library class Vector { constructor(x = 0, y = 0) { this.x = x; this.y = y; } add(vec) { return new Vector(this.x + vec.x, this.y + vec.y); } sub(vec) { return new Vector(this.x - vec.x, this.y - vec.y); } mul(scalar) { return new Vector(this.x * scalar, this.y * scalar); } magnitude() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const mag = this.magnitude(); return mag !== 0 ? new Vector(this.x / mag, this.y / mag) : new Vector(); } dot(vec) { return this.x * vec.x + this.y * vec.y; } reflect(normal) { const dot = this.dot(normal); return this.sub(normal.mul(2 * dot)); } rotate(angle) { const cos = Math.cos(angle); const sin = Math.sin(angle); return new Vector( this.x * cos - this.y * sin, this.x * sin + this.y * cos ); } } // Perlin noise implementation for procedural background class PerlinNoise { constructor() { this.gradient = {}; } randVector(x, y) { const angle = Math.cos(x * 12.9898 + y * 78.233) * 43758.5453 % Math.PI * 2; return new Vector(Math.cos(angle), Math.sin(angle)); } dotGridGradient(ix, iy, x, y) { const grad = this.getGradient(ix, iy); const dx = x - ix; const dy = y - iy; return dx * grad.x + dy * grad.y; } lerp(a, b, t) { return a + t * (b - a); } getGradient(ix, iy) { const key = `${ix},${iy}`; let grad = this.gradient[key]; if (!grad) { grad = this.randVector(ix, iy); this.gradient[key] = grad; } return grad; } noise(x, y) { const x0 = Math.floor(x); const x1 = x0 + 1; const y0 = Math.floor(y); const y1 = y0 + 1; const sx = x - x0; const sy = y - y0; const n0 = this.dotGridGradient(x0, y0, x, y); const n1 = this.dotGridGradient(x1, y0, x, y); const ix0 = this.lerp(n0, n1, sx); const n2 = this.dotGridGradient(x0, y1, x, y); const n3 = this.dotGridGradient(x1, y1, x, y); const ix1 = this.lerp(n2, n3, sx); return this.lerp(ix0, ix1, sy); } fractal(x, y, octaves, persistence) { let total = 0; let frequency = 1; let amplitude = 1; let maxValue = 0; for(let i = 0; i < octaves; i++) { total += this.noise(x * frequency, y * frequency) * amplitude; maxValue += amplitude; amplitude *= persistence; frequency *= 2; } return total / maxValue; } } const perlinNoise = new PerlinNoise(); // Paddle with deformable physics class Paddle { constructor(x, isLeft) { this.x = x; this.y = canvas.height / 2 - PADDLE_HEIGHT / 2; this.width = PADDLE_WIDTH; this.height = PADDLE_HEIGHT; this.isLeft = isLeft; this.targetHeight = PADDLE_HEIGHT; this.currentHeight = PADDLE_HEIGHT; this.springVelocity = 0; this.color = isLeft ? '#63e3ff' : '#ff66a7'; } update(height) { const force = SPRING_STIFFNESS * (height - this.currentHeight) - SPRING_DAMPING * this.springVelocity; this.springVelocity += force * 0.016; this.currentHeight += this.springVelocity * 0.016; } draw() { ctx.fillStyle = this.color; const deformDelta = this.height - this.currentHeight; ctx.fillRect( this.x, this.y + deformDelta / 2, this.width, this.currentHeight ); // Draw paddle reflections ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; ctx.fillRect(this.x - 1, this.y + deformDelta / 2 + 5, 2, this.currentHeight - 10); } } // Ball with physics class Ball { constructor() { this.radius = BALL_RADIUS; this.pos = new Vector(canvas.width/2, canvas.height/2); this.vel = new Vector(); this.spin = 0; this.reset(); this.trail = []; this.maxTrail = 15; } reset() { this.pos = new Vector(canvas.width/2, canvas.height/2); const angle = (Math.random() * Math.PI/3) - Math.PI/6 + (Math.random() > 0.5 ? Math.PI : 0); this.spin = 0; const speed = INITIAL_BALL_SPEED * (1 + Math.random() * 0.2); this.vel.x = Math.cos(angle) * speed; this.vel.y = Math.sin(angle) * speed; this.trail = []; } update() { // Save position for trail this.trail.push({x: this.pos.x, y: this.pos.y}); if (this.trail.length > this.maxTrail) { this.trail.shift(); } // Update position this.pos = this.pos.add(this.vel); // Handle wall collisions if (this.pos.y <= this.radius) { this.pos.y = this.radius; this.vel.y = -this.vel.y; } else if (this.pos.y >= canvas.height - this.radius) { this.pos.y = canvas.height - this.radius; this.vel.y = -this.vel.y; } // Gradually reduce spin this.spin *= 0.99; // Ensure ball doesn't move too fast const speed = this.vel.magnitude(); if (speed > MAX_BALL_SPEED) { this.vel = this.vel.normalize().mul(MAX_BALL_SPEED); } } draw() { // Draw trail ctx.fillStyle = 'rgba(200, 230, 255, 0.85)'; this.trail.forEach((pos, i) => { const alpha = i / this.trail.length; const radius = this.radius * alpha; ctx.beginPath(); ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2); ctx.fill(); }); // Draw ball ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2); ctx.fill(); // Draw spin indicator ctx.strokeStyle = '#63e3ff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.radius * 0.7, 0, Math.PI * Math.max(Math.min(Math.abs(this.spin)*12, 2), 0.2) * (this.spin > 0 ? 1 : -1)); ctx.stroke(); } } // Initialize game objects const leftPaddle = new Paddle(PADDLE_MARGIN, true); const rightPaddle = new Paddle(canvas.width - PADDLE_MARGIN - PADDLE_WIDTH, false); const ball = new Ball(); // Initialize background particles for (let i = 0; i < PARTICLE_COUNT; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: Math.random() * 3 + 1, speed: Math.random() * 0.5 + 0.1 }); } // Handle keyboard input window.addEventListener('keydown', (e) => keys[e.key] = true); window.addEventListener('keyup', (e) => keys[e.key] = false); // Draw procedural swirling background function drawBackground() { const timestamp = Date.now() * 0.001; const scoreFactor = (scoreLeft + scoreRight) * 0.1; // Create gradient based on score const bgGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); // Colors based on player scores const hueLeft = 180 + scoreLeft * 5; const hueRight = 340 - scoreRight * 3; bgGradient.addColorStop(0, `hsl(${hueLeft}, 80%, 10%)`); bgGradient.addColorStop(0.35, `hsl(${((hueLeft + hueRight) / 2)}, 85%, 5%)`); bgGradient.addColorStop(1, `hsl(${hueRight}, 80%, 10%)`); ctx.fillStyle = bgGradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw noise-based swirling effect ctx.globalCompositeOperation = 'soft-light'; ctx.fillStyle = 'rgba(200, 230, 255, 0.05)'; const noiseScale = 0.02; const noiseOffsetX = timestamp * 0.5; const noiseOffsetY = timestamp * 0.3; // Create noise field effect for (let y = 0; y < canvas.height; y += 20) { for (let x = 0; x < canvas.width; x += 20) { const noiseVal = perlinNoise.fractal( (x + noiseOffsetX) * noiseScale, (y + noiseOffsetY) * noiseScale, 3, 0.5 ); const rotation = noiseVal * Math.PI * 2; const intensity = (noiseVal * 0.5 + 0.5) * 15; ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); ctx.fillRect(0, 0, intensity, 1); ctx.restore(); } } ctx.globalCompositeOperation = 'source-over'; // Draw particles ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; particles.forEach(p => { p.x = (p.x + p.speed) % canvas.width; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); }); // Draw court lines ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(canvas.width / 2, 10); ctx.lineTo(canvas.width / 2, canvas.height - 10); ctx.setLineDash([10, 15]); ctx.stroke(); ctx.setLineDash([]); // Draw scores ctx.font = '80px Orbitron, sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = leftPaddle.color; ctx.shadowBlur = 30; ctx.fillStyle = 'white'; ctx.fillText(scoreLeft.toString(), canvas.width/4, 90); ctx.shadowColor = rightPaddle.color; ctx.fillText(scoreRight.toString(), canvas.width*3/4, 90); ctx.shadowBlur = 0; } // Update game logic function updateGame(delta) { // Handle slow-motion toggle slowMo = keys[' ']; const timeFactor = slowMo ? 0.2 : 1; const dt = delta * timeFactor; // Update paddles based on keyboard input if (keys['w'] || keys['W']) leftPaddle.y = Math.max(0, leftPaddle.y - PADDLE_SPEED * dt); if (keys['s'] || keys['S']) leftPaddle.y = Math.min(canvas.height - leftPaddle.height, leftPaddle.y + PADDLE_SPEED * dt); if (keys['ArrowUp']) rightPaddle.y = Math.max(0, rightPaddle.y - PADDLE_SPEED * dt); if (keys['ArrowDown']) rightPaddle.y = Math.min(canvas.height - rightPaddle.height, rightPaddle.y + PADDLE_SPEED * dt); // Update ball position ball.update(); // Handle ball-paddle collisions with ball spin effect function checkCollision(paddle) { const halfWidth = paddle.width / 2; const halfHeight = paddle.height / 2; const paddleCenterX = paddle.x + halfWidth; const paddleCenterY = paddle.y + halfHeight; // Calculate relative positions const diffX = ball.pos.x - paddleCenterX; const diffY = ball.pos.y - paddleCenterY; // Collision detection if (Math.abs(diffX) <= ball.radius + halfWidth && Math.abs(diffY) <= ball.radius + halfHeight) { const paddleAngle = (diffY / halfHeight) * Math.PI/4; // Apply deformation effect paddle.targetHeight = PADDLE_HEIGHT * (1 - DEFORM_INTENSITY * ball.vel.magnitude()/MAX_BALL_SPEED); let normal; if (paddle.isLeft) { normal = new Vector(1, paddleAngle); } else { normal = new Vector(-1, paddleAngle); } // Original bounce vector const originalVelVector = ball.vel.normalize(); // Calculate outgoing velocity with ball spin effect const spinEffect = ball.spin * SPIN_FACTOR; const newVelX = ball.vel.x > 0 ? -ball.vel.x : -ball.vel.x * 0.95; ball.vel = normal.add(new Vector(spinEffect, paddleAngle)).normalize() .mul(Math.max(ball.vel.magnitude(), INITIAL_BALL_SPEED)); // Apply spin based on impact position if (Math.abs(ball.spin) < 1) { ball.spin += paddleAngle * 1.5; } // Visual feedback ctx.fillStyle = paddle.color; ctx.globalAlpha = 0.3; ctx.beginPath(); ctx.arc(ball.pos.x, ball.pos.y, ball.radius * 2, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1.0; return true; } paddle.targetHeight = PADDLE_HEIGHT; return false; } // Check collisions if (checkCollision(leftPaddle) || checkCollision(rightPaddle)) { paddleHit = true; } // Update paddle physics leftPaddle.update(leftPaddle.targetHeight); rightPaddle.update(rightPaddle.targetHeight); // Handle scoring if (ball.pos.x < 0) { scoreRight++; ball.reset(); } else if (ball.pos.x > canvas.width) { scoreLeft++; ball.reset(); } } // Draw center message function drawCenterMessage() { ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; ctx.font = '14px Arial'; ctx.textAlign = 'center'; ctx.fillText('MINIMALIST PONG', canvas.width/2, canvas.height - 30); if (slowMo) { ctx.fillStyle = '#ff893b'; ctx.font = '26px Orbitron'; ctx.fillText('SLOW MOTION ACTIVATED', canvas.width/2, canvas.height/2); } } // Main game loop let lastTimestamp = 0; function gameLoop(timestamp) { const delta = timestamp - lastTimestamp; lastTimestamp = timestamp; // Update and draw updateGame(delta / (1000/60)); ctx.clearRect(0, 0, canvas.width, canvas.height); drawBackground(); leftPaddle.draw(); rightPaddle.draw(); ball.draw(); drawCenterMessage(); requestAnimationFrame(gameLoop); } // Start the game gameLoop(0); </script> </body> </html> ``` ## Features Implemented: 1. **Minimalist Visual Design**: Clean UI with futuristic aesthetics 2. **Rubber Paddle Physics**: Paddles deform dynamically based on ball velocity using custom spring-damper physics 3. **Dynamic Gradient Background**: Changes colors based on player scores 4. **Perlin Noise Swirl**: Procedurally generated swirling background effect 5. **Ball Spin Physics**: Implemented angular momentum that affects bounce angles 6. **Slow Motion**: Hold spacebar to activate slow motion mode 7. **Visual Effects**: Ball trails, particle systems, and collision feedback 8. **Vector Math Library**: Custom physics calculations without dependencies Use W/S keys for left paddle movement and Up/Down arrows for the right paddle. Hold SPACE for slow motion effect. The game scores are displayed prominently at the top of each side.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Transcendent Pong</title> <style> body { margin: 0; padding: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Courier New', monospace; overflow: hidden; } canvas { border: 2px solid #333; box-shadow: 0 0 50px rgba(255, 255, 255, 0.1); } .ui { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); color: white; font-size: 24px; z-index: 10; } .controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #888; font-size: 12px; text-align: center; z-index: 10; } </style> </head> <body> <div class="ui"> <div id="score">0 : 0</div> </div> <div class="controls"> W/S: Player 1 | ↑/↓: Player 2 | SPACE: Slow Motion </div> <canvas id="gameCanvas" width="800" height="400"></canvas> <script> // Vector math library class Vector2 { constructor(x = 0, y = 0) { this.x = x; this.y = y; } static add(v1, v2) { return new Vector2(v1.x + v2.x, v1.y + v2.y); } static subtract(v1, v2) { return new Vector2(v1.x - v2.x, v1.y - v2.y); } static multiply(v, scalar) { return new Vector2(v.x * scalar, v.y * scalar); } static dot(v1, v2) { return v1.x * v2.x + v1.y * v2.y; } static normalize(v) { const mag = Math.sqrt(v.x * v.x + v.y * v.y); return mag > 0 ? new Vector2(v.x / mag, v.y / mag) : new Vector2(0, 0); } static magnitude(v) { return Math.sqrt(v.x * v.x + v.y * v.y); } static reflect(v, normal) { const dotProduct = Vector2.dot(v, normal); return Vector2.subtract(v, Vector2.multiply(normal, 2 * dotProduct)); } copy() { return new Vector2(this.x, this.y); } } // Perlin noise implementation class PerlinNoise { constructor() { this.gradients = {}; this.memory = {}; } rand_vect() { const theta = Math.random() * 2 * Math.PI; return new Vector2(Math.cos(theta), Math.sin(theta)); } dot_prod_grid(x, y, vx, vy) { const hash = `${x},${y}`; let g_vect; if (this.gradients[hash]) { g_vect = this.gradients[hash]; } else { g_vect = this.rand_vect(); this.gradients[hash] = g_vect; } const d_vect = new Vector2(x - vx, y - vy); return Vector2.dot(d_vect, g_vect); } smootherstep(t) { return t * t * t * (t * (t * 6 - 15) + 10); } interp(x, a, b) { return a + this.smootherstep(x) * (b - a); } noise(x, y) { const xf = Math.floor(x); const yf = Math.floor(y); const tl = this.dot_prod_grid(xf, yf, x, y); const tr = this.dot_prod_grid(xf + 1, yf, x, y); const bl = this.dot_prod_grid(xf, yf + 1, x, y); const br = this.dot_prod_grid(xf + 1, yf + 1, x, y); const xt = this.interp(x - xf, tl, tr); const xb = this.interp(x - xf, bl, br); const v = this.interp(y - yf, xt, xb); return v; } } // Spring-damper system for paddle deformation class SpringDamper { constructor(stiffness = 0.8, damping = 0.6) { this.stiffness = stiffness; this.damping = damping; this.position = 0; this.velocity = 0; this.target = 0; } update(deltaTime) { const force = (this.target - this.position) * this.stiffness - this.velocity * this.damping; this.velocity += force * deltaTime; this.position += this.velocity * deltaTime; } setTarget(target) { this.target = target; } } // Ball class with spin physics class Ball { constructor(x, y, radius) { this.position = new Vector2(x, y); this.velocity = new Vector2(0, 0); this.radius = radius; this.spin = 0; // Angular velocity this.maxSpin = 0.1; this.trail = []; this.maxTrailLength = 10; } update(deltaTime) { this.position = Vector2.add(this.position, Vector2.multiply(this.velocity, deltaTime)); // Add position to trail this.trail.push(this.position.copy()); if (this.trail.length > this.maxTrailLength) { this.trail.shift(); } // Decay spin over time this.spin *= 0.999; } reflectWithSpin(normal, impactVelocity) { // Apply spin effect to reflection const spinEffect = new Vector2(-normal.y * this.spin, normal.x * this.spin); const newVelocity = Vector2.reflect(this.velocity, normal); this.velocity = Vector2.add(newVelocity, Vector2.multiply(spinEffect, 0.3)); // Add spin based on impact this.spin += Vector2.magnitude(impactVelocity) * 0.001; this.spin = Math.max(-this.maxSpin, Math.min(this.maxSpin, this.spin)); } render(ctx) { // Render trail for (let i = 0; i < this.trail.length; i++) { const alpha = (i + 1) / this.trail.length * 0.3; const size = this.radius * alpha; ctx.save(); ctx.globalAlpha = alpha; ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(this.trail[i].x, this.trail[i].y, size, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } // Render ball with spin visualization ctx.save(); ctx.translate(this.position.x, this.position.y); ctx.rotate(performance.now() * this.spin * 0.01); const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.radius); gradient.addColorStop(0, '#ffffff'); gradient.addColorStop(0.7, '#cccccc'); gradient.addColorStop(1, '#999999'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI * 2); ctx.fill(); // Spin indicator ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(this.radius * 0.7, 0); ctx.stroke(); ctx.restore(); } } // Paddle class with deformation class Paddle { constructor(x, y, width, height) { this.position = new Vector2(x, y); this.width = width; this.height = height; this.velocity = new Vector2(0, 0); this.deformation = new SpringDamper(0.5, 0.7); this.speed = 300; } update(deltaTime) { this.deformation.update(deltaTime); this.position = Vector2.add(this.position, Vector2.multiply(this.velocity, deltaTime)); // Keep paddle on screen this.position.y = Math.max(this.height / 2, Math.min(400 - this.height / 2, this.position.y)); } onBallHit(ballVelocity) { const impactForce = Vector2.magnitude(ballVelocity) * 0.1; this.deformation.setTarget(impactForce); } render(ctx) { const deform = this.deformation.position; ctx.save(); ctx.translate(this.position.x, this.position.y); // Create deformed paddle shape const gradient = ctx.createLinearGradient(-this.width/2, 0, this.width/2, 0); gradient.addColorStop(0, '#666'); gradient.addColorStop(0.5, '#fff'); gradient.addColorStop(1, '#666'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.moveTo(-this.width/2, -this.height/2); ctx.bezierCurveTo(-this.width/2 - deform, -this.height/4, -this.width/2 - deform, this.height/4, -this.width/2, this.height/2); ctx.lineTo(this.width/2, this.height/2); ctx.bezierCurveTo(this.width/2 + deform, this.height/4, this.width/2 + deform, -this.height/4, this.width/2, -this.height/2); ctx.closePath(); ctx.fill(); ctx.restore(); } } // Main game class class TranscendentPong { constructor() { this.canvas = document.getElementById('gameCanvas'); this.ctx = this.canvas.getContext('2d'); this.noise = new PerlinNoise(); this.timeOffset = 0; // Game objects this.ball = new Ball(400, 200, 8); this.paddle1 = new Paddle(30, 200, 15, 80); this.paddle2 = new Paddle(770, 200, 15, 80); // Game state this.score = { player1: 0, player2: 0 }; this.slowMotion = false; this.timeScale = 1.0; this.targetTimeScale = 1.0; // Input handling this.keys = {}; this.setupInput(); // Start game this.resetBall(); this.lastTime = performance.now(); this.gameLoop(); } setupInput() { document.addEventListener('keydown', (e) => { this.keys[e.code] = true; if (e.code === 'Space') { e.preventDefault(); this.targetTimeScale = 0.3; } }); document.addEventListener('keyup', (e) => { this.keys[e.code] = false; if (e.code === 'Space') { this.targetTimeScale = 1.0; } }); } resetBall() { this.ball.position = new Vector2(400, 200); const angle = (Math.random() - 0.5) * Math.PI / 3; const speed = 200; this.ball.velocity = new Vector2( Math.cos(angle) * speed * (Math.random() > 0.5 ? 1 : -1), Math.sin(angle) * speed ); this.ball.spin = 0; } update(deltaTime) { // Smooth time scale transition this.timeScale += (this.targetTimeScale - this.timeScale) * deltaTime * 5; const adjustedDeltaTime = deltaTime * this.timeScale; // Handle input if (this.keys['KeyW']) this.paddle1.velocity.y = -this.paddle1.speed; else if (this.keys['KeyS']) this.paddle1.velocity.y = this.paddle1.speed; else this.paddle1.velocity.y = 0; if (this.keys['ArrowUp']) this.paddle2.velocity.y = -this.paddle2.speed; else if (this.keys['ArrowDown']) this.paddle2.velocity.y = this.paddle2.speed; else this.paddle2.velocity.y = 0; // Update game objects this.ball.update(adjustedDeltaTime); this.paddle1.update(adjustedDeltaTime); this.paddle2.update(adjustedDeltaTime); // Ball collision with walls if (this.ball.position.y <= this.ball.radius || this.ball.position.y >= 400 - this.ball.radius) { this.ball.velocity.y *= -1; this.ball.reflectWithSpin(new Vector2(0, this.ball.position.y <= this.ball.radius ? 1 : -1), this.ball.velocity); } // Ball collision with paddles this.checkPaddleCollision(this.paddle1); this.checkPaddleCollision(this.paddle2); // Scoring if (this.ball.position.x < 0) { this.score.player2++; this.resetBall(); } else if (this.ball.position.x > 800) { this.score.player1++; this.resetBall(); } this.timeOffset += adjustedDeltaTime; } checkPaddleCollision(paddle) { const dx = this.ball.position.x - Math.max(paddle.position.x - paddle.width/2, Math.min(this.ball.position.x, paddle.position.x + paddle.width/2)); const dy = this.ball.position.y - Math.max(paddle.position.y - paddle.height/2, Math.min(this.ball.position.y, paddle.position.y + paddle.height/2)); if (dx * dx + dy * dy < this.ball.radius * this.ball.radius) { const normal = Vector2.normalize(Vector2.subtract(this.ball.position, paddle.position)); this.ball.reflectWithSpin(normal, paddle.velocity); paddle.onBallHit(this.ball.velocity); // Move ball out of collision this.ball.position = Vector2.add(paddle.position, Vector2.multiply(normal, this.ball.radius + paddle.width/2 + 1)); } } renderBackground() { const totalScore = this.score.player1 + this.score.player2; // Create dynamic gradient based on score const imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height); const data = imageData.data; for (let x = 0; x < this.canvas.width; x += 2) { for (let y = 0; y < this.canvas.height; y += 2) { const noiseX = x * 0.01 + this.timeOffset * 0.5; const noiseY = y * 0.01 + totalScore * 0.1; const noise = this.noise.noise(noiseX, noiseY); const intensity = (noise + 1) * 0.5; const r = Math.floor(intensity * (50 + totalScore * 10)); const g = Math.floor(intensity * (20 + totalScore * 5)); const b = Math.floor(intensity * (80 + totalScore * 15)); const index = (y * this.canvas.width + x) * 4; data[index] = r; data[index + 1] = g; data[index + 2] = b; data[index + 3] = 255; // Fill adjacent pixels for performance if (x + 1 < this.canvas.width) { const index2 = (y * this.canvas.width + x + 1) * 4; data[index2] = r; data[index2 + 1] = g; data[index2 + 2] = b; data[index2 + 3] = 255; } if (y + 1 < this.canvas.height) { const index3 = ((y + 1) * this.canvas.width + x) * 4; data[index3] = r; data[index3 + 1] = g; data[index3 + 2] = b; data[index3 + 3] = 255; if (x + 1 < this.canvas.width) { const index4 = ((y + 1) * this.canvas.width + x + 1) * 4; data[index4] = r; data[index4 + 1] = g; data[index4 + 2] = b; data[index4 + 3] = 255; } } } } this.ctx.putImageData(imageData, 0, 0); // Add center line with dynamic opacity const lineOpacity = 0.3 + Math.sin(this.timeOffset * 2) * 0.1; this.ctx.save(); this.ctx.globalAlpha = lineOpacity; this.ctx.strokeStyle = '#fff'; this.ctx.lineWidth = 2; this.ctx.setLineDash([10, 10]); this.ctx.beginPath(); this.ctx.moveTo(400, 0); this.ctx.lineTo(400, 400); this.ctx.stroke(); this.ctx.restore(); } render() { // Clear canvas this.ctx.fillStyle = '#000'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Render dynamic background this.renderBackground(); // Render game objects this.paddle1.render(this.ctx); this.paddle2.render(this.ctx); this.ball.render(this.ctx); // Render slow motion effect if (this.timeScale < 0.8) { this.ctx.save(); this.ctx.globalAlpha = 0.3; this.ctx.fillStyle = '#0088ff'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); } // Update score display document.getElementById('score').textContent = `${this.score.player1} : ${this.score.player2}`; } gameLoop() { const currentTime = performance.now(); const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.016); this.lastTime = currentTime; this.update(deltaTime); this.render(); requestAnimationFrame(() => this.gameLoop()); } } // Start the game when page loads window.addEventListener('load', () => { new TranscendentPong(); }); </script> </body> </html> ``` This transcendent version of Pong includes all the requested features: 1. **Spring-damper paddle deformation**: Paddles deform when hit by the ball using realistic physics 2. **Dynamic gradient background**: Uses Perlin noise to create swirling effects that change with the score 3. **Ball spin physics**: Ball has angular velocity that affects reflection angles 4. **Custom vector math library**: Complete Vector2 class with all necessary operations 5. **Slow motion mode**: Hold SPACE to activate smooth slow-motion effect 6. **Modern visual effects**: Ball trails, gradient rendering, and smooth animations The game combines classic Pong mechanics with modern physics simulation and procedural graphics, creating a truly transcendent gaming experience that goes far beyond the original while maintaining its essential simplicity.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Transcendent Pong</title> <style> body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #000; overflow: hidden; } canvas { border: 2px solid #333; box-shadow: 0 0 20px rgba(255, 255, 255, 0.1); } #instructions { position: absolute; bottom: 10px; color: #666; font-family: Arial, sans-serif; font-size: 14px; text-align: center; width: 100%; } </style> </head> <body> <canvas id="gameCanvas" width="800" height="500"></canvas> <div id="instructions">Use W/S for left paddle, Arrow Up/Down for right paddle. Hold SPACE for slow motion.</div> <script> // Main Canvas Setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; // Game Variables let leftScore = 0; let rightScore = 0; let slowMotion = false; let lastTime = 0; let deltaTime = 0; // Custom Vector Math Library class Vector2 { constructor(x, y) { this.x = x; this.y = y; } add(v) { return new Vector2(this.x + v.x, this.y + v.y); } subtract(v) { return new Vector2(this.x - v.x, this.y - v.y); } multiply(scalar) { return new Vector2(this.x * scalar, this.y * scalar); } dot(v) { return this.x * v.x + this.y * v.y; } length() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const len = this.length(); if (len === 0) return new Vector2(0, 0); return new Vector2(this.x / len, this.y / len); } reflect(normal) { const normalized = normal.normalize(); const dot2 = 2 * this.dot(normalized); return this.subtract(normalized.multiply(dot2)); } } // Perlin Noise Implementation (Simplified) class PerlinNoise { constructor() { this.gradients = {}; this.memory = {}; } random_gradient(ix, iy) { const key = `${ix},${iy}`; if (!this.gradients[key]) { const angle = Math.random() * 2 * Math.PI; this.gradients[key] = new Vector2(Math.cos(angle), Math.sin(angle)); } return this.gradients[key]; } dot_grid_gradient(ix, iy, x, y) { const gradient = this.random_gradient(ix, iy); const dx = x - ix; const dy = y - iy; return dx * gradient.x + dy * gradient.y; } smoothstep(a0, a1, w) { return w * w * (3 - 2 * w) * (a1 - a0) + a0; } noise(x, y) { const key = `${x},${y}`; if (this.memory[key]) { return this.memory[key]; } const x0 = Math.floor(x); const x1 = x0 + 1; const y0 = Math.floor(y); const y1 = y0 + 1; const sx = x - x0; const sy = y - y0; const n0 = this.dot_grid_gradient(x0, y0, x, y); const n1 = this.dot_grid_gradient(x1, y0, x, y); const ix0 = this.smoothstep(n0, n1, sx); const n2 = this.dot_grid_gradient(x0, y1, x, y); const n3 = this.dot_grid_gradient(x1, y1, x, y); const ix1 = this.smoothstep(n2, n3, sx); this.memory[key] = this.smoothstep(ix0, ix1, sy); return this.memory[key]; } getValue(x, y, scale) { return (this.noise(x * scale, y * scale) + 1) * 0.5; } } // Paddle with spring-damper system class Paddle { constructor(x, y, width, height, controlUp, controlDown) { this.pos = new Vector2(x, y); this.baseWidth = width; this.width = width; this.baseHeight = height; this.height = height; this.deformations = { top: 0, bottom: 0, side: 0 }; this.speed = 450; this.controlUp = controlUp; this.controlDown = controlDown; // Spring-damper parameters this.stiffness = 500; this.damping = 15; this.velocities = { top: 0, bottom: 0, side: 0 }; } update(deltaTime, keys) { // Movement if (keys[this.controlUp]) { this.pos.y -= this.speed * deltaTime; } if (keys[this.controlDown]) { this.pos.y += this.speed * deltaTime; } // Constrain position this.pos.y = Math.max(this.height / 2, Math.min(height - this.height / 2, this.pos.y)); // Apply spring physics to deformations this.updateDeformation('top', deltaTime); this.updateDeformation('bottom', deltaTime); this.updateDeformation('side', deltaTime); } updateDeformation(part, deltaTime) { // F = -kx - cv const force = -this.stiffness * this.deformations[part] - this.damping * this.velocities[part]; const acceleration = force; // Update velocity and position this.velocities[part] += acceleration * deltaTime; this.deformations[part] += this.velocities[part] * deltaTime; } applyImpact(impact, position) { // Apply impact force to the appropriate part of the paddle const relativeY = position.y - this.pos.y; const relativePosY = relativeY / (this.height / 2); if (relativePosY < -0.5) { this.deformations.top += impact * 0.3; this.velocities.top += impact * 3; } else if (relativePosY > 0.5) { this.deformations.bottom += impact * 0.3; this.velocities.bottom += impact * 3; } this.deformations.side += impact * 0.5; this.velocities.side += impact * 5; } draw(ctx) { // Calculate deformed shape const sideOffset = Math.max(-this.baseWidth / 2, this.deformations.side); const deformedWidth = this.baseWidth + sideOffset; const topOffset = Math.max(-this.baseHeight / 4, this.deformations.top); const bottomOffset = Math.max(-this.baseHeight / 4, this.deformations.bottom); // Draw the deformed paddle ctx.beginPath(); ctx.moveTo(this.pos.x - deformedWidth / 2, this.pos.y - this.height / 2 + topOffset); ctx.lineTo(this.pos.x + deformedWidth / 2, this.pos.y - this.height / 2); ctx.lineTo(this.pos.x + deformedWidth / 2, this.pos.y + this.height / 2); ctx.lineTo(this.pos.x - deformedWidth / 2, this.pos.y + this.height / 2 + bottomOffset); ctx.closePath(); ctx.fillStyle = 'white'; ctx.fill(); } getRect() { return { x: this.pos.x - this.width / 2, y: this.pos.y - this.height / 2, width: this.width, height: this.height, right: this.pos.x + this.width / 2, bottom: this.pos.y + this.height / 2 }; } } // Ball with spin effect class Ball { constructor() { this.reset(); this.radius = 10; this.maxSpeed = 700; this.spin = 0; this.spinDecay = 0.99; this.spinEffect = 0.3; } reset() { this.pos = new Vector2(width / 2, height / 2); this.vel = new Vector2( (Math.random() > 0.5 ? 1 : -1) * (350 + Math.random() * 150), (Math.random() * 2 - 1) * 300 ); this.spin = 0; } update(deltaTime, leftPaddle, rightPaddle) { // Apply spin effect to velocity (subtle course change) const spinVectorY = this.spin * this.spinEffect; this.vel.y += spinVectorY * deltaTime; // Update position this.pos = this.pos.add(this.vel.multiply(deltaTime)); // Wall collisions if (this.pos.y - this.radius < 0 || this.pos.y + this.radius > height) { this.vel.y *= -1; this.pos.y = (this.pos.y - this.radius < 0) ? this.radius : height - this.radius; this.spin *= 0.7; // Reduce spin on wall bounce } // Score points if (this.pos.x < 0) { rightScore++; this.reset(); } else if (this.pos.x > width) { leftScore++; this.reset(); } // Paddle collisions this.checkPaddleCollision(leftPaddle); this.checkPaddleCollision(rightPaddle); // Decay spin this.spin *= this.spinDecay; // Cap speed const speed = this.vel.length(); if (speed > this.maxSpeed) { this.vel = this.vel.multiply(this.maxSpeed / speed); } } checkPaddleCollision(paddle) { const rect = paddle.getRect(); const closestX = Math.max(rect.x, Math.min(this.pos.x, rect.right)); const closestY = Math.max(rect.y, Math.min(this.pos.y, rect.bottom)); const distX = this.pos.x - closestX; const distY = this.pos.y - closestY; const distance = Math.sqrt(distX * distX + distY * distY); if (distance <= this.radius) { // Calculate collision point and impact force const impactPoint = new Vector2(closestX, closestY); const impactForce = this.vel.length() * 0.02; paddle.applyImpact(impactForce, impactPoint); // Determine new velocity const collisionNormal = this.pos.subtract(impactPoint).normalize(); // Add spin based on where the ball hit the paddle const relativeY = (closestY - paddle.pos.y) / (paddle.height / 2); this.spin = relativeY * 50; // Spin value based on hit position // Apply spin to the reflection angle const spinFactor = this.spin * 0.01; const spinAdjustedNormal = new Vector2( collisionNormal.x, collisionNormal.y + spinFactor ).normalize(); // Reflect velocity with slight speed increase this.vel = this.vel.reflect(spinAdjustedNormal) .multiply(1.05); // Ensure horizontal direction is correct based on which paddle was hit if (paddle === leftPaddle && this.vel.x < 0) this.vel.x *= -1; if (paddle === rightPaddle && this.vel.x > 0) this.vel.x *= -1; // Move ball outside paddle to prevent sticking const pushDistance = this.radius - distance + 1; this.pos = this.pos.add(collisionNormal.multiply(pushDistance)); } } draw(ctx) { ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = 'white'; ctx.fill(); // Visualize spin (optional) if (Math.abs(this.spin) > 5) { const spinIndicatorLength = this.radius * 0.7; const angle = Math.sign(this.spin) * Math.PI / 2; ctx.beginPath(); ctx.moveTo(this.pos.x, this.pos.y); ctx.lineTo( this.pos.x + Math.cos(angle) * spinIndicatorLength, this.pos.y + Math.sin(angle) * spinIndicatorLength ); ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; ctx.lineWidth = 2; ctx.stroke(); } } } // Dynamic Background class DynamicBackground { constructor() { this.noise = new PerlinNoise(); this.time = 0; this.scale = 0.005; this.speed = 0.2; this.colorOffset = 0; } update(deltaTime, leftScore, rightScore) { this.time += deltaTime * this.speed; // Adjust color based on score differential this.colorOffset = (rightScore - leftScore) * 30; } draw(ctx) { // Create gradient based on scores const totalScore = leftScore + rightScore; const hue1 = (210 + this.colorOffset) % 360; const hue2 = (280 + this.colorOffset) % 360; // Draw noise-based background for (let y = 0; y < height; y += 5) { for (let x = 0; x < width; x += 5) { // Get noise value for this pixel const noiseValue = this.noise.getValue( x * this.scale + this.time, y * this.scale + this.time, 1 ); // Interpolate between colors based on noise const hue = hue1 + (hue2 - hue1) * noiseValue; const saturation = 70 + noiseValue * 30; const lightness = 15 + noiseValue * 10 + totalScore * 1.5; ctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`; ctx.fillRect(x, y, 5, 5); } } } } // Initialize game objects const leftPaddle = new Paddle(30, height / 2, 15, 100, 'w', 's'); const rightPaddle = new Paddle(width - 30, height / 2, 15, 100, 'ArrowUp', 'ArrowDown'); const ball = new Ball(); const background = new DynamicBackground(); // Key states const keys = {}; window.addEventListener('keydown', (e) => { keys[e.key.toLowerCase()] = true; if (e.key === ' ') slowMotion = true; }); window.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; if (e.key === ' ') slowMotion = false; }); // Game loop function gameLoop(currentTime) { if (!lastTime) lastTime = currentTime; // Calculate time since last frame deltaTime = (currentTime - lastTime) / 1000; lastTime = currentTime; // Apply slow motion if activated if (slowMotion) { deltaTime *= 0.25; } // Cap delta time to prevent physics issues if (deltaTime > 0.05) deltaTime = 0.05; // Update game objects leftPaddle.update(deltaTime, keys); rightPaddle.update(deltaTime, keys); ball.update(deltaTime, leftPaddle, rightPaddle); background.update(deltaTime, leftScore, rightScore); // Render background.draw(ctx); // Draw center line ctx.beginPath(); ctx.setLineDash([15, 15]); ctx.moveTo(width / 2, 0); ctx.lineTo(width / 2, height); ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; ctx.lineWidth = 2; ctx.stroke(); ctx.setLineDash([]); // Draw paddles and ball leftPaddle.draw(ctx); rightPaddle.draw(ctx); ball.draw(ctx); // Draw score ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.font = '48px Arial'; ctx.textAlign = 'center'; ctx.fillText(leftScore, width / 4, 50); ctx.fillText(rightScore, width * 3 / 4, 50); // Request next frame requestAnimationFrame(gameLoop); } // Start the game requestAnimationFrame(gameLoop); </script> </body> </html> ```
Below is a complete HTML file with embedded JavaScript to create a minimalist, modern version of Pong using HTML5 Canvas. This implementation includes deformable paddles with a spring-damper system, a dynamic gradient background influenced by score using a custom Perlin noise approximation, ball spin affecting ricochet angles with a custom vector math library, and a slow-motion mode triggered by a key press. All rendering and physics are handled with pure JavaScript and Canvas, without external libraries. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Modern Pong</title> <style> body { margin: 0; overflow: hidden; background: #000; } canvas { display: block; } </style> </head> <body> <canvas id="pongCanvas"></canvas> <script> const canvas = document.getElementById('pongCanvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Custom Vector Math Library class Vec2 { constructor(x, y) { this.x = x; this.y = y; } add(v) { return new Vec2(this.x + v.x, this.y + v.y); } sub(v) { return new Vec2(this.x - v.x, this.y - v.y); } mul(s) { return new Vec2(this.x * s, this.y * s); } dot(v) { return this.x * v.x + this.y * v.y; } mag() { return Math.sqrt(this.x * this.x + this.y * this.y); } norm() { const m = this.mag(); return m > 0 ? new Vec2(this.x / m, this.y / m) : new Vec2(0, 0); } } // Perlin Noise Approximation for Background function perlinNoise(x, y) { const xi = Math.floor(x) & 255; const yi = Math.floor(y) & 255; const xf = x - Math.floor(x); const yf = y - Math.floor(y); const u = fade(xf); const v = fade(yf); const aa = hash(xi, yi); const ab = hash(xi, yi + 1); const ba = hash(xi + 1, yi); const bb = hash(xi + 1, yi + 1); return lerp(v, lerp(u, grad(aa, xf, yf), grad(ba, xf - 1, yf)), lerp(u, grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1))); } function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } function lerp(t, a, b) { return a + t * (b - a); } function grad(hash, x, y) { const h = hash & 3; if (h === 0) return x + y; if (h === 1) return -x + y; if (h === 2) return x - y; return -x - y; } const p = Array(256).fill().map((_, i) => i); for (let i = 255; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [p[i], p[j]] = [p[j], p[i]]; } function hash(x, y) { return p[(p[x] + y) & 255]; } // Game Objects const ball = { pos: new Vec2(canvas.width / 2, canvas.height / 2), vel: new Vec2(5, 5), spin: 0, radius: 10 }; const paddle1 = { x: 50, y: canvas.height / 2 - 50, width: 20, height: 100, speed: 8, score: 0, deform: Array(5).fill(0), targetDeform: Array(5).fill(0) }; const paddle2 = { x: canvas.width - 70, y: canvas.height / 2 - 50, width: 20, height: 100, speed: 8, score: 0, deform: Array(5).fill(0), targetDeform: Array(5).fill(0) }; // Input Handling const keys = { w: false, s: false, ArrowUp: false, ArrowDown: false, Shift: false }; document.addEventListener('keydown', (e) => { if (keys.hasOwnProperty(e.key)) keys[e.key] = true; }); document.addEventListener('keyup', (e) => { if (keys.hasOwnProperty(e.key)) keys[e.key] = false; }); // Spring-Damper for Paddle Deformation function updateDeform(paddle, impact) { for (let i = 0; i < paddle.deform.length; i++) { paddle.targetDeform[i] = impact * (1 - Math.abs(i - 2) / 2); const vel = (paddle.targetDeform[i] - paddle.deform[i]) * 0.3; // Spring paddle.deform[i] += vel; paddle.deform[i] *= 0.9; // Damping } } // Physics and Collision with Spin function update(delta) { const speedFactor = keys.Shift ? 0.3 : 1.0; // Slow motion ball.pos = ball.pos.add(ball.vel.mul(speedFactor)); if (ball.pos.y <= 0 || ball.pos.y >= canvas.height) ball.vel.y = -ball.vel.y; if (ball.pos.x < 0) { paddle2.score++; ball.pos = new Vec2(canvas.width / 2, canvas.height / 2); ball.vel = new Vec2(5, 5); } if (ball.pos.x > canvas.width) { paddle1.score++; ball.pos = new Vec2(canvas.width / 2, canvas.height / 2); ball.vel = new Vec2(-5, 5); } // Paddle collision with spin effect const checkPaddleHit = (paddle, side) => { if ((side === 'left' && ball.pos.x < paddle.x + paddle.width && ball.pos.x > paddle.x && ball.vel.x < 0) || (side === 'right' && ball.pos.x + ball.radius > paddle.x && ball.pos.x < paddle.x + paddle.width && ball.vel.x > 0)) { if (ball.pos.y > paddle.y && ball.pos.y < paddle.y + paddle.height) { const relY = (ball.pos.y - paddle.y) / paddle.height; const spinEffect = ball.spin * 0.1; ball.vel.x = -ball.vel.x * 1.05; ball.vel.y += spinEffect + (relY - 0.5) * 10; ball.spin *= -0.8; updateDeform(paddle, ball.vel.mag() * 0.2); } } }; checkPaddleHit(paddle1, 'left'); checkPaddleHit(paddle2, 'right'); ball.spin *= 0.98; // Spin decay } // Paddle Movement function movePaddles() { if (keys.w && paddle1.y > 0) paddle1.y -= paddle1.speed; if (keys.s && paddle1.y < canvas.height - paddle1.height) paddle1.y += paddle1.speed; if (keys.ArrowUp && paddle2.y > 0) paddle2.y -= paddle2.speed; if (keys.ArrowDown && paddle2.y < canvas.height - paddle2.height) paddle2.y += paddle2.speed; } // Render with Dynamic Background let noiseTime = 0; function render() { noiseTime += 0.01; ctx.clearRect(0, 0, canvas.width, canvas.height); // Dynamic gradient background based on score const scoreDiff = paddle1.score - paddle2.score; const r = 100 + scoreDiff * 10; const g = 100 - scoreDiff * 10; for (let y = 0; y < canvas.height; y += 2) { for (let x = 0; x < canvas.width; x += 2) { const n = perlinNoise(x * 0.01 + noiseTime, y * 0.01 + noiseTime); const offset = n * 50; ctx.fillStyle = `rgb(${r + offset}, ${g + offset}, 150)`; ctx.fillRect(x, y, 2, 2); } } // Draw paddles with deformation const drawPaddle = (paddle) => { ctx.beginPath(); ctx.fillStyle = '#fff'; const segHeight = paddle.height / paddle.deform.length; for (let i = 0; i < paddle.deform.length; i++) { const y = paddle.y + i * segHeight; const nextY = paddle.y + (i + 1) * segHeight; const def = paddle.deform[i]; ctx.lineTo(paddle.x + def, y); if (i === paddle.deform.length - 1) ctx.lineTo(paddle.x + def, nextY); } for (let i = paddle.deform.length - 1; i >= 0; i--) { const y = paddle.y + i * segHeight; const nextY = paddle.y + (i + 1) * segHeight; const def = paddle.deform[i]; ctx.lineTo(paddle.x + paddle.width + def, y); if (i === paddle.deform.length - 1) ctx.lineTo(paddle.x + paddle.width + def, nextY); } ctx.closePath(); ctx.fill(); }; drawPaddle(paddle1); drawPaddle(paddle2); // Draw ball ctx.beginPath(); ctx.arc(ball.pos.x, ball.pos.y, ball.radius, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); // Draw score ctx.font = '48px Arial'; ctx.fillText(paddle1.score, canvas.width / 4, 50); ctx.fillText(paddle2.score, 3 * canvas.width / 4, 50); } // Game Loop with Slow Motion let lastTime = 0; function gameLoop(timestamp) { const delta = timestamp - lastTime; lastTime = timestamp; movePaddles(); update(delta / 16.67); render(); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); // Resize Handling window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; paddle1.x = 50; paddle2.x = canvas.width - 70; ball.pos = new Vec2(canvas.width / 2, canvas.height / 2); }); </script> </body> </html> ``` ### Explanation of Features: - **Deformable Paddles**: Paddles use a spring-damper system to deform based on ball velocity upon impact. Each paddle is segmented into parts that bend and return to shape dynamically. - **Dynamic Background**: A procedural gradient background shifts colors based on the score difference and uses a Perlin noise approximation to create a swirling, organic effect over time. - **Ball Spin Physics**: A custom vector math library (`Vec2`) handles ball movement and collisions. Spin is applied to the ball, affecting ricochet angles off paddles, with decay over time. - **Slow Motion Mode**: Holding the Shift key reduces the game speed to 30% of normal, implemented via `requestAnimationFrame` for smooth frame rate control. - **Pure Implementation**: All logic, from physics to noise generation, is coded without external libraries, using only HTML5 Canvas and JavaScript. ### Controls: - Player 1: W (up), S (down) - Player 2: Arrow Up, Arrow Down - Slow Motion: Hold Shift This code can be saved as an HTML file and opened in a browser to play the game with the described modern effects.
Here is a minimalist web-based Pong game, reimagined with modern physics and procedural graphics, all in a single HTML file with pure JavaScript. ### Features * **Deformable Paddles**: Paddles have a "rubber" feel, wobbling and deforming on impact thanks to a custom spring-damper physics system. * **Dynamic Background**: A swirling, generative background powered by a custom Perlin noise algorithm. The colors of the nebula-like background shift to reflect the current score leader. * **Ball Spin Physics**: The ball gains spin when hit by a moving paddle. This spin affects its trajectory and how it bounces off surfaces. * **Slow Motion**: Hold the `Shift` key to enter a smooth slow-motion mode, perfect for appreciating the detailed physics. * **Self-Contained**: No external libraries or dependencies. Everything, including the vector math, physics engine, and noise algorithm, is written from scratch. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Quantum Pong</title> <style> body { margin: 0; padding: 0; background-color: #000; color: #fff; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; font-family: 'Consolas', 'Courier New', monospace; text-align: center; } canvas { background-color: #0c0c0c; display: block; } .container { position: relative; } .overlay { position: absolute; top: 20px; left: 0; right: 0; pointer-events: none; } .score { font-size: 4em; font-weight: bold; color: rgba(255, 255, 255, 0.5); letter-spacing: 0.1em; } .controls { position: absolute; bottom: 20px; left: 0; right: 0; color: rgba(255, 255, 255, 0.4); font-size: 1em; } </style> </head> <body> <div class="container"> <div class="overlay"> <div id="score" class="score">0 | 0</div> </div> <canvas id="gameCanvas"></canvas> <div class="controls"> Move Mouse to Control Paddle | Hold SHIFT for Slow-Motion </div> </div> <script> // --- CORE SETUP --- const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const scoreEl = document.getElementById('score'); const WIDTH = 1000; const HEIGHT = 600; canvas.width = WIDTH; canvas.height = HEIGHT; // --- CUSTOM VECTOR MATH LIBRARY --- class Vector2D { constructor(x = 0, y = 0) { this.x = x; this.y = y; } add(v) { return new Vector2D(this.x + v.x, this.y + v.y); } sub(v) { return new Vector2D(this.x - v.x, this.y - v.y); } mult(s) { return new Vector2D(this.x * s, this.y * s); } mag() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const m = this.mag(); return m > 0 ? new Vector2D(this.x / m, this.y / m) : new Vector2D(); } dot(v) { return this.x * v.x + this.y * v.y; } clone() { return new Vector2D(this.x, this.y); } } // --- CUSTOM PERLIN NOISE GENERATOR --- const PerlinNoise = new (function() { this.p = new Uint8Array(512); this.init = () => { const p = Array.from({length: 256}, (_, i) => i); for (let i = p.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [p[i], p[j]] = [p[j], p[i]]; } for (let i = 0; i < 256; i++) { this.p[i] = this.p[i + 256] = p[i]; } }; this.init(); const fade = t => t * t * t * (t * (t * 6 - 15) + 10); const lerp = (t, a, b) => a + t * (b - a); const grad = (hash, x, y, z) => { const h = hash & 15; const u = h < 8 ? x : y; const v = h < 4 ? y : h === 12 || h === 14 ? x : z; return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v); }; this.noise = (x, y, z) => { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; const Z = Math.floor(z) & 255; x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z); const u = fade(x); const v = fade(y); const w = fade(z); const A = this.p[X] + Y, AA = this.p[A] + Z, AB = this.p[A + 1] + Z; const B = this.p[X + 1] + Y, BA = this.p[B] + Z, BB = this.p[B + 1] + Z; return lerp(w, lerp(v, lerp(u, grad(this.p[AA], x, y, z), grad(this.p[BA], x - 1, y, z)), lerp(u, grad(this.p[AB], x, y - 1, z), grad(this.p[BB], x - 1, y - 1, z))), lerp(v, lerp(u, grad(this.p[AA + 1], x, y, z - 1), grad(this.p[BA + 1], x - 1, y, z - 1)), lerp(u, grad(this.p[AB + 1], x, y - 1, z - 1), grad(this.p[BB + 1], x - 1, y - 1, z - 1)))); }; })(); // --- CUSTOM PADDLE PHYSICS ENGINE --- class Vertex { constructor(x, y) { this.pos = new Vector2D(x, y); this.originalPos = new Vector2D(x, y); this.vel = new Vector2D(); this.force = new Vector2D(); this.mass = 1; } applyForce(force) { this.force = this.force.add(force); } update(dt) { const acceleration = this.force.mult(1 / this.mass); this.vel = this.vel.add(acceleration.mult(dt)); this.pos = this.pos.add(this.vel.mult(dt)); this.force = new Vector2D(); } } // --- GAME ENTITIES --- class Ball { constructor(x, y, radius) { this.pos = new Vector2D(x, y); this.vel = new Vector2D(); this.radius = radius; this.spin = 0; // angular velocity this.spinDamping = 0.99; this.maxSpin = 15; } reset() { this.pos = new Vector2D(WIDTH / 2, HEIGHT / 2); this.vel = new Vector2D(Math.random() > 0.5 ? 400 : -400, Math.random() * 200 - 100); this.spin = 0; } update(dt) { this.pos = this.pos.add(this.vel.mult(dt)); this.spin *= this.spinDamping; // Wall collisions if (this.pos.y - this.radius < 0 || this.pos.y + this.radius > HEIGHT) { this.vel.y *= -1; // Magnus effect on horizontal walls this.vel.x += this.spin * 0.5; // Apply spin influence this.pos.y = Math.max(this.radius, Math.min(HEIGHT - this.radius, this.pos.y)); this.spin *= -0.8; // Dampen and reverse spin on wall hit } } draw(ctx) { ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); } } class Paddle { constructor(x, y, width, height, isAI = false) { this.pos = new Vector2D(x, y); this.width = width; this.height = height; this.isAI = isAI; this.aiDifficulty = 0.08; // Deformable paddle setup this.numVertices = 10; this.vertices = []; for (let i = 0; i < this.numVertices; i++) { const vertY = y - height/2 + (i / (this.numVertices-1)) * height; this.vertices.push(new Vertex(x, vertY)); } // Spring-damper properties this.stiffness = 20000; this.damping = 200; this.lastVelY = 0; } update(dt, targetY, ball) { // Update main paddle position (AI or player) let newY; if(this.isAI) { const targetCenter = this.pos.y + (ball.pos.y - this.pos.y) * this.aiDifficulty; newY = targetCenter; } else { newY = targetY; } this.lastVelY = (newY - this.pos.y) / dt; this.pos.y = Math.max(this.height/2, Math.min(HEIGHT - this.height/2, newY)); // Update vertices based on spring-damper system for (let i = 0; i < this.numVertices; i++) { const vertex = this.vertices[i]; // Force pulling vertex back to its ideal position on the paddle const idealY = this.pos.y - this.height/2 + (i / (this.numVertices-1)) * this.height; const displacement = vertex.pos.y - idealY; const springForce = -this.stiffness * displacement; // Damping force const dampingForce = -this.damping * vertex.vel.y; vertex.applyForce(new Vector2D(0, springForce + dampingForce)); // Keep X position fixed vertex.pos.x = this.pos.x; vertex.update(dt); } } applyImpact(ballImpactPos, ballVel) { let closestVertex = this.vertices[0]; let minDist = Infinity; let closestIndex = 0; // Find the vertex closest to the impact this.vertices.forEach((v, index) => { const dist = Math.abs(v.pos.y - ballImpactPos.y); if (dist < minDist) { minDist = dist; closestVertex = v; closestIndex = index; } }); // Apply impulse based on ball velocity (rubber effect) const impactForce = ballVel.y * 0.5; closestVertex.vel.y += impactForce; // Propagate a smaller force to neighbors if (closestIndex > 0) this.vertices[closestIndex - 1].vel.y += impactForce * 0.4; if (closestIndex < this.numVertices - 1) this.vertices[closestIndex + 1].vel.y += impactForce * 0.4; } draw(ctx) { ctx.beginPath(); ctx.moveTo(this.vertices[0].pos.x, this.vertices[0].pos.y); for (let i = 1; i < this.numVertices; i++) { const xc = (this.vertices[i - 1].pos.x + this.vertices[i].pos.x) / 2; const yc = (this.vertices[i - 1].pos.y + this.vertices[i].pos.y) / 2; ctx.quadraticCurveTo(this.vertices[i-1].pos.x, this.vertices[i-1].pos.y, xc, yc); } ctx.lineTo(this.vertices[this.numVertices-1].pos.x, this.vertices[this.numVertices-1].pos.y); ctx.lineWidth = this.width; ctx.strokeStyle = '#fff'; ctx.lineCap = 'round'; ctx.stroke(); } } // --- GAME MANAGER --- class Game { constructor() { this.player = new Paddle(30, HEIGHT / 2, 15, 100); this.ai = new Paddle(WIDTH - 30, HEIGHT / 2, 15, 100, true); this.ball = new Ball(WIDTH / 2, HEIGHT / 2, 8); this.score = { player: 0, ai: 0 }; this.isSlowMo = false; this.lastTime = 0; this.mouseY = HEIGHT / 2; // Background properties this.bgTime = 0; this.bgCanvas = document.createElement('canvas'); this.bgCanvas.width = WIDTH / 4; // Render at lower res for performance this.bgCanvas.height = HEIGHT / 4; this.bgCtx = this.bgCanvas.getContext('2d'); this.init(); } init() { document.addEventListener('mousemove', e => { const rect = canvas.getBoundingClientRect(); this.mouseY = e.clientY - rect.top; }); document.addEventListener('keydown', e => { if (e.key === 'Shift') this.isSlowMo = true; }); document.addEventListener('keyup', e => { if (e.key === 'Shift') this.isSlowMo = false; }); this.ball.reset(); this.updateScore(); this.updateBackground(); } update(dt) { this.player.update(dt, this.mouseY); this.ai.update(dt, 0, this.ball); this.ball.update(dt); this.checkCollisions(); this.checkScore(); } checkCollisions() { const checkPaddle = (paddle) => { const ball = this.ball; const paddleEdgeX = paddle.pos.x + (paddle.pos.x < WIDTH / 2 ? paddle.width / 2 : -paddle.width / 2); // Broad-phase collision check if (Math.abs(ball.pos.x - paddle.pos.x) > 50) return; // Find closest point on paddle segment line let closestY = Infinity; let onPaddle = false; for(let i=0; i < paddle.vertices.length-1; i++){ const v1 = paddle.vertices[i]; const v2 = paddle.vertices[i+1]; if(ball.pos.y >= v1.pos.y && ball.pos.y <= v2.pos.y){ closestY = ball.pos.y; onPaddle = true; break; } } if(!onPaddle){ closestY = ball.pos.y < paddle.vertices[0].pos.y ? paddle.vertices[0].pos.y : paddle.vertices[paddle.vertices.length-1].pos.y; } if (ball.pos.x - ball.radius < paddleEdgeX && ball.pos.x + ball.radius > paddleEdgeX && ball.vel.x < 0 && paddle.pos.x < WIDTH/2 || ball.pos.x + ball.radius > paddleEdgeX && ball.pos.x - ball.radius < paddleEdgeX && ball.vel.x > 0 && paddle.pos.x > WIDTH/2) { if(Math.abs(ball.pos.y - closestY) < paddle.height/2 + ball.radius){ // Collision occurred ball.vel.x *= -1.05; // Speed up ball slightly on hit ball.pos.x = paddleEdgeX + (paddle.pos.x < WIDTH / 2 ? ball.radius : -ball.radius); // Add spin based on paddle's vertical velocity at impact const paddleVelY = paddle.lastVelY; const spinImparted = paddleVelY * 0.005; ball.spin = Math.max(-ball.maxSpin, Math.min(ball.maxSpin, ball.spin + spinImparted)); // Add "english" based on where it hit the paddle const hitOffset = (ball.pos.y - paddle.pos.y) / (paddle.height / 2); ball.vel.y += hitOffset * 150; // Trigger paddle deformation paddle.applyImpact(ball.pos, ball.vel); } } }; checkPaddle(this.player); checkPaddle(this.ai); } checkScore() { if (this.ball.pos.x < -this.ball.radius) { this.score.ai++; this.ball.reset(); this.updateScore(); } else if (this.ball.pos.x > WIDTH + this.ball.radius) { this.score.player++; this.ball.reset(); this.updateScore(); } } updateScore() { scoreEl.textContent = `${this.score.player} | ${this.score.ai}`; this.updateBackground(); // Update background colors on score change } updateBackground() { const bgW = this.bgCanvas.width; const bgH = this.bgCanvas.height; const imageData = this.bgCtx.createImageData(bgW, bgH); const data = imageData.data; const scale = 0.02; const scoreDiff = this.score.player - this.score.ai; const balance = Math.max(-1, Math.min(1, scoreDiff / 5)); // -1 (AI winning) to 1 (Player winning) // Player winning color (cool) const c1_r = 5, c1_g = 20, c1_b = 60; // AI winning color (warm) const c2_r = 60, c2_g = 10, c2_b = 20; // Mix colors based on score balance const r_base = (c1_r * (1 + balance) + c2_r * (1 - balance)) / 2; const g_base = (c1_g * (1 + balance) + c2_g * (1 - balance)) / 2; const b_base = (c1_b * (1 + balance) + c2_b * (1 - balance)) / 2; for (let y = 0; y < bgH; y++) { for (let x = 0; x < bgW; x++) { const i = (y * bgW + x) * 4; const noiseVal = PerlinNoise.noise(x * scale, y * scale, this.bgTime); const brightness = (noiseVal + 1) / 2 * 128; // map [-1,1] to [0,128] data[i] = r_base + brightness; data[i + 1] = g_base + brightness; data[i + 2] = b_base + brightness * 1.5; // More blue in highlights data[i + 3] = 255; } } this.bgCtx.putImageData(imageData, 0, 0); } draw(dt) { // Draw background this.bgTime += dt * 0.1; if(Math.floor(this.bgTime * 10) !== Math.floor((this.bgTime-dt*0.1) * 10)) { // Only update background texture periodically for performance this.updateBackground(); } ctx.save(); ctx.globalAlpha = 0.5; ctx.imageSmoothingEnabled = true; ctx.drawImage(this.bgCanvas, 0, 0, WIDTH, HEIGHT); ctx.restore(); // Draw center line ctx.beginPath(); ctx.setLineDash([5, 15]); ctx.moveTo(WIDTH / 2, 0); ctx.lineTo(WIDTH / 2, HEIGHT); ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; ctx.lineWidth = 2; ctx.stroke(); ctx.setLineDash([]); // Draw entities this.player.draw(ctx); this.ai.draw(ctx); this.ball.draw(ctx); } loop(currentTime) { if (!this.lastTime) this.lastTime = currentTime; const deltaTime = (currentTime - this.lastTime) / 1000; this.lastTime = currentTime; const timeScale = this.isSlowMo ? 0.2 : 1.0; const effectiveDeltaTime = deltaTime * timeScale; this.update(effectiveDeltaTime); ctx.clearRect(0, 0, WIDTH, HEIGHT); this.draw(effectiveDeltaTime); requestAnimationFrame(this.loop.bind(this)); } } // --- START GAME --- const game = new Game(); requestAnimationFrame(game.loop.bind(game)); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <title>Minimalist Pong</title> <style> body { margin: 0; overflow: hidden; background-color: black; } canvas { display: block; } </style> </head> <body> <canvas id="pongCanvas"></canvas> <script> (function() { const canvas = document.getElementById('pongCanvas'); const ctx = canvas.getContext('2d'); // Canvas dimensions canvas.width = 800; canvas.height = 600; // Game constants const PADDLE_HEIGHT = 100; const PADDLE_WIDTH = 10; const BALL_SIZE = 10; const INITIAL_BALL_SPEED = 5; const SLOW_MOTION_SCALE = 0.2; const SPRING_CONSTANT = 50; const DAMPING_CONSTANT = 5; // Player & AI Paddles let playerPaddleY = canvas.height / 2 - PADDLE_HEIGHT / 2; let aiPaddleY = canvas.height / 2 - PADDLE_HEIGHT / 2; let playerPaddleDeformation = 0; let aiPaddleDeformation = 0; // Ball let ballX = canvas.width / 2; let ballY = canvas.height / 2; let ballSpeedX = INITIAL_BALL_SPEED; let ballSpeedY = INITIAL_BALL_SPEED; let ballSpin = 0; // Spin influences ricochet angle // Score let playerScore = 0; let aiScore = 0; // Slow Motion let slowMotionActive = false; // Vector Math (simple 2D Vector) class Vector2 { constructor(x, y) { this.x = x; this.y = y; } add(other) { return new Vector2(this.x + other.x, this.y + other.y); } subtract(other) { return new Vector2(this.x - other.x, this.y - other.y); } multiply(scalar) { return new Vector2(this.x * scalar, this.y * scalar); } } // Perlin Noise (simple implementation) function perlinNoise(x, y) { let xi = Math.floor(x); let yi = Math.floor(y); let xf = x - xi; let yf = y - yi; let v1 = Math.random(); let v2 = Math.random(); let v3 = Math.random(); let v4 = Math.random(); let u = fade(xf); let v = fade(yf); let dotProduct1 = gradient(xi, yi, v1); let dotProduct2 = gradient(xi + 1, yi, v2); let dotProduct3 = gradient(xi, yi + 1, v3); let dotProduct4 = gradient(xi + 1, yi + 1, v4); let interpolated1 = interpolate(dotProduct1, dotProduct2, u); let interpolated2 = interpolate(dotProduct3, dotProduct4, u); return interpolate(interpolated1, interpolated2, v); } function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } function gradient(xi, yi, randomValue) { return randomValue; // Simplified gradient. Can use more complex patterns } function interpolate(a, b, t) { return a + t * (b - a); } // Paddle Deformation (Spring-Damper) function updatePaddleDeformation(paddleY, ballSpeed) { const targetDeformation = ballSpeed * 0.5; //Deformation is related to speed const delta = targetDeformation - playerPaddleDeformation; playerPaddleDeformation += (delta * SPRING_CONSTANT) - (playerPaddleDeformation * DAMPING_CONSTANT); } // Game Loop function gameLoop() { if (slowMotionActive) { ballSpeedX *= SLOW_MOTION_SCALE; ballSpeedY *= SLOW_MOTION_SCALE; } else { ballSpeedX = Math.max(Math.min(ballSpeedX, 10), -10); ballSpeedY = Math.max(Math.min(ballSpeedY, 10), -10); } // Background Gradient with Perlin Noise let gradientColor = `hsl(${Math.floor(perlinNoise(playerScore * 0.01, aiScore * 0.01) * 360)}, 100%, 50%)`; ctx.fillStyle = gradientColor; ctx.fillRect(0, 0, canvas.width, canvas.height); // Update Paddle Deformation updatePaddleDeformation(playerPaddleY, Math.abs(ballSpeedX)); updatePaddleDeformation(aiPaddleY, Math.abs(ballSpeedX)); // Player Paddle ctx.fillStyle = 'white'; ctx.fillRect(20, playerPaddleY + playerPaddleDeformation, PADDLE_WIDTH, PADDLE_HEIGHT); // AI Paddle ctx.fillStyle = 'white'; ctx.fillRect(canvas.width - 20 - PADDLE_WIDTH, aiPaddleY + aiPaddleDeformation, PADDLE_WIDTH, PADDLE_HEIGHT); // Ball ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(ballX, ballY, BALL_SIZE, 0, 2 * Math.PI); ctx.fill(); // Ball Movement ballX += ballSpeedX; ballY += ballSpeedY; // Ball Collisions if (ballY - BALL_SIZE < 0 || ballY + BALL_SIZE > canvas.height) { ballSpeedY = -ballSpeedY; } // Player Collision if (ballX - BALL_SIZE < 20 && ballY + BALL_SIZE > playerPaddleY && ballY - BALL_SIZE < playerPaddleY + PADDLE_HEIGHT) { ballSpeedX = -ballSpeedX; // Apply spin based on where the ball hits the paddle const relativeY = (ballY - playerPaddleY) / PADDLE_HEIGHT - 0.5; ballSpin = relativeY * 2; // Increase spin based on hit position } // AI Collision if (ballX + BALL_SIZE > canvas.width - 20 && ballY + BALL_SIZE > aiPaddleY && ballY - BALL_SIZE < aiPaddleY + PADDLE_HEIGHT) { ballSpeedX = -ballSpeedX; // Apply spin based on where the ball hits the paddle const relativeY = (ballY - aiPaddleY) / PADDLE_HEIGHT - 0.5; ballSpin = relativeY * 2; } // Scoring if (ballX - BALL_SIZE < 0) { aiScore++; resetBall(); } else if (ballX + BALL_SIZE > canvas.width) { playerScore++; resetBall(); } // AI Movement (Basic) aiPaddleY = Math.max(0, Math.min(canvas.height - PADDLE_HEIGHT, ballY - PADDLE_HEIGHT / 2)); // Apply spin to the ball's trajectory if (ballSpin !== 0) { const spinInfluence = 0.05; ballSpeedY += ballSpin * spinInfluence; } // Request next frame requestAnimationFrame(gameLoop); } function resetBall() { ballX = canvas.width / 2; ballY = canvas.height / 2; ballSpeedX = INITIAL_BALL_SPEED * (Math.random() < 0.5 ? -1 : 1); ballSpeedY = INITIAL_BALL_SPEED * (Math.random() < 0.5 ? -1 : 1); ballSpin = 0; } // Event Listeners document.addEventListener('keydown', (event) => { if (event.key === 'w' || event.key === 'W') { playerPaddleY -= 5; } else if (event.key === 's' || event.key === 'S') { playerPaddleY += 5; } else if (event.key === 'Shift') { slowMotionActive = true; } }); document.addEventListener('keyup', (event) => { if (event.key === 'Shift') { slowMotionActive = false; } }); //Keep paddle within bounds function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } playerPaddleY = clamp(playerPaddleY, 0, canvas.height - PADDLE_HEIGHT); aiPaddleY = clamp(aiPaddleY, 0, canvas.height - PADDLE_HEIGHT); // Start the game gameLoop(); })(); </script> </body> </html> ```
Here's a minimalist Pong with modern physics and effects. The code includes a custom vector library, spring-damper system, Perlin-like noise, and spin physics: ```html <!DOCTYPE html> <html> <body style="margin:0; overflow:hidden; background:#000"> <canvas id="game"></canvas> <script> const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Vector Math Library class Vector2 { constructor(x=0, y=0) { this.x = x; this.y = y; } add(v) { return new Vector2(this.x + v.x, this.y + v.y); } subtract(v) { return new Vector2(this.x - v.x, this.y - v.y); } multiply(s) { return new Vector2(this.x * s, this.y * s); } dot(v) { return this.x * v.x + this.y * v.y; } cross(v) { return this.x * v.y - this.y * v.x; } length() { return Math.sqrt(this.x*this.x + this.y*this.y); } normalize() { const len = this.length(); return len ? new Vector2(this.x/len, this.y/len) : new Vector2(); } rotate(angle) { return new Vector2( this.x * Math.cos(angle) - this.y * Math.sin(angle), this.x * Math.sin(angle) + this.y * Math.cos(angle) ); } } // Perlin-like Noise Generator const Noise = { grad: (hash, x, y) => { const h = hash & 15; const grad = 1 + (h & 7); return ((h & 8) ? -grad : grad) * x + ((h & 4) ? -grad : grad) * y; }, noise: (x, y) => { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; x -= Math.floor(x); y -= Math.floor(y); const u = x * x * x * (x * (x * 6 - 15) + 10); const v = y * y * y * (y * (y * 6 - 15) + 10); const p = new Array(512); for(let i=0; i<256; i++) p[i] = p[i+256] = Math.floor(Math.random()*256); const A = p[X] + Y, B = p[X+1] + Y; return this.lerp(v, this.lerp(u, this.grad(p[A], x, y), this.grad(p[B], x-1, y)), this.lerp(u, this.grad(p[A+1], x, y-1), this.grad(p[B+1], x-1, y-1)) ) * 0.5 + 0.5; }, lerp: (t, a, b) => a + t * (b - a) }; // Game Objects const Ball = { pos: new Vector2(canvas.width/2, canvas.height/2), vel: new Vector2(5, 2), radius: 10, spin: 0, update: (dt) => { Ball.pos.x += Ball.vel.x * dt; Ball.pos.y += Ball.vel.y * dt; // Spin physics const spinEffect = Ball.spin * 0.01; Ball.vel = Ball.vel.rotate(spinEffect * dt); } }; const Paddle = { left: { points: Array(10).fill().map((_,i) => new Vector2(50, canvas.height/2 + (i-5)*20)), stiffness: 0.3, damping: 0.8 }, right: { points: Array(10).fill().map((_,i) => new Vector2(canvas.width-50, canvas.height/2 + (i-5)*20)), stiffness: 0.3, damping: 0.8 }, update: (dt) => { [Paddle.left, Paddle.right].forEach(paddle => { paddle.points.forEach((point, i) => { // Spring-damper system const restY = paddle.points[5].y + (i-5)*20; const displacement = point.y - restY; const force = -Paddle.stiffness * displacement; point.y += (force - paddle.damping * point.y) * dt; }); }); } }; // Game State let score = { left: 0, right: 0 }; let slowMo = false; let lastTime = 0; // Input Handling window.addEventListener('keydown', e => { if(e.key === 'Shift') slowMo = true; // Paddle controls if(e.key === 'w') Paddle.left.pos.y -= 10; if(e.key === 's') Paddle.left.pos.y += 10; if(e.key === 'ArrowUp') Paddle.right.pos.y -= 10; if(e.key === 'ArrowDown') Paddle.right.pos.y += 10; }); window.addEventListener('keyup', e => { if(e.key === 'Shift') slowMo = false; }); // Game Loop function gameLoop(timestamp) { const dt = (timestamp - lastTime) / 1000 * (slowMo ? 0.3 : 1); lastTime = timestamp; // Clear canvas ctx.fillStyle = getGradientBackground(); ctx.fillRect(0, 0, canvas.width, canvas.height); // Update physics Ball.update(dt); Paddle.update(dt); checkCollisions(); // Draw objects drawBall(); drawPaddles(); drawScore(); requestAnimationFrame(gameLoop); } function getGradientBackground() { const noise = Noise.noise(score.left/10 + score.right/10, timestamp/10000); const hue = 200 + noise * 60; return `hsl(${hue}, 70%, 20%)`; } function checkCollisions() { // Wall collisions if(Ball.pos.y < Ball.radius || Ball.pos.y > canvas.height-Ball.radius) { Ball.vel.y *= -1; Ball.spin += Ball.vel.x * 0.01; } // Paddle collisions if(Ball.pos.x < 60 && Ball.vel.x < 0) { if(Math.abs(Ball.pos.y - Paddle.left.points[5].y) < 50) { Ball.vel.x *= -1.1; Ball.spin += (Ball.pos.y - Paddle.left.points[5].y) * 0.01; deformPaddle(Paddle.left, Ball.pos); } } if(Ball.pos.x > canvas.width-60 && Ball.vel.x > 0) { if(Math.abs(Ball.pos.y - Paddle.right.points[5].y) < 50) { Ball.vel.x *= -1.1; Ball.spin += (Ball.pos.y - Paddle.right.points[5].y) * 0.01; deformPaddle(Paddle.right, Ball.pos); } } // Score if(Ball.pos.x < 0 || Ball.pos.x > canvas.width) { score[Ball.vel.x < 0 ? 'right' : 'left']++; resetBall(); } } function deformPaddle(paddle, impactPos) { paddle.points.forEach(point => { const dist = Math.abs(point.y - impactPos.y); if(dist < 30) { const force = 30 - dist; point.y += (impactPos.y > point.y ? 1 : -1) * force * 0.5; } }); } function resetBall() { Ball.pos = new Vector2(canvas.width/2, canvas.height/2); Ball.vel = new Vector2(Math.random() > 0.5 ? 5 : -5, (Math.random()-0.5)*4); Ball.spin = 0; } function drawBall() { ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(Ball.pos.x, Ball.pos.y, Ball.radius, 0, Math.PI*2); ctx.fill(); } function drawPaddles() { [Paddle.left, Paddle.right].forEach(paddle => { ctx.beginPath(); ctx.moveTo(paddle.points[0].x, paddle.points[0].y); for(let i=1; i<paddle.points.length; i++) { ctx.lineTo(paddle.points[i].x, paddle.points[i].y); } ctx.strokeStyle = '#fff'; ctx.lineWidth = 5; ctx.stroke(); }); } function drawScore() { ctx.fillStyle = '#fff'; ctx.font = '30px monospace'; ctx.fillText(score.left, canvas.width/4, 50); ctx.fillText(score.right, canvas.width*3/4, 50); } // Start Game resetBall(); requestAnimationFrame(gameLoop); </script> </body> </html> ``` Key features implemented: 1. Vector math library for physics calculations 2. Spring-damper system for paddle deformation 3. Perlin-like noise for dynamic background 4. Ball spin physics affecting ricochet angles 5. Slow motion mode with frame rate adjustment 6. Score-based color shifting 7. Minimalist visual design The paddles deform using a series of connected points with spring physics. The background uses a procedural noise function to create shifting colors. Ball spin is calculated based on impact position and affects subsequent collisions. Hold Shift to activate slow motion.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Transcendent Pong</title> <style> html, body { background: #111; margin: 0; padding: 0; overflow: hidden; width: 100vw; height: 100vh; } body { width: 100vw; height: 100vh; } canvas { display: block; margin: 0 auto; background: transparent; position: absolute; left: 0; top: 0; right: 0; bottom: 0; } * { box-sizing: border-box;} </style> </head> <body> <canvas id="pong"></canvas> <script> /* ========== VECTOR MATH LIBRARY ========== */ class Vec2 { constructor(x=0, y=0) { this.x = x; this.y = y; } clone() { return new Vec2(this.x, this.y); } add(v) { this.x+=v.x; this.y+=v.y; return this; } sub(v) { this.x-=v.x; this.y-=v.y; return this; } mul(s) { this.x*=s; this.y*=s; return this; } div(s) { this.x/=s; this.y/=s; return this; } dot(v) { return this.x*v.x + this.y*v.y; } length() { return Math.hypot(this.x, this.y); } norm() { let l=this.length(); if(l>0) { this.x/=l; this.y/=l; } return this; } perp() { return new Vec2(-this.y, this.x); } angle() { return Math.atan2(this.y, this.x); } set(x,y) { this.x=x; this.y=y; return this;} static from(v) { return new Vec2(v.x, v.y);} static add(a,b){ return new Vec2(a.x+b.x, a.y+b.y); } static sub(a,b){ return new Vec2(a.x-b.x, a.y-b.y); } static mul(v,s){ return new Vec2(v.x*s, v.y*s); } static lerp(a, b, t){ return new Vec2(a.x + (b.x - a.x)*t, a.y + (b.y - a.y)*t);} } /* ========== PERLIN NOISE IMPLEMENTATION ========== */ // Lightweight 2D Perlin/Gradient Noise class PerlinNoise { constructor(seed=0) { this.gradients = {}; this.perm = []; for(let i=0; i<256; ++i) this.perm[i]=i; // Shuffle perm with seed let r = () => { seed = (seed*9301+49297)%233280; return seed/233280; }; for (let i = 255; i > 0; i--) { let j = Math.floor(r() * (i + 1)); [this.perm[i], this.perm[j]] = [this.perm[j], this.perm[i]]; } } grad(ix, iy) { let key = ix+','+iy; if (this.gradients[key]) return this.gradients[key]; let theta = ((this.perm[(ix+this.perm[iy&255])&255])/256)*2*Math.PI; let v = {x: Math.cos(theta), y: Math.sin(theta)}; this.gradients[key]=v; return v; } dotGrid(ix, iy, x, y) { let grad = this.grad(ix, iy); let dx = x - ix, dy = y - iy; return grad.x * dx + grad.y * dy; } fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } lerp(a,b,t) { return a+(b-a)*t;} noise(x, y) { let ix=Math.floor(x), iy=Math.floor(y); let fx=x-ix, fy=y-iy; let v00=this.dotGrid(ix,iy,x,y); let v10=this.dotGrid(ix+1,iy,x,y); let v01=this.dotGrid(ix,iy+1,x,y); let v11=this.dotGrid(ix+1,iy+1,x,y); let u=this.fade(fx), v=this.fade(fy); let vx0 = this.lerp(v00, v10, u); let vx1 = this.lerp(v01, v11, u); return this.lerp(vx0, vx1, v) * 0.5 + 0.5; } } /* ========== RUBBER/SPRING-DAMPER PADDLE ========== */ class Paddle { constructor(x, y, width, length, isLeft) { this.basePos = new Vec2(x, y); this.width=width; this.length=length; this.isLeft=isLeft; // For deformation: we model paddle as a set of vertical nodes moved by a spring-damper this.nNodes = 12; this.nodes = []; this.vels = []; this.nodeTargets = []; // in y: top to bottom for(let i=0; i<this.nNodes; ++i) { this.nodes[i] = 0; // initial 'x' deformation at each node this.vels[i] = 0; this.nodeTargets[i] = 0; } } applyDeformation(ball, intersectCoef, forceFac) { // intersectCoef: 0 at top, 1 at bottom, -1 = no contact if (intersectCoef < 0) return; let idx = Math.floor(intersectCoef*(this.nNodes-1)); let f = forceFac*ball.vel.x*ball.radius/2; if (!this.isLeft) f*=-1; // Distribute force across several nodes (to get smooth deformation) for(let i=-2;i<=2;++i){ let j=idx+i; if(j>=0 && j<this.nNodes){ // Gaussian distribution of force let g=Math.exp(-0.8*i*i); this.vels[j]+= f*g; } } } update(dt) { // Apply spring-damper to each node toward 0 const k = 200, d = 20; for(let i=0; i<this.nNodes; ++i){ let F = -k * this.nodes[i] - d * this.vels[i]; this.vels[i] += (F) * dt; this.nodes[i] += this.vels[i] * dt; } // Smooth between nodes for nice shape for(let pass=0; pass<2; ++pass){ let prev = this.nodes[0]; for(let i=1;i<this.nNodes-1;++i){ let avg=(prev+this.nodes[i+1])/2; this.nodes[i]=this.nodes[i]*0.8+avg*0.2; prev = this.nodes[i]; } } } draw(ctx) { ctx.save(); ctx.beginPath(); for(let i=0;i<this.nNodes;++i){ let ty = this.basePos.y + (i/(this.nNodes-1)-0.5)*this.length; let tx = this.basePos.x + this.nodes[i]; if(i===0) ctx.moveTo(tx-this.width/2, ty); else ctx.lineTo(tx-this.width/2, ty); } for(let i=this.nNodes-1;i>=0;--i){ let ty = this.basePos.y + (i/(this.nNodes-1)-0.5)*this.length; let tx = this.basePos.x + this.nodes[i]; ctx.lineTo(tx+this.width/2, ty); } ctx.closePath(); ctx.fillStyle = "rgba(255,255,255,0.88)"; ctx.shadowColor="#000a"; ctx.shadowBlur=3; ctx.fill(); ctx.restore(); } moveTo(y, height) { // Clamp base y const half = this.length/2; this.basePos.y = Math.max(half, Math.min(height-half, y)); } getSegments() { // For collision: return lines from node i to i+1 (in world coords) let segs=[]; for(let i=0;i<this.nNodes-1;++i){ let t0=(i)/(this.nNodes-1); let t1=(i+1)/(this.nNodes-1); let y0=this.basePos.y + (t0-0.5)*this.length; let y1=this.basePos.y + (t1-0.5)*this.length; let x0=this.basePos.x + this.nodes[i]; let x1=this.basePos.x + this.nodes[i+1]; segs.push([ new Vec2(x0, y0), new Vec2(x1, y1), t0 // for interpolation ]); } return segs; } } /* ========== BALL OBJECT W/ SPIN PHYSICS ========== */ class Ball { constructor(x, y, r) { this.pos = new Vec2(x, y); let angle = Math.random()*Math.PI*2; this.vel = new Vec2(Math.cos(angle), Math.sin(angle)); this.vel.mul(330 + Math.random()*60); this.radius = r; this.spin = 0; // Spin (rads/sec) this.angularVel = 0; } update(dt) { // Move position this.pos.add(Vec2.mul(this.vel, dt)); // Integrate spin this.spin += this.angularVel*dt; // Friction on spin (air resistance) this.angularVel *= Math.exp(-dt*2); } draw(ctx) { // Ball with slight drop shadow and faint spin indicator line ctx.save(); ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI*2); ctx.shadowColor="#33f6"; ctx.shadowBlur=12; ctx.fillStyle="#fff"; ctx.fill(); ctx.shadowBlur=0; // Draw a spin indicator (direction of rotation) ctx.save(); ctx.translate(this.pos.x, this.pos.y); ctx.rotate(this.spin); ctx.strokeStyle="#19f"; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(0,0,this.radius*0.68,0,Math.PI*0.5); ctx.stroke(); ctx.restore(); ctx.restore(); } } /* ========== MAIN GAME LOGIC ========== */ const canvas = document.getElementById('pong'); const ctx = canvas.getContext('2d'); let width = window.innerWidth, height = window.innerHeight; canvas.width = width; canvas.height = height; // Perlin Noise instance for dynamic background const perlin = new PerlinNoise(Math.floor(Math.random()*99999)); // Ball, Paddles, Scores let ball = new Ball(width/2, height/2, 13); let paddleL = new Paddle(38, height/2, 18, height/5, true); let paddleR = new Paddle(width-38, height/2, 18, height/5, false); let score = [0,0]; let keys = {}; let slowmo = false; // Player/AI Control let paddleLSpeed = 0; let aiTargetY = height/2; let aiTrackStrength = 0.15; // For timing/gameplay let lastTimestamp = 0; // Event listeners document.addEventListener('keydown', e=>{ keys[e.code]=1; }); document.addEventListener('keyup', e=>{ keys[e.code]=0; }); window.addEventListener('resize', ()=>{ width=window.innerWidth; height=window.innerHeight; canvas.width = width; canvas.height = height; }); /* ========== SLOW MOTION ========== */ const SLOWMO_KEY = "ShiftLeft"; // Hold L-Shift let slowmoScale = 1.0; function drawDynamicBackground() { // Perlin noise flow field: swirl moving over time & influenced by score let t = performance.now()*0.00013; let totalScore = score[0]+score[1]; // Map scores to color pairs let colA = [65+score[0]*36, 44+score[1]*15, 127+totalScore*15]; let colB = [30+score[1]*34, 110+score[0]*34, 90+totalScore*13]; // Generate bitmap let img = ctx.createImageData(width, height); let data = img.data; // Sample grid spacing (for perf: not 1:1 unless needed) const fidelity = Math.max(1,Math.round(Math.max(width,height)/420)); for(let y=0;y<height;y+=fidelity) for(let x=0;x<width;x+=fidelity){ // Swirling coordinates let nx = (x/width-0.5), ny=(y/height-0.5); let swirl = Math.sin(t + 4*nx + 2*ny + perlin.noise(3*nx+totalScore, 3*ny+t)); let n = perlin.noise(2*nx + t, 2*ny-t + swirl*0.3 + totalScore*0.17); // Map n:0-1 to interpolate color let r = Math.floor(colA[0]*n+colB[0]*(1-n)); let g = Math.floor(colA[1]*n+colB[1]*(1-n)); let b = Math.floor(colA[2]*n+colB[2]*(1-n)); for(let dy=0;dy<fidelity;++dy) for(let dx=0;dx<fidelity;++dx){ let px = (y+dy)*width+(x+dx); if(px*4 >= data.length) continue; data[px*4] = r; data[px*4+1] = g; data[px*4+2] = b; data[px*4+3] = 255; } } ctx.putImageData(img, 0,0); } /* ========== GAMELOOP ========== */ function gameLoop(ts) { let dt = (lastTimestamp ? (ts - lastTimestamp)/1000 : 0.01); lastTimestamp = ts; // Slowmo slowmo = !!keys[SLOWMO_KEY]; let targetSlow = slowmo ? 0.20 : 1.0; slowmoScale += (targetSlow - slowmoScale)*0.21; dt *= slowmoScale; /* --- UPDATE --- */ // Player movement (W/S or ARROWUP/ARROWDOWN) let move=0; if(keys["KeyW"]||keys["ArrowUp"]) move-=1; if(keys["KeyS"]||keys["ArrowDown"]) move+=1; paddleLSpeed += (move*425 - paddleLSpeed)*0.33; paddleL.moveTo(paddleL.basePos.y+paddleLSpeed*dt, height); // AI movement (track ball with smoothing) let aiGoal = ball.pos.y + (Math.random()-0.5)*20 + Math.sin(ts*0.0004)*36; aiTargetY += (aiGoal-aiTargetY)*aiTrackStrength; paddleR.moveTo(aiTargetY, height); // Update paddles paddleL.update(dt); paddleR.update(dt); // Update ball ball.update(dt); // Wall bounce (top/bottom) if(ball.pos.y-ball.radius < 0){ ball.pos.y=ball.radius; ball.vel.y*=-1; // Add a little randomness and spin loss ball.vel.x += (Math.random()-0.5)*38; ball.angularVel*=0.88; } if(ball.pos.y+ball.radius > height){ ball.pos.y=height-ball.radius; ball.vel.y*=-1; ball.vel.x += (Math.random()-0.5)*38; ball.angularVel*=0.88; } // Paddle collision function collideBallPaddle(paddle, isLeft) { let segs = paddle.getSegments(); let closest = {ds:1e9, k:-1, seg:null, proj:null, coef:-1}; for(let i=0;i<segs.length;++i){ let [a, b, t0] = segs[i]; // Line (a to b), find closest point on segment to ball // Get vector ab, vector ap let ab = Vec2.sub(b,a); let ap = Vec2.sub(ball.pos,a); let abLen = ab.length(); if(abLen < 1e-7) continue; ab.div(abLen); // project ap onto ab let t = ap.dot(ab); t = Math.max(0, Math.min(abLen, t)); let proj = Vec2.add(a, Vec2.mul(ab, t)); let d = Vec2.sub(ball.pos, proj).length(); if(d < closest.ds) { closest = {ds:d, k:i, seg:[a,b], proj:proj, coef:t0+(t/abLen)*(1/(segs.length-1))}; } } // If closest approach < radius + thickness/2 => collision let colW= paddle.width*0.55 + 1.5; if(closest.ds < ball.radius + colW) { // Reflect: get surface normal (ab.perp), ensure correct direction let a=closest.seg[0], b=closest.seg[1]; let ab = Vec2.sub(b,a); let normal = ab.perp().norm(); if((isLeft && normal.x<0) || (!isLeft && normal.x>0)) normal.mul(-1); // Outlet: add in spin/ricochet let vrel = Vec2.sub(ball.vel, Vec2.mul(normal, normal.dot(ball.vel))); // Add effect of spin on angle let spinEff = ball.angularVel * 28; // amount of tangent vel from spin vrel.add(Vec2.mul(normal.perp(), spinEff)); // Now reflect main velocity across normal let v = Vec2.sub(ball.vel, Vec2.mul(normal, 2*normal.dot(ball.vel))); // Friction/energy loss (none if moving nearly perpendicular) let ric = 0.95+Math.abs(normal.dot(ball.vel)/ball.vel.length())*0.045; v.mul(ric); // Add tangent (from vrel) v.add(Vec2.mul(normal.perp(), spinEff)); // Move ball out of paddle (avoid sticking) ball.pos = Vec2.add(closest.proj, Vec2.mul(normal, ball.radius+colW+0.6)); ball.vel = v; // Apply change to spin based on tangent velocity difference and normal impulse // If user hits up/down, can apply more spin (if colliding at top/bottom of paddle) let paddleV = isLeft ? paddleLSpeed : (aiTargetY - paddleR.basePos.y)/dt; // 'contact' is -1 (paddle top) to +1 (bottom) let contact = (closest.coef-0.5)*2; let spinInc = contact* (ball.vel.length()/210) + paddleV*0.004; ball.angularVel += spinInc; // Deform paddle paddle.applyDeformation(ball, closest.coef, 0.22); return true; } return false; } // Left/Right collision, goal (reset on miss) let goal = false, scoredPlayer = -1; if(ball.pos.x-ball.radius < 0) { // Missed left score[1]++; scoredPlayer=1; goal=true; } else if(ball.pos.x+ball.radius > width) { // Missed right score[0]++; scoredPlayer=0; goal=true; } else { collideBallPaddle(paddleL, true); collideBallPaddle(paddleR, false); } if(goal){ // Reset ball let dir = scoredPlayer===0 ? 1 : -1; ball = new Ball(width/2, height/2, 13); ball.vel = new Vec2(dir*(320+Math.random()*40), (Math.random()-0.5)*230); ball.angularVel = (Math.random()-0.5)*3; paddleL.basePos.y=height/2; paddleR.basePos.y=height/2; aiTargetY = height/2; lastTimestamp=ts+200; // slight pause when scoring } /* --- DRAW --- */ drawDynamicBackground(); // Middle line ctx.save(); ctx.globalAlpha=0.22; for(let i=0;i<33;++i){ ctx.beginPath(); ctx.arc(width/2, height*i/32, 5.9, 0, Math.PI*2); ctx.fillStyle="#fff"; ctx.fill(); } ctx.globalAlpha=1.0; ctx.restore(); paddleL.draw(ctx); paddleR.draw(ctx); ball.draw(ctx); // Scores ctx.save(); ctx.font = `bold ${Math.round(height/9)}px 'SF Mono', 'Fira Mono', 'Consolas', monospace`; ctx.textAlign="center"; ctx.globalAlpha = 0.8+0.15*Math.sin(ts*0.0026); // Side fade ctx.shadowColor="#000c"; ctx.shadowBlur=14; ctx.fillStyle="#fff"; ctx.fillText( score[0], width/2 - width/8, height/6 ); ctx.fillText( score[1], width/2 + width/8, height/6 ); ctx.globalAlpha=1.0; ctx.shadowBlur=0; ctx.font = "bold 24px monospace"; ctx.fillStyle="#eee"; if(slowmoScale<0.75){ ctx.save(); ctx.globalAlpha = 0.55 + 0.5*(0.7-slowmoScale); ctx.fillText( "SLOW-MO", width/2, height*0.21 ); ctx.restore(); } ctx.restore(); requestAnimationFrame(gameLoop); } // Start! requestAnimationFrame(gameLoop); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <title>Transcendental Pong</title> <style> html, body { margin: 0; overflow: hidden; background: black; } canvas { display: block; } </style> </head> <body> <canvas id="game"></canvas> <script> // Vector Math Library class Vector2 { constructor(x=0, y=0) { this.x = x; this.y = y; } add(v) { return new Vector2(this.x + v.x, this.y + v.y) } sub(v) { return new Vector2(this.x - v.x, this.y - v.y) } mul(s) { return new Vector2(this.x * s, this.y * s) } div(s) { return this.mul(1/s) } mag() { return Math.hypot(this.x, this.y) } norm() { return this.div(this.mag() || 1) } dot(v) { return this.x * v.x + this.y * v.y } angle() { return Math.atan2(this.y, this.x) } rotate(theta) { const sin = Math.sin(theta); const cos = Math.cos(theta); return new Vector2( this.x * cos - this.y * sin, this.x * sin + this.y * cos ); } lerp(to, t) { return new Vector2( this.x + (to.x - this.x) * t, this.y + (to.y - this.y) * t ); } } // Perlin Noise Implementation class Perlin { constructor() { this.perm = new Uint8Array(512); const p = new Uint8Array(256); for (let i = 0; i < 256; i++) p[i] = i; let seed = 1; for (let i = 255; i > 0; i--) { seed = (seed * 173958.523) % 1; let j = Math.floor(seed * i); let temp = p[i]; p[i] = p[j]; p[j] = temp; } for (let i = 0; i < 512; i++) this.perm[i] = p[i % 256]; } noise(x, y) { const xi = Math.floor(x) & 255; const yi = Math.floor(y) & 255; const xf = x - Math.floor(x); const yf = y - Math.floor(y); const u = this.fade(xf); const v = this.fade(yf); const a = this.perm[xi] + yi; const aa = this.perm[a] % 12; const ab = this.perm[a + 1] % 12; const b = this.perm[xi + 1] + yi; const ba = this.perm[b] % 12; const bb = this.perm[b + 1] % 12; const g00 = this.gradient(aa, xf, yf); const g01 = this.gradient(ab, xf, yf - 1); const g10 = this.gradient(ba, xf - 1, yf); const g11 = this.gradient(bb, xf - 1, yf - 1); return this.lerp( this.lerp(g00, g10, u), this.lerp(g01, g11, u), v ); } fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } lerp(a, b, x) { return a + x * (b - a); } gradient(hash, x, y) { const h = hash % 4; const u = h < 2 ? x : -y; const v = h % 2 === 0 ? y : x; return u + v; } } // Canvas Setup const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); let width, height, pixelRatio; function resizeCanvas() { pixelRatio = window.devicePixelRatio || 1; width = canvas.width = innerWidth; height = canvas.height = innerHeight; ctx.scale(pixelRatio, pixelRatio); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); // Game State const GameState = { score: { left: 0, right: 0 }, isSlowed: false }; // Physics Engine class SpringParticle { constructor(pos, mass=0.1) { this.pos = new Vector2(pos.x, pos.y); this.prev = new Vector2(pos.x, pos.y); this.acc = new Vector2(0, 0); this.mass = mass; } update(delta=0.01) { let vel = this.pos.sub(this.prev).mul(delta ? 1/delta : 1); this.prev = this.pos; this.pos = this.pos.add(vel.add(this.acc.mul(delta * delta)).mul(this.mass)); this.acc = new Vector2(0, 0); } applyForce(force) { this.acc = this.acc.add(force.mul(this.mass)); } constrain(min, max) { this.pos = new Vector2( Math.max(Math.min(this.pos.x, max.x), min.x), Math.max(Math.min(this.pos.y, max.y), min.y) ); } } class SpringConstraint { constructor(p1, p2, restLength=60, stiffness=0.1, damping=0.01) { this.p1 = p1; this.p2 = p2; this.restLength = restLength; this.stiffness = stiffness; this.damping = damping; } update(delta=0.01) { const deltaVec = this.p2.pos.sub(this.p1.pos); const dist = deltaVec.mag(); const diff = dist - this.restLength; const percent = (diff / dist) * this.stiffness; const correction = deltaVec.mul(percent * 0.5); this.p1.pos = this.p1.pos.add(correction); this.p2.pos = this.p2.pos.sub(correction); const vel = this.p2.prev.sub(this.p1.prev); const dir = deltaVec.norm(); const proj = vel.dot(dir); if (proj > 0) return; this.p2.applyForce(dir.mul(-proj * this.damping)); this.p1.applyForce(dir.mul(proj * this.damping)); } } // Paddle Physics class Paddle { constructor(x, isRight, options={}) { this.isRight = isRight; this.baseX = x; this.points = []; this.constraints = []; for (let i = 0; i < 5; i++) { const y = (height/2 - 75) + i * 30; this.points.push(new SpringParticle(new Vector2(x, y))); } // Structural constraints for (let i = 0; i < this.points.length - 1; i++) { this.constraints.push(new SpringConstraint( this.points[i], this.points[i + 1], 30, 0.1, 0.001 )); } // Diagonal constraints for stability for (let i = 0; i < this.points.length - 2; i++) { this.constraints.push(new SpringConstraint( this.points[i], this.points[i + 2], 60, 0.05, 0.0005 )); } } update(delta) { // Apply gravity (simulate weight) for (const point of this.points) { point.applyForce(new Vector2(0, 0.5)); } // Update constraints for (const constraint of this.constraints) { constraint.update(delta); } // Keep base position constrained for (const point of this.points) { point.constrain( new Vector2(0, 0), new Vector2(width, height) ); } // Update positions for (const point of this.points) { point.update(delta); } } draw(ctx) { ctx.beginPath(); ctx.moveTo(this.points[0].pos.x, this.points[0].pos.y); for (let i = 1; i < this.points.length; i++) { ctx.lineTo(this.points[i].pos.x, this.points[i].pos.y); } ctx.lineWidth = 12; ctx.strokeStyle = '#ffffff'; ctx.lineCap = 'round'; ctx.stroke(); } bounce(ball) { const center = this.points[Math.floor(this.points.length/2)].pos; let closestPoint = { dist: Infinity }; // Check all segments for (let i = 0; i < this.points.length - 1; i++) { const a = this.points[i]; const b = this.points[i + 1]; const delta = b.pos.sub(a.pos); const len = delta.mag(); const dot = ((ball.pos.x - a.pos.x) * (b.pos.x - a.pos.x) + (ball.pos.y - a.pos.y) * (b.pos.y - a.pos.y)) / Math.max(len * len, 1); const clamped = Math.min(Math.max(dot, 0), 1); const projection = a.pos.add(delta.mul(clamped)); const distance = ball.pos.sub(projection).mag(); if (distance < ball.radius) { const normal = ball.pos.sub(projection).norm(); const tangent = new Vector2(normal.y, -normal.x).mul( ball.spin * 0.05 ); // Spin calculation based on paddle motion (simplified) const hitOffset = (projection.y - center.y) / 10; ball.vel = ball.vel.add(normal.mul(0.1)); ball.vel = ball.vel.add(tangent); ball.spin += hitOffset * ball.vel.mag() * 0.1 * (this.isRight ? 1 : -1); // Apply paddle deformation force const force = normal.mul(-ball.vel.mag() * 0.5); // Hit specific point a.applyForce(force.mul(0.7)); b.applyForce(force.mul(0.7)); // Spread effect to neighbors const neighborForce = force.mul(0.3); if (i > 0) this.points[i-1].applyForce(neighborForce); if (i < this.points.length - 2) this.points[i+2].applyForce(neighborForce); // Reset ball position ball.pos = projection.add(normal.mul(ball.radius)); return true; } } return false; } } // Ball Physics class Ball { constructor() { this.reset(); } reset() { this.pos = new Vector2(width/2, height/2); this.vel = new Vector2(3, 2).rotate(Math.random() * Math.PI * 2); this.radius = 8; this.spin = 0; } update(delta) { this.pos = this.pos.add(this.vel.mul(delta)); this.radius = 8 + Math.sin(Date.now()*0.001) * 2; // Edge collision if (this.pos.y < this.radius || this.pos.y > height - this.radius) { this.vel.y = -this.vel.y; const offset = (Date.now() / 1000) % 1 * Math.PI * 2; this.pos.y += Math.sin(offset) * 0.2; } } draw(ctx) { ctx.save(); ctx.translate(this.pos.x, this.pos.y); ctx.rotate(this.spin * 0.01); ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, 2 * Math.PI); ctx.fillStyle = '#ffffff'; ctx.shadowBlur = 20; ctx.shadowColor = '#ffffff'; ctx.fill(); // Spin indicator ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, this.radius*0.8, -Math.PI/2 + this.spin*0.6, -Math.PI/2 + this.spin*0.6 + Math.PI*2*0.3); ctx.lineWidth = 2; ctx.strokeStyle = '#00ffff'; ctx.stroke(); ctx.restore(); } } // Game Loop const leftPaddle = new Paddle(20, false); const rightPaddle = new Paddle(width - 20, true); const ball = new Ball(); const perlin = new Perlin(); let keys = {}; window.addEventListener('keydown', e => { keys[e.code] = true; if (e.code === 'Space') GameState.isSlowed = true; }); window.addEventListener('keyup', e => { keys[e.code] = false; if (e.code === 'Space') GameState.isSlowed = false; }); function update(delta) { const timeFactor = GameState.isSlowed ? 0.3 : 1; const dt = delta * timeFactor; // Move paddles with keyboard (simple AI if no input) const paddleSpeed = 5; if (keys['KeyW']) leftPaddle.points[2].pos.y -= paddleSpeed; if (keys['KeyS']) leftPaddle.points[2].pos.y += paddleSpeed; if (keys['ArrowUp']) rightPaddle.points[2].pos.y -= paddleSpeed; if (keys['ArrowDown']) rightPaddle.points[2].pos.y += paddleSpeed; // Update physics leftPaddle.update(dt); rightPaddle.update(dt); // Ball physics ball.update(dt); // Check collisions if (ball.pos.x < 0 || ball.pos.x > width) { if (ball.pos.x < 0) GameState.score.right++; else GameState.score.left++; ball.reset(); } if (leftPaddle.bounce(ball) || rightPaddle.bounce(ball)) { // Ball sound effect } } function drawGradient() { const imageData = ctx.createImageData(width, height); const data = imageData.data; const gradientProgress = (GameState.score.left + GameState.score.right) * 0.1; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; const nx = x/width * 20; const ny = y/height * 20 + gradientProgress; const val = Math.min(0.5 + perlin.noise(nx, ny) * 0.5, 0.9); let color; const phase = (Date.now() * 0.001 + nx + ny) % (2 * Math.PI); if (val > 0.7) { color = { r: 255 * (0.5 + Math.sin(phase + 0) * 0.5), g: 100 * (0.5 + Math.sin(phase + 2) * 0.5), b: 50 * (0.5 + Math.sin(phase + 4) * 0.5) }; } else { color = { r: 255 * (0.3 + Math.sin(phase + 0) * 0.1), g: 255 * (0.3 + Math.sin(phase + 2) * 0.1), b: 255 * (0.3 + Math.sin(phase + 4) * 0.1) }; } data[index] = color.r; data[index + 1] = color.g; data[index + 2] = color.b; data[index + 3] = 255; } } ctx.putImageData(imageData, 0, 0); } function render() { ctx.clearRect(0, 0, width, height); drawGradient(); // Score counter ctx.fillStyle = '#ffffff'; ctx.font = '20px monospace'; ctx.fillText(GameState.score.left, width/2 - 40, 30); ctx.fillText(GameState.score.right, width/2 + 40 - 15, 30); // Net line for (let y = 0; y < height; y += 30) { ctx.fillStyle = '#888'; ctx.fillRect(width/2-1, y, 2, 15); } // Entities leftPaddle.draw(ctx); rightPaddle.draw(ctx); ball.draw(ctx); } let lastTime = 0; function gameLoop(timestamp) { const delta = (timestamp - lastTime) / 10; lastTime = timestamp; update(delta); render(); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); </script> </body> </html> ```
Below is a minimalist version of Pong implemented using HTML5 Canvas and pure JavaScript with the specified features: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Modern Pong</title> <style> canvas { background-color: #000; } </style> </head> <body> <canvas id="gameCanvas" width="800" height="600"></canvas> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const PADDLE_WIDTH = 15; const PADDLE_HEIGHT = 100; const BALL_RADIUS = 10; const PADDLE_VEL = 10; const PADDLE_SPRING_STRENGTH = 0.1; const PADDLE_SPRING_DAMPING = 0.1; let playerPaddleY = canvas.height / 2 - PADDLE_HEIGHT / 2; let aiPaddleY = canvas.height / 2 - PADDLE_HEIGHT / 2; let ball = { x: canvas.width / 2, y: canvas.height / 2, dx: 5, dy: 5, spin: 0 }; let playerScore = 0; let aiScore = 0; let slowMotion = false; function drawPaddle(x, y) { ctx.fillRect(x, y, PADDLE_WIDTH, PADDLE_HEIGHT); } function drawBall(x, y) { ctx.beginPath(); ctx.arc(x, y, BALL_RADIUS, 0, 2 * Math.PI); ctx.fill(); } function updateBall() { if (ball.y + ball.dy < BALL_RADIUS || ball.y + ball.dy > canvas.height - BALL_RADIUS) { ball.dy = -ball.dy; } if (ball.x + ball.dx < 0 || ball.x + ball.dx > canvas.width) { if (ball.x + ball.dx < 0) { aiScore++; } else { playerScore++; } resetBall(); } if ((ball.x + ball.dx < PADDLE_WIDTH && ball.y > playerPaddleY && ball.y < playerPaddleY + PADDLE_HEIGHT) || (ball.x + ball.dx > canvas.width - PADDLE_WIDTH && ball.y > aiPaddleY && ball.y < aiPaddleY + PADDLE_HEIGHT)) { ball.dx = -ball.dx; ball.spin = (ball.y - (playerPaddleY + PADDLE_HEIGHT / 2)) / PADDLE_HEIGHT; } ball.x += ball.dx; ball.y += ball.dy; } function resetBall() { ball.x = canvas.width / 2; ball.y = canvas.height / 2; ball.dx = -ball.dx; ball.dy = 5; ball.spin = 0; } function updatePaddles() { if (ball.dy > 0) { aiPaddleY += Math.min(PADDLE_VEL, ball.y - aiPaddleY - PADDLE_HEIGHT / 2); } else { aiPaddleY -= Math.min(PADDLE_VEL, aiPaddleY + PADDLE_HEIGHT / 2 - ball.y); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawPaddle(0, playerPaddleY); drawPaddle(canvas.width - PADDLE_WIDTH, aiPaddleY); drawBall(ball.x, ball.y); ctx.font = "48px serif"; ctx.fillText(playerScore, 100, 100); ctx.fillText(aiScore, canvas.width - 100, 100); } function update() { updateBall(); updatePaddles(); draw(); requestAnimationFrame(update); } document.addEventListener('keydown', (event) => { if (event.key === 'ArrowUp') { playerPaddleY -= PADDLE_VEL; } else if (event.key === 'ArrowDown') { playerPaddleY += PADDLE_VEL; } else if (event.key === ' ') { slowMotion = true; } }); document.addEventListener('keyup', (event) => { if (event.key === ' ') { slowMotion = false; } }); function animate(timestamp) { if (!slowMotion) { update(); } else { setTimeout(() => requestAnimationFrame(animate), 50); } } requestAnimationFrame(animate); </script> </body> </html> ``` This implementation covers the basic operations for a minimalist Pong game with some modern features: - Paddles move vertically and have a simple form of AI for the opponent. - The ball bounces off the top and bottom of the canvas. - Ball physics include a slight spin effect that affects ricochet angles. - The game tracks the player and AI scores. - Holding the spacebar triggers a slow motion mode. The background and paddle deformation suggested (rubber effect using spring-damper systems and dynamic gradient background using Perlin noise) are complex features that would require a more extensive implementation of physics and rendering logic, including more advanced vector math and noise algorithms. The above code is a simplified starting point that you can build upon to add those features.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Minimalist Modern Pong</title> <style> html, body { margin:0; padding:0; overflow:hidden; background:#000; height:100%; width:100%; user-select:none; -webkit-touch-callout:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; } canvas { display:block; margin:0 auto; background:#000; image-rendering: pixelated; } </style> </head> <body> <canvas id="pong"></canvas> <script> (() => { // ==== Vector2 Class ==== class Vec2 { constructor(x=0,y=0){this.x=x;this.y=y;} clone(){return new Vec2(this.x,this.y);} add(v){this.x+=v.x;this.y+=v.y;return this;} sub(v){this.x-=v.x;this.y-=v.y;return this;} mul(s){this.x*=s;this.y*=s;return this;} div(s){this.x/=s;this.y/=s;return this;} length(){return Math.sqrt(this.x*this.x+this.y*this.y);} lengthSq(){return this.x*this.x+this.y*this.y;} normalize(){let len=this.length(); if(len>0){this.div(len);}return this;} dot(v){return this.x*v.x+this.y*v.y;} perp(){return new Vec2(-this.y,this.x);} // perpendicular vector static add(a,b){return new Vec2(a.x+b.x,a.y+b.y);} static sub(a,b){return new Vec2(a.x-b.x,a.y-b.y);} static mul(v,s){return new Vec2(v.x*s,v.y*s);} static lerp(a,b,t){return new Vec2(a.x*(1-t)+b.x*t,a.y*(1-t)+b.y*t);} } // ==== Simplex Noise / Perlin Noise Implementation ==== // We'll use a classic Simplex noise 2D for swirling dynamic bg. // Implementation adapted for quick use. class SimplexNoise { constructor(random=Math.random){ this.p = new Uint8Array(256); for(let i=0;i<256;i++) this.p[i] = i; for(let i=255;i>0;i--){ let r = Math.floor(random()*(i+1)); let tmp = this.p[i]; this.p[i] = this.p[r]; this.p[r] = tmp; } this.perm = new Uint8Array(512); for(let i=0;i<512;i++) this.perm[i]=this.p[i & 255]; } // Gradient directions for 2D grad2 = [ [1,1],[-1,1],[1,-1],[-1,-1], [1,0],[-1,0],[0,1],[0,-1] ]; dot(g,x,y){return g[0]*x+g[1]*y;} noise2D(xin,yin){ const F2= 0.366025403; // (Math.sqrt(3)-1)/2 const G2= 0.211324865; // (3-Math.sqrt(3))/6 let s = (xin+yin)*F2; let i = Math.floor(xin+s); let j = Math.floor(yin+s); let t = (i+j)*G2; let X0 = i - t; let Y0 = j - t; let x0 = xin - X0; let y0 = yin - Y0; let i1,j1; if(x0>y0){i1=1;j1=0;}else{i1=0;j1=1;} let x1 = x0 - i1 + G2; let y1 = y0 - j1 + G2; let x2 = x0 - 1 + 2*G2; let y2 = y0 - 1 + 2*G2; let ii = i & 255; let jj = j & 255; let gi0 = this.perm[ii+this.perm[jj]] % 8; let gi1 = this.perm[ii+i1+this.perm[jj+j1]] % 8; let gi2 = this.perm[ii+1+this.perm[jj+1]] % 8; function calc(t,x,y,g,noiseDot){ let tt = 0.5 - x*x - y*y; if(tt<0) return 0; tt *= tt; return tt*tt*noiseDot(g,x,y); } let n0 = calc(t, x0, y0, this.grad2[gi0], this.dot.bind(this)); let n1 = calc(t, x1, y1, this.grad2[gi1], this.dot.bind(this)); let n2 = calc(t, x2, y2, this.grad2[gi2], this.dot.bind(this)); // scale result to approx [-1,1] return 70*(n0+n1+n2); } } // ==== Spring-Damper System ==== // We'll model the paddle as 3 points along y, connected by springs to origin position & neighbors. // The ball impact sends impulse to these points, which oscillate with spring-damper physics. class SpringPoint { constructor(restY){ this.restY = restY; this.y = restY; this.v = 0; this.mass = 1; } } class RubberPaddle { constructor(x, height, segments=3){ this.x = x; this.height = height; this.segments = segments; this.points = []; this.restYs = []; // vertically distribute segments equally around center for(let i=0;i<segments;i++){ let y = (i/(segments-1)-0.5)*height; this.points.push(new SpringPoint(y)); this.restYs.push(y); } // parameters for springs: this.kRest = 300; // spring constant to rest position this.kNeighbor = 150; // spring constant between neighbors this.damping = 20; // damping constant } // update points physics timestep dt update(dt){ let pts = this.points; for(let i=0;i<this.segments;i++){ let p = pts[i]; // spring to rest position let force = -this.kRest * (p.y - p.restY); // spring to neighbors if(i>0) force += -this.kNeighbor * (p.y - pts[i-1].y); if(i<this.segments-1) force += -this.kNeighbor * (p.y - pts[i+1].y); // damping force force += -this.damping*p.v; // acceleration let a = force/p.mass; p.v += a*dt; } // update position for(let p of pts) { p.y += p.v*dt; } } // add impulse to points based on ball velocity vertical component near y applyImpulse(relativeY, impulse){ // relativeY in paddle space (-height/2 to height/2) // distribute impulse weighted by distance // Normalize to [0...segments-1] and apply impulse diminishing by distance for(let i=0;i<this.segments;i++){ let p = this.points[i]; let dist = Math.abs(p.y - relativeY); // influence decreases exponentially with distance let influence = Math.exp(-dist*10); p.v += impulse*influence; } } // draw paddle shape with rubber deformation using bezier curves draw(ctx, centerY){ let pts = this.points; let width = 10; ctx.fillStyle = '#ddd'; ctx.beginPath(); // We'll create a smooth polygon with quadratic curves connecting points offset by the deformation // Left edge at constant x ctx.moveTo(this.x - width/2, centerY + pts[0].y); // Curve down the left side for(let i=1; i<pts.length; i++){ let cpX = this.x - width*0.8; let cpY = centerY + (pts[i-1].y + pts[i].y)/2; ctx.quadraticCurveTo(cpX, cpY, this.x - width/2, centerY + pts[i].y); } // Bottom right corner ctx.lineTo(this.x + width/2, centerY + pts[pts.length-1].y); // Curve up the right side for(let i=pts.length-2; i>=0; i--){ let cpX = this.x + width*0.8; let cpY = centerY + (pts[i+1].y + pts[i].y)/2; ctx.quadraticCurveTo(cpX, cpY, this.x + width/2, centerY + pts[i].y); } ctx.closePath(); ctx.fill(); } } // ==== Ball Class ==== // Position, velocity, spin (angular velocity, affects ricochet angle) class Ball { constructor(x,y,r){ this.pos = new Vec2(x,y); this.r = r; this.vel = new Vec2(150, 100); // px/s initial velocity this.spin = 0; // angular velocity (could represent spin/magnitude) this.spinEffect = 0.3; // influence of spin on bounce angle this.mass = 1; } update(dt){ this.pos.add(Vec2.mul(this.vel, dt)); } draw(ctx){ let grd = ctx.createRadialGradient(this.pos.x, this.pos.y, this.r*0.2, this.pos.x, this.pos.y, this.r); grd.addColorStop(0, `hsl(${(this.spin*360)%360}, 80%, 60%)`); grd.addColorStop(1, '#ccc'); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.r, 0, Math.PI*2); ctx.fill(); } } // ==== Game Setup ==== const canvas = document.getElementById('pong'); const ctx = canvas.getContext('2d'); let W,H; function resize(){ W = window.innerWidth; H = window.innerHeight; canvas.width = W; canvas.height = H; } window.addEventListener('resize', resize); resize(); // Game state: // Paddles const paddleHeight = 120; const paddleMargin = 40; const leftPaddle = new RubberPaddle(paddleMargin, paddleHeight, 5); const rightPaddle = new RubberPaddle(W - paddleMargin, paddleHeight, 5); let leftY = H/2; let rightY = H/2; // Ball const ball = new Ball(W/2, H/2, 12); // Scores let scores = [0,0]; // Slow motion toggle let slowMotion = false; window.addEventListener('keydown', e => { if(e.key === 'Shift') slowMotion = true; }); window.addEventListener('keyup', e=>{ if(e.key === 'Shift') slowMotion = false; }); // Custom vector math to add spin effect on ricochet. // When ball hits paddle, spin changes proportional to difference in paddle segment velocity and ball vertical velocity. // Ricochet angle affected by spin. // ==== Background class with swirling procedural gradient ==== // We'll paint a fullscreen gradient rectangles with colors based on Perlin noise function shaped swirl that is "time + score" dependent const noise = new SimplexNoise(); function lerpColor(a,b,t){ return [ a[0]*(1-t)+b[0]*t, a[1]*(1-t)+b[1]*t, a[2]*(1-t)+b[2]*t ]; } function rgbToCss(c){ return `rgb(${c[0]|0},${c[1]|0},${c[2]|0})`; } // Base colors for scores for interpolation const baseColors = [ [30,30,60], // dark blue [60,30,30], // dark red [30,60,30], // dark green [60,60,30], // olive [30,60,60], // teal ]; function backgroundGradient(time, score){ // We draw gradient in a grid of small rectangles with color modulated by swirling noise: // because canvas gradient cannot do complex swirling noise, we'll fill a pixelated noise map scaled up for showing effect. const granularity = 15; const cols = Math.ceil(W/granularity); const rows = Math.ceil(H/granularity); // Based on scores interpolate color palette let idxA = score[0] % baseColors.length; let idxB = (score[1]) % baseColors.length; let colorA = baseColors[idxA]; let colorB = baseColors[idxB]; // mix colors by ratio (score[0]/ (score[0]+score[1]+1)) let total = score[0]+score[1]+1; let tcolor = score[0]/total; let baseColor = lerpColor(colorA,colorB, tcolor); // We'll create a swirling noise centered at canvas center, rotating over time let cx = W/2, cy = H/2; for(let y=0;y<rows;y++){ for(let x=0;x<cols;x++){ let px = x*granularity; let py = y*granularity; let dx = px - cx, dy = py - cy; let angle = Math.atan2(dy,dx); let radius = Math.sqrt(dx*dx + dy*dy) / 200; // swirl is angle plus time rotation let swirl = angle*3 + time*0.6; // noise coord mixing radius and swirl + score-based offset let n = noise.noise2D(radius*Math.cos(swirl)+time*0.1, radius*Math.sin(swirl)+time*0.1); n = (n+1)/2; // 0-1 // intensity modulated by radius (fade out at edges) let intensity = 1-Math.min(radius/6,1); let col = lerpColor(baseColor, [baseColor[0]*0.8+40*n, baseColor[1]*0.8+60*n, baseColor[2]*0.8+90*n], n*intensity); ctx.fillStyle = rgbToCss(col); ctx.fillRect(px, py, granularity, granularity); } } } // ==== Input handling ==== // we'll map W/S for left paddle, ArrowUp/ArrowDown for right let input = { leftUp:false, leftDown:false, rightUp:false, rightDown:false }; window.addEventListener('keydown', e=>{ switch(e.key){ case 'w': input.leftUp = true; break; case 's': input.leftDown = true; break; case 'ArrowUp': input.rightUp = true; break; case 'ArrowDown': input.rightDown = true; break; } }); window.addEventListener('keyup', e=>{ switch(e.key){ case 'w': input.leftUp = false; break; case 's': input.leftDown = false; break; case 'ArrowUp': input.rightUp = false; break; case 'ArrowDown': input.rightDown = false; break; } }); // ==== Game Loop ==== let lastTime = 0; // Max velocity for paddle Y (px/s) const PADDLE_SPEED = 280; function update(dt){ // Paddle movement clamp if(input.leftUp) leftY -= PADDLE_SPEED*dt; if(input.leftDown) leftY += PADDLE_SPEED*dt; if(input.rightUp) rightY -= PADDLE_SPEED*dt; if(input.rightDown) rightY += PADDLE_SPEED*dt; leftY = Math.min(Math.max(leftY, paddleHeight/2), H - paddleHeight/2); rightY = Math.min(Math.max(rightY, paddleHeight/2), H - paddleHeight/2); // Update paddles spring physics leftPaddle.update(dt); rightPaddle.update(dt); // Move ball ball.update(dt); // Ball collision with top/bottom walls if(ball.pos.y - ball.r < 0){ ball.pos.y = ball.r; ball.vel.y = -ball.vel.y * 0.9; ball.spin *= 0.8; } else if(ball.pos.y + ball.r > H){ ball.pos.y = H - ball.r; ball.vel.y = -ball.vel.y * 0.9; ball.spin *= 0.8; } // Ball collision with paddles (rectangular AABB bounds extended by paddle width) // We find segment index where collision happens: const paddleWidth = 10; // Left paddle AABB if(ball.pos.x - ball.r < leftPaddle.x + paddleWidth/2){ if(ball.pos.y > leftY - paddleHeight/2 - ball.r && ball.pos.y < leftY + paddleHeight/2 + ball.r){ if(ball.vel.x < 0){ // moving left to right ball.pos.x = leftPaddle.x + paddleWidth/2 + ball.r; // Find local Y on paddle coordinate space let localY = ball.pos.y - leftY; // Calculate approximate paddle segment velocity weighted average // For spin effect: estimate v of paddle points near localY weighted by distance let paddleV = 0; let sumW = 0; for(let pt of leftPaddle.points){ const dist = Math.abs(pt.y - localY); const w = Math.exp(-dist*10); paddleV += pt.v * w; sumW += w; } paddleV /= sumW; // Reflect X velocity and add spin effect on angle // Spin updated by difference of paddle vertical velocity and ball vertical velocity // ball.vel.y effectively tangent direction to paddle surface ball.spin += (paddleV - ball.vel.y)*0.005; // adjust velocity direction with small rotation based on spin let speed = ball.vel.length(); let normal = new Vec2(1, 0); // base reflection vector with slight vertical bounce depend on contact point / paddle shape let bounceTangent = (localY/paddleHeight)*1.3; let reflected = new Vec2(normal.x, normal.y + ball.spinEffect * ball.spin + bounceTangent); reflected.normalize(); reflected.mul(speed); ball.vel = reflected; // Apply impulse to paddle points based on ball x velocity magnitude (horizontal bounce force) and impact y location leftPaddle.applyImpulse(localY, ball.vel.y*0.25); } } } // Right paddle if(ball.pos.x + ball.r > rightPaddle.x - paddleWidth/2){ if(ball.pos.y > rightY - paddleHeight/2 - ball.r && ball.pos.y < rightY + paddleHeight/2 + ball.r){ if(ball.vel.x > 0){ // moving right to left (towards right paddle) ball.pos.x = rightPaddle.x - paddleWidth/2 - ball.r; let localY = ball.pos.y - rightY; let paddleV = 0; let sumW = 0; for(let pt of rightPaddle.points){ const dist = Math.abs(pt.y - localY); const w = Math.exp(-dist*10); paddleV += pt.v * w; sumW += w; } paddleV /= sumW; ball.spin += (paddleV - ball.vel.y)*0.005; let speed = ball.vel.length(); let normal = new Vec2(-1, 0); let bounceTangent = (localY/paddleHeight)*1.3; let reflected = new Vec2(normal.x, normal.y + ball.spinEffect * ball.spin + bounceTangent); reflected.normalize(); reflected.mul(speed); ball.vel = reflected; rightPaddle.applyImpulse(localY, ball.vel.y*0.25); } } } // Check scoring, reset ball if out of bounds if(ball.pos.x < -ball.r){ scores[1]++; resetBall(-1); } else if(ball.pos.x > W + ball.r){ scores[0]++; resetBall(1); } } function resetBall(direction = 1){ ball.pos.x = W/2; ball.pos.y = H/2; // launch ball in direction (left or right) with random angle let angle = (Math.random()*Math.PI*0.6) - Math.PI*0.3; // [-54deg..54deg] let speed = 180; ball.vel.x = speed * direction * Math.cos(angle); ball.vel.y = speed * Math.sin(angle); ball.spin = 0; leftY = H/2; rightY = H/2; // Reset paddles deformation instantly for(let p of leftPaddle.points){ p.y = p.restY; p.v = 0; } for(let p of rightPaddle.points){ p.y = p.restY; p.v = 0; } } // ==== Draw Functions ==== function drawScore(){ ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '48px Helvetica, Arial, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(scores[0], W*0.25, 70); ctx.fillText(scores[1], W*0.75, 70); } // ==== Main loop ==== function loop(t){ if(!lastTime) lastTime = t; let dt = (t - lastTime)/1000; lastTime = t; // Slow motion speed factor let speedFactor = slowMotion ? 0.25 : 1; dt *= speedFactor; // Clamp dt max to avoid big jumps on tab focus or slow CPU dt = Math.min(dt, 0.05); // Background backgroundGradient(t*0.002, scores); // Update update(dt); // Draw paddles leftPaddle.draw(ctx, leftY); rightPaddle.draw(ctx, rightY); // Draw ball ball.draw(ctx); // Draw scores drawScore(); // Draw slow motion indicator if(slowMotion){ ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.font = '18px monospace'; ctx.textAlign = 'right'; ctx.fillText('SLOW MO', W - 20, H - 20); } requestAnimationFrame(loop); } // Start game resetBall(1); requestAnimationFrame(loop); })(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Advanced Pong</title> <style> body { margin: 0; overflow: hidden; background: black; } canvas { display: block; } </style> </head> <body> <canvas id="game" width="800" height="600"></canvas> <script> class PerlinNoise { constructor() { this.grad3 = [ [1,1,0], [-1,1,0], [1,-1,0], [-1,-1,0], [1,0,1], [-1,0,1], [1,0,-1], [-1,0,-1], [0,1,1], [0,-1,1], [0,1,-1], [0,-1,-1] ]; this.perm = []; for (let i = 0; i < 256; i++) this.perm[i] = Math.floor(Math.random() * 256); } noise(x, y, z) { const F2 = 0.3333333; const s = (x + y + z) * F2; const i = Math.floor(x + s), j = Math.floor(y + s), k = Math.floor(z + s); const G2 = 2.0 - F2; const ti = i & 255, tj = j & 255, tk = k & 255; const permDi = this.perm[ti] + tj, permDk = this.perm[permDi] + tk; const gv = this.grad3[this.perm[permDk] % 12]; const t = 0.5 - (i + j + k) * 0.5; const n = gv[0] * (x - (i - s)) + gv[1] * (y - (j - s)) + gv[2] * (z - (k - s)); return 48 * n * t * t; } smooth(x, y) { return this.noise(x * 0.01, y * 0.01, 0); } } class Paddle { constructor(x, y, w, h) { this.x = x; this.y = y; this.width = w; this.height = h; this.vy = 0; this.compress = 0; this.compressVel = 0; } update() { const k = 0.05, d = 0.98; this.compressVel = d * this.compressVel + k * (-this.compress); this.compress += this.compressVel; this.y += this.vy; } draw(ctx) { const h = this.height * (1 - 0.35 * this.compress); ctx.fillStyle = "rgba(255,255,255,0.8)"; ctx.fillRect(this.x, this.y, this.width, h); } } class Ball { constructor() { this.reset(); } reset() { this.x = 400; this.y = 300; this.radius = 10; this.vx = (Math.random() > 0.5 ? 1 : -1) * 7; this.vy = (Math.random() - 0.5) * 2; this.spin = 0; } update(dt, scale) { this.x += this.vx * dt * scale; this.y += this.vy * dt * scale; } draw(ctx) { ctx.fillStyle = "#0ff"; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fill(); } } const canvas = document.getElementById("game"); const ctx = canvas.getContext("2d"); let keys = {}; let score = [0, 0]; let timeScale = 1; const perlin = new PerlinNoise(); const leftPaddle = new Paddle(20, 250, 10, 100); const rightPaddle = new Paddle(770, 250, 10, 100); const ball = new Ball(); document.addEventListener("keydown", e => keys[e.key] = true); document.addEventListener("keyup", e => keys[e.key] = false); function update() { leftPaddle.vy = (keys["ArrowUp"] ? -5 : 0) + (keys["ArrowDown"] ? 5 : 0); rightPaddle.vy = (keys["w"] ? -5 : 0) + (keys["s"] ? 5 : 0); if (keys["Shift"]) timeScale = 0.25; leftPaddle.update(); rightPaddle.update(); ball.update(0.016, timeScale); // Paddle collisions if (ball.x - ball.radius < leftPaddle.x + leftPaddle.width && ball.x + ball.radius > leftPaddle.x && ball.y > leftPaddle.y && ball.y < leftPaddle.y + leftPaddle.height) { ball.vx = -ball.vx * 1.05; const relY = ball.y - (leftPaddle.y + leftPaddle.height/2); ball.vy = relY * 0.2; leftPaddle.compress = 30 + Math.abs(ball.vx) * 0.2; ball.spin = Math.max(-1, Math.min(1, relY * 0.1)); } else if (ball.x - ball.radius < 0) { score[1]++; ball.reset(); } if (ball.x + ball.radius > rightPaddle.x && ball.x - ball.radius < rightPaddle.x + rightPaddle.width && ball.y > rightPaddle.y && ball.y < rightPaddle.y + rightPaddle.height) { ball.vx = -ball.vx * 1.05; const relY = ball.y - (rightPaddle.y + rightPaddle.height/2); ball.vy = relY * 0.2; rightPaddle.compress = 30 + Math.abs(ball.vx) * 0.2; ball.spin = -Math.max(-1, Math.min(1, relY * 0.1)); } else if (ball.x + ball.radius > 800) { score[0]++; ball.reset(); } // Wall collision if (ball.y < ball.radius || ball.y > 600 - ball.radius) { ball.vy *= -1; if (ball.spin !== 0) ball.spin *= 0.8; if (Math.abs(ball.vy) > 0.5) ball.vy += ball.spin; } } function drawBackground() { const time = Date.now() * 0.001; const baseHue = 240 + (score[0] + score[1]) * 5 + perlin.smooth(time, 0) * 40; const gradient = ctx.createLinearGradient(0, 0, 800, 600); gradient.addColorStop(0, `hsl(${baseHue}, 80%, 30%)`); gradient.addColorStop(1, `hsl(${(baseHue + 120) % 360}, 80%, 50%)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 800, 600); // Perlin pattern ctx.fillStyle = "rgba(0,0,0,0.1)"; for (let x = 0; x < 800; x += 40) { for (let y = 0; y < 600; y += 40) { const n = perlin.smooth(x, y) * 40 + time * 2; ctx.fillRect(x + n, y + n, 5, 5); } } } function drawUI() { ctx.fillStyle = "#fff"; ctx.font = "30px Arial"; ctx.fillText(score[0].toString(), 370, 50); ctx.fillText(score[1].toString(), 420, 50); // Slow motion indicator if (keys["Shift"]) { ctx.fillStyle = "rgba(255,0,0,0.3)"; ctx.fillRect(0, 0, 800, 600); } } function loop() { update(); drawBackground(); leftPaddle.draw(ctx); rightPaddle.draw(ctx); ball.draw(ctx); drawUI(); requestAnimationFrame(loop); } loop(); </script> </body> </html> ```