- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 410
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor ball physics into separate module. Add theme kinds and effects
1 parent
9253fd4
commit f3df8bc
Showing
7 changed files
with
576 additions
and
236 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
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
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,185 @@ | ||
import { PetSize } from '../common/types'; | ||
import { PetElement } from './pets'; | ||
import { BallState } from './states'; | ||
|
||
/// Bouncing ball components, credit https://stackoverflow.com/a/29982343 | ||
const gravity: number = 0.6, | ||
damping: number = 0.9, | ||
traction: number = 0.8, | ||
interval: number = 1000 / 24; // msec for single frame | ||
let then: number = 0; // last draw | ||
var ballState: BallState; | ||
|
||
var canvas: HTMLCanvasElement | null; | ||
var ballRadius: number; | ||
var floor: number; | ||
|
||
function calculateBallRadius(size: PetSize): number { | ||
if (size === PetSize.nano) { | ||
return 2; | ||
} else if (size === PetSize.small) { | ||
return 3; | ||
} else if (size === PetSize.medium) { | ||
return 4; | ||
} else if (size === PetSize.large) { | ||
return 8; | ||
} else { | ||
return 1; // Shrug | ||
} | ||
} | ||
|
||
export function setupBallThrowing( | ||
canvasName: string, | ||
petSize: PetSize, | ||
floor_: number, | ||
): void { | ||
canvas = document.getElementById(canvasName) as HTMLCanvasElement; | ||
ballRadius = calculateBallRadius(petSize); | ||
floor = floor_; | ||
} | ||
|
||
function resetBall(): void { | ||
if (ballState) { | ||
ballState.paused = true; | ||
} | ||
if (canvas) { | ||
canvas.style.display = 'block'; | ||
} | ||
ballState = new BallState(100, 100, 4, 5); | ||
} | ||
|
||
export function dynamicThrowOn(pets: PetElement[]) { | ||
let startMouseX: number; | ||
let startMouseY: number; | ||
let endMouseX: number; | ||
let endMouseY: number; | ||
console.log('Enabling dynamic throw'); | ||
window.onmousedown = (e) => { | ||
if (ballState) { | ||
ballState.paused = true; | ||
} | ||
if (canvas) { | ||
canvas.style.display = 'block'; | ||
} | ||
endMouseX = e.clientX; | ||
endMouseY = e.clientY; | ||
startMouseX = e.clientX; | ||
startMouseY = e.clientY; | ||
ballState = new BallState(e.clientX, e.clientY, 0, 0); | ||
|
||
pets.forEach((petEl) => { | ||
if (petEl.pet.canChase && canvas) { | ||
petEl.pet.chase(ballState, canvas); | ||
} | ||
}); | ||
ballState.paused = true; | ||
|
||
drawBall(); | ||
|
||
window.onmousemove = (ev) => { | ||
ev.preventDefault(); | ||
if (ballState) { | ||
ballState.paused = true; | ||
} | ||
startMouseX = endMouseX; | ||
startMouseY = endMouseY; | ||
endMouseX = ev.clientX; | ||
endMouseY = ev.clientY; | ||
ballState = new BallState(ev.clientX, ev.clientY, 0, 0); | ||
drawBall(); | ||
}; | ||
window.onmouseup = (ev) => { | ||
ev.preventDefault(); | ||
window.onmouseup = null; | ||
window.onmousemove = null; | ||
|
||
ballState = new BallState( | ||
endMouseX, | ||
endMouseY, | ||
endMouseX - startMouseX, | ||
endMouseY - startMouseY, | ||
); | ||
pets.forEach((petEl) => { | ||
if (petEl.pet.canChase && canvas) { | ||
petEl.pet.chase(ballState, canvas); | ||
} | ||
}); | ||
throwBall(); | ||
}; | ||
}; | ||
} | ||
|
||
export function dynamicThrowOff() { | ||
console.log('Disabling dynamic throw'); | ||
window.onmousedown = null; | ||
if (ballState) { | ||
ballState.paused = true; | ||
} | ||
if (canvas) { | ||
canvas.style.display = 'none'; | ||
} | ||
} | ||
|
||
export function throwBall() { | ||
if (!canvas) { | ||
return; | ||
} | ||
|
||
if (!ballState.paused) { | ||
requestAnimationFrame(throwBall); | ||
} | ||
|
||
// throttling the frame rate | ||
const now = Date.now(); | ||
const elapsed = now - then; | ||
if (elapsed <= interval) { | ||
return; | ||
} | ||
then = now - (elapsed % interval); | ||
|
||
if (ballState.cx + ballRadius >= canvas.width) { | ||
ballState.vx = -ballState.vx * damping; | ||
ballState.cx = canvas.width - ballRadius; | ||
} else if (ballState.cx - ballRadius <= 0) { | ||
ballState.vx = -ballState.vx * damping; | ||
ballState.cx = ballRadius; | ||
} | ||
if (ballState.cy + ballRadius + floor >= canvas.height) { | ||
ballState.vy = -ballState.vy * damping; | ||
ballState.cy = canvas.height - ballRadius - floor; | ||
// traction here | ||
ballState.vx *= traction; | ||
} else if (ballState.cy - ballRadius <= 0) { | ||
ballState.vy = -ballState.vy * damping; | ||
ballState.cy = ballRadius; | ||
} | ||
|
||
ballState.vy += gravity; | ||
|
||
ballState.cx += ballState.vx; | ||
ballState.cy += ballState.vy; | ||
drawBall(); | ||
} | ||
|
||
function drawBall() { | ||
if (!canvas) { | ||
return; | ||
} | ||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; | ||
ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
|
||
ctx.beginPath(); | ||
ctx.arc(ballState.cx, ballState.cy, ballRadius, 0, 2 * Math.PI, false); | ||
ctx.fillStyle = '#2ed851'; | ||
ctx.fill(); | ||
} | ||
|
||
export function throwAndChase(pets: PetElement[]) { | ||
resetBall(); | ||
throwBall(); | ||
pets.forEach((petEl) => { | ||
if (petEl.pet.canChase && canvas) { | ||
petEl.pet.chase(ballState, canvas); | ||
} | ||
}); | ||
} |
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,14 @@ | ||
import { ColorThemeKind, PetSize } from '../../common/types'; | ||
|
||
export interface Effect { | ||
name: string; | ||
description: string; | ||
init( | ||
canvas: HTMLCanvasElement, | ||
scale: PetSize, | ||
floor: number, | ||
themeKind: ColorThemeKind, | ||
): void; | ||
enable(): void; | ||
disable(): void; | ||
} |
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,181 @@ | ||
/* | ||
* Falling snow effect | ||
* Based on https://codepen.io/carlosrodas/pen/LYzbaMm | ||
*/ | ||
import { ColorThemeKind, PetSize } from '../../common/types'; | ||
import { Effect } from './effect'; | ||
|
||
class Vector2 { | ||
x: number; | ||
y: number; | ||
|
||
constructor(x: number, y: number) { | ||
this.x = x; | ||
this.y = y; | ||
} | ||
} | ||
|
||
function floorRandom(min: number, max: number) { | ||
return Math.floor(Math.random() * (max - min + 1) + min); | ||
} | ||
|
||
class Particle { | ||
origin: Vector2; | ||
position: Vector2; | ||
velocity: Vector2; | ||
size: number; | ||
amplitude: number; | ||
dx: number; | ||
|
||
constructor( | ||
origin: Vector2, | ||
velocity: Vector2, | ||
size: number, | ||
amplitude: number, | ||
) { | ||
this.origin = origin; | ||
this.position = new Vector2(origin.x, origin.y); | ||
this.velocity = velocity || new Vector2(0, 0); | ||
this.size = size; | ||
this.amplitude = amplitude; | ||
|
||
// randomize start values a bit | ||
this.dx = Math.random() * 100; | ||
} | ||
|
||
update(timeDelta: number) { | ||
this.position.y += this.velocity.y * timeDelta; | ||
|
||
// oscillate the x value between -amplitude and +amplitude | ||
this.dx += this.velocity.x * timeDelta; | ||
this.position.x = this.origin.x + this.amplitude * Math.sin(this.dx); | ||
} | ||
} | ||
|
||
export class SnowEffect implements Effect { | ||
name: string = 'Snow'; | ||
description: string = 'Falling snow effect'; | ||
|
||
canvas?: HTMLCanvasElement; | ||
ctx?: CanvasRenderingContext2D; | ||
particles: Array<Particle> = []; | ||
running: boolean = false; | ||
|
||
startTime: number = 0; | ||
frameTime: number = 0; | ||
|
||
pAmount: number = 5000; // Snowiness | ||
pSize: number[] = [0.5, 1.5]; // min and max size | ||
pSwing: number[] = [0.1, 1]; // min and max oscilation speed for x movement | ||
pSpeed: number[] = [40, 100]; // min and max y speed | ||
pAmplitude: number[] = [25, 50]; // min and max distance for x movement | ||
|
||
enable(): void { | ||
this.running = true; | ||
this.startTime = this.frameTime = Date.now(); | ||
this.loop(); | ||
} | ||
|
||
disable(): void { | ||
this.running = false; | ||
} | ||
|
||
init( | ||
canvas: HTMLCanvasElement, | ||
scale: PetSize, | ||
floor: number, | ||
themeKind: ColorThemeKind, | ||
): void { | ||
// use the container width and height | ||
this.canvas = canvas; | ||
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D; | ||
this.initParticles(); | ||
} | ||
|
||
loop() { | ||
if (this.running) { | ||
this.clear(); | ||
this.update(); | ||
this.draw(); | ||
this.queue(); | ||
} | ||
} | ||
|
||
private initParticles() { | ||
if (!this.canvas) { | ||
return; | ||
} | ||
// clear the particles array | ||
this.particles.length = 0; | ||
|
||
for (var i = 0; i < this.pAmount; i++) { | ||
var origin = new Vector2( | ||
floorRandom(0, this.canvas.width), | ||
floorRandom(-this.canvas.height, 0), | ||
); | ||
var velocity = new Vector2( | ||
floorRandom(this.pSwing[0], this.pSwing[1]), | ||
floorRandom(this.pSpeed[0], this.pSpeed[1]), | ||
); | ||
var size = floorRandom(this.pSize[0], this.pSize[1]); | ||
var amplitude = floorRandom(this.pAmplitude[0], this.pAmplitude[1]); | ||
|
||
this.particles.push( | ||
new Particle(origin, velocity, size, amplitude), | ||
); | ||
} | ||
} | ||
|
||
private update() { | ||
if (!this.canvas) { | ||
return; | ||
} | ||
// calculate the time since the last frame | ||
var timeNow = Date.now(); | ||
var timeDelta = timeNow - this.frameTime; | ||
|
||
for (var i = 0; i < this.particles.length; i++) { | ||
var particle = this.particles[i]; | ||
particle.update(timeDelta); | ||
|
||
if (particle.position.y - particle.size > this.canvas.height) { | ||
// reset the particle to the top and a random x position | ||
particle.position.y = -particle.size; | ||
particle.position.x = particle.origin.x = | ||
Math.random() * this.canvas.width; | ||
particle.dx = Math.random() * 100; | ||
} | ||
} | ||
|
||
// save this time for the next frame | ||
this.frameTime = timeNow; | ||
} | ||
|
||
private draw() { | ||
if (!this.ctx) { | ||
return; | ||
} | ||
this.ctx.fillStyle = 'rgb(255,255,255)'; | ||
|
||
for (var i = 0; i < this.particles.length; i++) { | ||
var particle = this.particles[i]; | ||
this.ctx.fillRect( | ||
particle.position.x, | ||
particle.position.y, | ||
particle.size, | ||
particle.size, | ||
); | ||
} | ||
} | ||
|
||
private clear() { | ||
if (!this.ctx || !this.canvas) { | ||
return; | ||
} | ||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | ||
} | ||
|
||
private queue() { | ||
window.requestAnimationFrame(this.loop); | ||
} | ||
} |
Oops, something went wrong.