Skip to content

Instantly share code, notes, and snippets.

@ceving
Last active February 14, 2024 20:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ceving/ad5f3bcb7a63a8d4e14a30a28f249202 to your computer and use it in GitHub Desktop.
Save ceving/ad5f3bcb7a63a8d4e14a30a28f249202 to your computer and use it in GitHub Desktop.
Bouncing Ball with requestAnimationFrame in JavaScript
<!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="&#x23F5;">
<input type="button" id="halt" value="&#x23F8;">
<input type="button" id="step" value="&#x23ED;">
<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