├── Ball.js ├── Paddle.js ├── index.html ├── script.js └── styles.css /Ball.js: -------------------------------------------------------------------------------- 1 | const INITIAL_VELOCITY = 0.025 2 | const VELOCITY_INCREASE = 0.00001 3 | 4 | export default class Ball { 5 | constructor(ballElem) { 6 | this.ballElem = ballElem 7 | this.reset() 8 | } 9 | 10 | get x() { 11 | return parseFloat(getComputedStyle(this.ballElem).getPropertyValue("--x")) 12 | } 13 | 14 | set x(value) { 15 | this.ballElem.style.setProperty("--x", value) 16 | } 17 | 18 | get y() { 19 | return parseFloat(getComputedStyle(this.ballElem).getPropertyValue("--y")) 20 | } 21 | 22 | set y(value) { 23 | this.ballElem.style.setProperty("--y", value) 24 | } 25 | 26 | rect() { 27 | return this.ballElem.getBoundingClientRect() 28 | } 29 | 30 | reset() { 31 | this.x = 50 32 | this.y = 50 33 | this.direction = { x: 0 } 34 | while ( 35 | Math.abs(this.direction.x) <= 0.2 || 36 | Math.abs(this.direction.x) >= 0.9 37 | ) { 38 | const heading = randomNumberBetween(0, 2 * Math.PI) 39 | this.direction = { x: Math.cos(heading), y: Math.sin(heading) } 40 | } 41 | this.velocity = INITIAL_VELOCITY 42 | } 43 | 44 | update(delta, paddleRects) { 45 | this.x += this.direction.x * this.velocity * delta 46 | this.y += this.direction.y * this.velocity * delta 47 | this.velocity += VELOCITY_INCREASE * delta 48 | const rect = this.rect() 49 | 50 | if (rect.bottom >= window.innerHeight || rect.top <= 0) { 51 | this.direction.y *= -1 52 | } 53 | 54 | if (paddleRects.some(r => isCollision(r, rect))) { 55 | this.direction.x *= -1 56 | } 57 | } 58 | } 59 | 60 | function randomNumberBetween(min, max) { 61 | return Math.random() * (max - min) + min 62 | } 63 | 64 | function isCollision(rect1, rect2) { 65 | return ( 66 | rect1.left <= rect2.right && 67 | rect1.right >= rect2.left && 68 | rect1.top <= rect2.bottom && 69 | rect1.bottom >= rect2.top 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /Paddle.js: -------------------------------------------------------------------------------- 1 | const SPEED = 0.02 2 | 3 | export default class Paddle { 4 | constructor(paddleElem) { 5 | this.paddleElem = paddleElem 6 | this.reset() 7 | } 8 | 9 | get position() { 10 | return parseFloat( 11 | getComputedStyle(this.paddleElem).getPropertyValue("--position") 12 | ) 13 | } 14 | 15 | set position(value) { 16 | this.paddleElem.style.setProperty("--position", value) 17 | } 18 | 19 | rect() { 20 | return this.paddleElem.getBoundingClientRect() 21 | } 22 | 23 | reset() { 24 | this.position = 50 25 | } 26 | 27 | update(delta, ballHeight) { 28 | this.position += SPEED * delta * (ballHeight - this.position) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Document 10 | 11 | 12 |
13 |
0
14 |
0
15 |
16 |
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | import Ball from "./Ball.js" 2 | import Paddle from "./Paddle.js" 3 | 4 | const ball = new Ball(document.getElementById("ball")) 5 | const playerPaddle = new Paddle(document.getElementById("player-paddle")) 6 | const computerPaddle = new Paddle(document.getElementById("computer-paddle")) 7 | const playerScoreElem = document.getElementById("player-score") 8 | const computerScoreElem = document.getElementById("computer-score") 9 | 10 | let lastTime 11 | function update(time) { 12 | if (lastTime != null) { 13 | const delta = time - lastTime 14 | ball.update(delta, [playerPaddle.rect(), computerPaddle.rect()]) 15 | computerPaddle.update(delta, ball.y) 16 | const hue = parseFloat( 17 | getComputedStyle(document.documentElement).getPropertyValue("--hue") 18 | ) 19 | 20 | document.documentElement.style.setProperty("--hue", hue + delta * 0.01) 21 | 22 | if (isLose()) handleLose() 23 | } 24 | 25 | lastTime = time 26 | window.requestAnimationFrame(update) 27 | } 28 | 29 | function isLose() { 30 | const rect = ball.rect() 31 | return rect.right >= window.innerWidth || rect.left <= 0 32 | } 33 | 34 | function handleLose() { 35 | const rect = ball.rect() 36 | if (rect.right >= window.innerWidth) { 37 | playerScoreElem.textContent = parseInt(playerScoreElem.textContent) + 1 38 | } else { 39 | computerScoreElem.textContent = parseInt(computerScoreElem.textContent) + 1 40 | } 41 | ball.reset() 42 | computerPaddle.reset() 43 | } 44 | 45 | document.addEventListener("mousemove", e => { 46 | playerPaddle.position = (e.y / window.innerHeight) * 100 47 | }) 48 | 49 | window.requestAnimationFrame(update) 50 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | *, *::after, *::before { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --hue: 200; 7 | --saturation: 50%; 8 | --foreground-color: hsl(var(--hue), var(--saturation), 75%); 9 | --background-color: hsl(var(--hue), var(--saturation), 20%); 10 | } 11 | 12 | body { 13 | margin: 0; 14 | background-color: var(--background-color); 15 | overflow: hidden; 16 | } 17 | 18 | .paddle { 19 | --position: 50; 20 | 21 | position: absolute; 22 | background-color: var(--foreground-color); 23 | top: calc(var(--position) * 1vh); 24 | transform: translateY(-50%); 25 | width: 1vh; 26 | height: 10vh; 27 | } 28 | 29 | .paddle.left { 30 | left: 1vw; 31 | } 32 | 33 | .paddle.right { 34 | right: 1vw; 35 | } 36 | 37 | .ball { 38 | --x: 50; 39 | --y: 50; 40 | 41 | position: absolute; 42 | background-color: var(--foreground-color); 43 | left: calc(var(--x) * 1vw); 44 | top: calc(var(--y) * 1vh); 45 | border-radius: 50%; 46 | transform: translate(-50%, -50%); 47 | width: 2.5vh; 48 | height: 2.5vh; 49 | } 50 | 51 | .score { 52 | display: flex; 53 | justify-content: center; 54 | font-weight: bold; 55 | font-size: 7vh; 56 | color: var(--foreground-color); 57 | } 58 | 59 | .score > * { 60 | flex-grow: 1; 61 | flex-basis: 0; 62 | padding: 0 2vh; 63 | margin: 1vh 0; 64 | opacity: .5; 65 | } 66 | 67 | .score > :first-child { 68 | text-align: right; 69 | border-right: .5vh solid var(--foreground-color); 70 | } --------------------------------------------------------------------------------