├── 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 |
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 | }
--------------------------------------------------------------------------------