Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
class Ball { | ||
constructor({ x = 0, y = 0, velocity = 0, angle = 0, radius = 30 }) { | ||
this.radius = radius | ||
this.x = x | ||
this.y = y | ||
this.velocity = velocity | ||
this.angle = angle | ||
} | ||
|
||
update({ width, height }) { | ||
const dx = this.velocity * Math.cos(this.angle * (Math.PI / 180)) | ||
const dy = this.velocity * Math.sin(this.angle * (Math.PI / 180)) | ||
this.x += Math.round(dx) | ||
this.y += Math.round(dy) | ||
|
||
if (this.x < 0) { | ||
this.x = width - Math.abs(this.x) | ||
} | ||
if (this.x > width) { | ||
this.x = this.x - width | ||
} | ||
if (this.y < 0) { | ||
this.y = height - Math.abs(this.y) | ||
} | ||
if (this.y > height) { | ||
this.y = this.y - height | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* @param {{ctx: CanvasRenderingContext2D}} | ||
* @returns | ||
*/ | ||
render({ ctx, width, height }) { | ||
const { x, y } = this // snapshot | ||
ctx.beginPath() | ||
ctx.strokeStyle = 'white' | ||
ctx.arc(x, y, this.radius, 0, 2 * Math.PI) | ||
ctx.stroke() | ||
return () => { | ||
ctx.fillStyle = 'black' | ||
ctx.fillRect( | ||
x - this.radius - 1, | ||
y - this.radius - 1, | ||
this.radius * 2 + 2, | ||
this.radius * 2 + 2 | ||
) | ||
} | ||
} | ||
} | ||
|
||
export { Ball } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
export function detect(things) { | ||
const seen = new WeakSet() | ||
|
||
for (let i = 0; i < things.length; i += 1) { | ||
const asteroid = things[i] | ||
if (seen.has(asteroid)) continue | ||
|
||
const collision = things.find((collider) => { | ||
if (asteroid === collider) return false | ||
return collides(asteroid, collider) | ||
}) | ||
|
||
if (collision) { | ||
seen.add(collision) | ||
seen.add(asteroid) | ||
|
||
const [newAsteroid, newCollission] = getNewVectors(asteroid, collision) | ||
|
||
Object.assign(asteroid, newAsteroid) | ||
Object.assign(collision, newCollission) | ||
} | ||
} | ||
} | ||
|
||
function sigDig(x) { | ||
return parseFloat(x.toFixed(3)) | ||
} | ||
|
||
export function collides(o1, o2) { | ||
const x1 = Math.abs(o1.x - o2.x) | ||
const y1 = Math.abs(o1.y - o2.y) | ||
const d = (x1 ** 2 + y1 ** 2) ** 0.5 | ||
return d <= o1.radius + o2.radius | ||
} | ||
|
||
const hyp = (a, b) => (a ** 2 + b ** 2) ** 0.5 | ||
|
||
const degrad = (i) => i * (180 / Math.PI) | ||
|
||
export const angle = (x, y) => { | ||
if (x === 0 && y === 0) return 0 | ||
if (x === 0) return y > 0 ? 90 : 270 | ||
const offset = x < 0 ? 180 : y < 0 ? 360 : 0 | ||
return Math.round(degrad(Math.atan(y / x)) + offset) | ||
} | ||
|
||
// export const angle = (x, y) => | ||
// degrad(Math.atan2(y, x)) + (y < 0 ? 360 : 0) | ||
|
||
export function getNewVectors(o1, o2) { | ||
const m1 = o1.radius | ||
const m2 = o2.radius | ||
const vx1 = Math.round( | ||
o1.velocity * sigDig(Math.cos(o1.angle * (Math.PI / 180))) | ||
) | ||
const vy1 = Math.round( | ||
o1.velocity * sigDig(Math.sin(o1.angle * (Math.PI / 180))) | ||
) | ||
const vx2 = Math.round( | ||
o2.velocity * sigDig(Math.cos(o2.angle * (Math.PI / 180))) | ||
) | ||
const vy2 = Math.round( | ||
o2.velocity * sigDig(Math.sin(o2.angle * (Math.PI / 180))) | ||
) | ||
const M = m1 + m2 | ||
|
||
const dx1 = o1.x - o2.x | ||
const dy1 = o1.y - o2.y | ||
|
||
const dx2 = o2.x - o1.x | ||
const dy2 = o2.y - o1.y | ||
|
||
const calc = (v, m, dx, dy, dvx, dvy, o) => { | ||
const p1 = (2 * m) / M | ||
const p2 = (dvx * dx + dvy * dy) / (dx * dx + dy * dy) | ||
return Math.round(v - p1 * p2 * o) | ||
} | ||
|
||
const newX1 = calc(vx1, m2, dx1, dy1, vx1 - vx2, vy1 - vy2, dx1) | ||
|
||
const newY1 = calc(vy1, m2, dx1, dy1, vx1 - vx2, vy1 - vy2, dy1) | ||
|
||
const newX2 = calc(vx2, m1, dx2, dy2, vx2 - vx1, vy2 - vy1, dx2) | ||
|
||
const newY2 = calc(vy2, m1, dx2, dy2, vx2 - vx1, vy2 - vy1, dy2) | ||
|
||
return [ | ||
{ angle: angle(newX1, newY1), velocity: Math.round(hyp(newY1, newX1)) }, | ||
{ angle: angle(newX2, newY2), velocity: Math.round(hyp(newY2, newX2)) }, | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { detect } from './collision.mjs' | ||
|
||
let tickSpeed = null | ||
let tid = null | ||
let state = null | ||
let things | ||
let width | ||
let height | ||
|
||
export function start( | ||
{ width: initialWidth, height: initialHeight, things: initialThings }, | ||
queue = true, | ||
initialTickSpeed = 16 | ||
) { | ||
tickSpeed = initialTickSpeed | ||
things = initialThings | ||
width = initialWidth | ||
height = initialHeight | ||
if (queue) tid = setTimeout(update, tickSpeed) | ||
} | ||
|
||
export function resize() { | ||
width = document.body.clientWidth | ||
height = document.body.clientHeight | ||
} | ||
|
||
export function update() { | ||
if (tid !== null) tid = setTimeout(update, tickSpeed) | ||
things.forEach((thing) => thing.update({ state, width, height })) | ||
detect(things) | ||
} | ||
|
||
export function stop() { | ||
tickSpeed = null | ||
tid = null | ||
state = null | ||
clearTimeout(tid) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<title>Asteroids</title> | ||
<style> | ||
html, | ||
body, | ||
#game { | ||
padding: 0; | ||
margin: 0; | ||
position: absolute; | ||
width: 100%; | ||
height: 100%; | ||
} | ||
#menu { | ||
position: fixed; | ||
right: 0; | ||
top: 0; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div id="app"> | ||
<canvas id="game"></canvas> | ||
<div id="menu"> | ||
<button id="stop-button">Stop</button> | ||
<button id="start-button">Start</button> | ||
</div> | ||
</div> | ||
<script type="module" src="./index.mjs"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { | ||
start as startGame, | ||
stop as stopGame, | ||
resize as resizeGame, | ||
} from './game-loop.mjs' | ||
|
||
import { | ||
start as startRender, | ||
stop as stopRender, | ||
resize as resizeRender, | ||
} from './render-loop.mjs' | ||
|
||
import { Ball } from './ball.mjs' | ||
import { collides } from './collision.mjs' | ||
|
||
let startButton | ||
let stopButton | ||
let canvas | ||
let ctx | ||
let width | ||
let height | ||
|
||
const things = [] | ||
|
||
document.addEventListener('DOMContentLoaded', () => { | ||
canvas = document.getElementById('game') | ||
ctx = canvas.getContext('2d') | ||
width = canvas.width = document.body.clientWidth | ||
height = canvas.height = document.body.clientHeight | ||
|
||
stopButton = document.getElementById('stop-button') | ||
stopButton.addEventListener('click', handleStopButtonClick) | ||
|
||
startButton = document.getElementById('start-button') | ||
startButton.addEventListener('click', handleStartButtonClick) | ||
|
||
document.addEventListener('keydown', (e) => { | ||
if (e.key === 'Escape') handleStopButtonClick(e) | ||
}) | ||
|
||
handleStartButtonClick() | ||
}) | ||
|
||
document.addEventListener('resize', () => { | ||
resizeRender() | ||
resizeGame() | ||
}) | ||
|
||
function handleStartButtonClick(e) { | ||
e?.preventDefault() | ||
const initialState = { width, height, things } | ||
|
||
for (let i = 0; i < 30; i += 1) { | ||
const newThing = new Ball({ | ||
x: Math.floor(Math.random() * width), | ||
y: Math.floor(Math.random() * height), | ||
velocity: Math.floor(Math.random() * 5), | ||
angle: Math.floor(Math.random() * 360), | ||
radius: Math.floor(Math.random() * 5) + 10, | ||
}) | ||
|
||
if (newThing.velocity === 0 || things.some((x) => collides(newThing, x))) { | ||
i-- | ||
} else { | ||
things.push(newThing) | ||
} | ||
} | ||
|
||
ctx.fillStyle = 'black' | ||
ctx.fillRect(0, 0, width, height) | ||
startGame(initialState) | ||
startRender(canvas, initialState) | ||
} | ||
|
||
function handleStopButtonClick(e) { | ||
e.preventDefault() | ||
stopGame() | ||
stopRender() | ||
things.flush() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
let rfid = null | ||
let canvas = null | ||
let ctx | ||
let width | ||
let height | ||
let things | ||
|
||
export function start( | ||
canvas, | ||
{ width: initialWidth, height: initialHeight, things: initialThings } | ||
) { | ||
ctx = canvas.getContext('2d') | ||
width = initialWidth | ||
height = initialHeight | ||
things = initialThings | ||
rfid = requestAnimationFrame(render) | ||
} | ||
|
||
export function resize() { | ||
canvas.width = document.body.clientWidth | ||
canvas.height = document.body.clientHeight | ||
width = canvas.width | ||
height = canvas.height | ||
} | ||
|
||
const unrender = [] | ||
|
||
function render() { | ||
if (rfid !== null) rfid = requestAnimationFrame(render) | ||
if (unrender.length > 0) { | ||
unrender.forEach((unrenderer) => unrenderer()) | ||
unrender.splice(0, unrender.length) | ||
} | ||
things.forEach((thing) => { | ||
const unrenderer = thing.render({ ctx, width, height }) | ||
if (unrenderer) unrender.push(unrenderer) | ||
}) | ||
} | ||
|
||
export function stop() { | ||
cancelAnimationFrame(rfid) | ||
rfid = null | ||
canvas = null | ||
ctx = null | ||
width = null | ||
height = null | ||
} |