Last active
February 14, 2024 20:52
-
-
Save ceving/ad5f3bcb7a63a8d4e14a30a28f249202 to your computer and use it in GitHub Desktop.
Bouncing Ball with requestAnimationFrame in JavaScript
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Bounce</title> | |
<style> | |
body { background: silver; } | |
canvas { background: white; } | |
span.sign { display: inline-block; white-space: pre; width: 1ch; text-align: center; } | |
span.number { display: inline-block; white-space: pre; } | |
form { display: flex; align-items: center; margin-bottom: 1ex; } | |
label:not(:first-child) { margin-left: 2em; } | |
output { margin-left: 1ex; } | |
form > output { width: 3ch; } | |
input[type=button] { width: 2.5em; text-align: center; padding-bottom: 0.4ex; font-size: 100%; } | |
input[type=button] + input[type=button] { margin-left: 1ex; } | |
input[type=range] { margin: 0 0 0 1ex; } | |
input[type=checkbox] { margin: 0 0 0 1ex; } | |
</style> | |
</head> | |
<body> | |
<form> | |
<input type="button" id="play" value="⏵"> | |
<input type="button" id="halt" value="⏸"> | |
<input type="button" id="step" value="⏭"> | |
<label for="increment">Increment:</label> | |
<output for="increment"></output> | |
<input type="range" id="increment" min="1" max="100"> | |
<label for="direction">Direction:</label> | |
<output for="direction"></output> | |
<input type="range" id="direction" min="0" max="359"> | |
<label for="motion_jump">Jump</label> | |
<input type="radio" id="motion_jump" name="motion" value="jump" checked> | |
<label for="motion_slide">Slide</label> | |
<input type="radio" id="motion_slide" name="motion" value="slide"> | |
</form> | |
<canvas width="1200" height="900"></canvas> | |
<div> | |
<label>Frame Duration:</label><output id="frame_duration"></output> ms | |
<label>Increment X:</label><output id="increment_x"></output> | |
<label>Increment Y:</label><output id="increment_y"></output> | |
</div> | |
</body> | |
<script> | |
class DomValue { | |
#value; // the value | |
get value() { return this.#value; } | |
set value (value) { | |
this.#value = this.#normalization(value); | |
this.#setter(this.#formatter(this.#value)); | |
this.react(); } | |
#element; // the linked element | |
element(element) { // link the given element | |
if (element instanceof EventTarget) | |
this.#element = element; | |
else | |
throw new Error(`DomVal.element: Invalid argument: ${element}`); } | |
getbyid(id) { // link an element by in identifier | |
this.#element = document.getElementById(id); | |
if (this.#element) | |
return this; | |
else | |
throw new Error(`DomVal.byid: Element id does not exist: ${id}`); } | |
query(selector) { // link an element by a selector | |
this.#element = document.querySelector(selector); | |
if (this.#element) | |
return this; | |
else | |
throw new Error(`DomVal.query: Query returns nothing: ${query}`); } | |
#getter; // read the value from the linked element | |
getter(getter) { | |
this.#getter = getter; | |
return this; } | |
#setter; // write the value to the linked element | |
setter(setter) { | |
this.#setter = setter; | |
return this; } | |
#normalization; // normalization of the value | |
normalization(normalization) { | |
this.#normalization = normalization; | |
return this; } | |
#parser; // parser for the value of the linked element | |
parser(parser) { | |
this.#parser = parser; | |
return this; } | |
#formatter; // formater for the value for the linked element | |
formatter(formatter) { | |
this.#formatter = formatter; | |
return this; } | |
#reaction; // reaction to a change of the value | |
reaction(reaction) { | |
this.#reaction = reaction; | |
return this; } | |
constructor(value) { // create a new DomValue | |
this.#value = value; | |
this.#getter = ev => this.#element.value; | |
this.#setter = value => this.#element.value = value; | |
this.#parser = x => x; | |
this.#formatter = x => x; | |
this.#normalization = x => x; } | |
listener(type) { // listen to events of the linked element | |
this.#element.addEventListener(type, ev => { | |
this.#value = this.#parser(this.#getter(ev)); | |
this.react(); }); | |
return this; } | |
oninput() { // listen to input events of the linked element | |
return this.listener('input'); } | |
react() { // perform the reaction if a reaction has been defined | |
if (this.#reaction) | |
this.#reaction(this.#value); } | |
init() { // initialize element and reaction | |
this.#setter(this.#formatter(this.#value)); | |
this.react(); | |
return this; } | |
} | |
class Bounce { | |
constructor() { | |
this.frame_duration = new DomValue(0) | |
.getbyid('frame_duration') | |
.formatter(value => value.toFixed(2)); | |
this.view_canvas = document.querySelector("canvas"); | |
this.view_context = this.view_canvas.getContext("2d"); | |
this.draw_canvas = document.createElement('canvas'); | |
this.draw_canvas.width = this.view_canvas.width; | |
this.draw_canvas.height = this.view_canvas.height; | |
this.draw_context = this.draw_canvas.getContext("2d"); | |
this.items = []; | |
this.arid = null; | |
this.view_canvas.addEventListener('click', ev => { | |
const rect = this.view_canvas.getBoundingClientRect(); | |
const cx = (rect.right - rect.left) / 2; | |
const cy = (rect.bottom - rect.top) / 2; | |
const ex = ev.clientX - rect.left; | |
const ey = ev.clientY - rect.top; | |
const dx = ex - cx; | |
const dy = - (ey - cy); | |
const phi = Math.atan2(dy, dx); | |
const deg = 90 - phi / Math.PI * 180; | |
Ball.direction.value = deg; | |
}); | |
this.view_canvas.addEventListener('wheel', ev => { | |
ev.preventDefault(); | |
switch (true) { | |
case ev.deltaY < 0: | |
Ball.increment.value += 1; | |
break; | |
case ev.deltaY > 0: | |
Ball.increment.value -= 1; | |
break; | |
} | |
}); | |
document.getElementById('play').addEventListener('click', ev => { if (!this.arid) this.play(); }); | |
document.getElementById('halt').addEventListener('click', ev => { if (this.arid) this.halt(); }); | |
document.getElementById('step').addEventListener('click', ev => { this.step(); }); | |
} | |
step() { | |
const start_time = performance.now(); | |
this.draw_context.fillStyle = "rgb(255, 255, 255)"; | |
this.draw_context.fillRect( | |
0, 0, this.draw_canvas.width, this.draw_canvas.height); | |
for (const item of this.items) | |
item.render(this.draw_canvas, this.draw_context); | |
this.view_context.drawImage(this.draw_canvas, 0, 0); | |
this.frame_duration.value = performance.now() - start_time; | |
} | |
play() { | |
this.arid = requestAnimationFrame(() => { | |
this.play(); | |
this.step(); | |
}); | |
} | |
halt() { | |
cancelAnimationFrame(this.arid); | |
this.arid = null; | |
} | |
} | |
class Course { | |
#radiant = 0; | |
static D2R = Math.PI / 180; | |
static R2D = 180 / Math.PI; | |
static PI2 = 2 * Math.PI; | |
get degree () { return this.#radiant * Course.R2D; } | |
set degree (degree) { this.radiant = degree * Course.D2R; } | |
get radiant () { return this.#radiant; } | |
set radiant (radiant) { | |
radiant = Number(radiant); | |
switch (true) { | |
case radiant < 0: | |
this.#radiant = radiant + Course.PI2; | |
break; | |
case radiant >= Course.PI2: | |
this.#radiant = radiant - Course.PI2; | |
break; | |
default: | |
this.#radiant = radiant; | |
break; | |
} | |
} | |
increment(radiant) { this.radiant += radiant; } | |
decrement(radiant) { this.radiant -= radiant; } | |
right(degree) { this.increment(degree * Couse.D2R); } | |
left(degree) { this.decrement(degree * Couse.D2R); } | |
} | |
// Gamma corrected alpha value for γ=2.2 | |
function galpha(n) { return Math.pow(1/n, 1/2.2); } | |
class Ball { | |
static increment_x = new DomValue(0) | |
.getbyid('increment_x') | |
.formatter(value => value.toFixed(1)); | |
static increment_y = new DomValue(0) | |
.getbyid('increment_y') | |
.formatter(value => value.toFixed(1)); | |
static increment = new DomValue(20) | |
.getbyid('increment') | |
.oninput() | |
.parser(value => Number(value)) | |
.reaction((output => value => { | |
output.value = value; | |
Ball.increment_x.value = Math.cos((90 - Ball.direction.value) / 180 * Math.PI) * value; | |
Ball.increment_y.value = - Math.sin((90 - Ball.direction.value) / 180 * Math.PI) * value; | |
})(document.querySelector('output[for=increment]'))); | |
static direction = new DomValue(135) | |
.getbyid('direction') | |
.oninput() | |
.parser(value => Number(value)) | |
.normalization(value => { | |
value = Math.round(value); | |
switch(true) { | |
case value < 0: return value + 360; | |
case value >= 360: return value - 360; | |
default: return value; }}) | |
.reaction((output => value => { | |
output.value = value; | |
Ball.increment_x.value = Math.cos((90 - value) / 180 * Math.PI) * Ball.increment.value; | |
Ball.increment_y.value = - Math.sin((90 - value) / 180 * Math.PI) * Ball.increment.value; | |
})(document.querySelector('output[for=direction]'))); | |
static { | |
Ball.increment.init(); | |
Ball.direction.init(); | |
} | |
constructor(game) { | |
this.game = game; | |
this.x = 100; | |
this.y = 100; | |
this.r = 20; | |
this.twopi = 2 * Math.PI; | |
game.items.push(this); | |
} | |
render(canvas, context) { | |
this.move(canvas); | |
this.draw(context); | |
} | |
draw(context) { | |
context.beginPath(); | |
context.fillStyle = "rgb(255 0 0)"; | |
context.arc(this.x, this.y, this.r, 0, this.twopi); | |
context.fill(); | |
} | |
move(canvas) { | |
this.x += Ball.increment_x.value; | |
if (this.x > canvas.width) { | |
this.x = canvas.width - (this.x - canvas.width); | |
Ball.direction.value = 360 - Ball.direction.value; | |
} | |
if (this.x < 0) { | |
this.x = - this.x | |
Ball.direction.value = 360 - Ball.direction.value; | |
} | |
this.y += Ball.increment_y.value; | |
if (this.y > canvas.height) { | |
this.y = canvas.height - (this.y - canvas.height); | |
Ball.direction.value = 180 - Ball.direction.value; | |
} | |
if (this.y < 0) { | |
this.y = - this.y | |
Ball.direction.value = 180 - Ball.direction.value; | |
} | |
} | |
} | |
window.game = new Bounce; | |
new Ball(window.game); | |
window.game.step(); | |
</script> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment