Skip to content

Commit

Permalink
Release v1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
tswaters committed Aug 3, 2021
1 parent 616e176 commit df2fcdc
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 0 deletions.
53 changes: 53 additions & 0 deletions ball.mjs
@@ -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 }
91 changes: 91 additions & 0 deletions collision.mjs
@@ -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)) },
]
}
38 changes: 38 additions & 0 deletions game-loop.mjs
@@ -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)
}
34 changes: 34 additions & 0 deletions index.html
@@ -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>
80 changes: 80 additions & 0 deletions index.mjs
@@ -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()
}
47 changes: 47 additions & 0 deletions render-loop.mjs
@@ -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
}

0 comments on commit df2fcdc

Please sign in to comment.