Skip to content

Commit

Permalink
Refactor ball physics into separate module. Add theme kinds and effects
Browse files Browse the repository at this point in the history
tonybaloney committed Jan 27, 2025
1 parent 9253fd4 commit f3df8bc
Showing 7 changed files with 576 additions and 236 deletions.
1 change: 1 addition & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ export const enum Theme {
forest = 'forest',
castle = 'castle',
beach = 'beach',
winter = 'winter',
}

export const enum ColorThemeKind {
1 change: 1 addition & 0 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -901,6 +901,7 @@ class PetWebviewContainer implements IPetPanel {
</head>
<body>
<canvas id="petCanvas"></canvas>
<canvas id="effectCanvas"></canvas>
<div id="petsContainer"></div>
<div id="foreground"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
185 changes: 185 additions & 0 deletions src/panel/ball.ts
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);
}
});
}
14 changes: 14 additions & 0 deletions src/panel/effects/effect.ts
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;
}
181 changes: 181 additions & 0 deletions src/panel/effects/snow.ts
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,

Check failure on line 85 in src/panel/effects/snow.ts

GitHub Actions / build (18.x)

'scale' is defined but never used
floor: number,

Check failure on line 86 in src/panel/effects/snow.ts

GitHub Actions / build (18.x)

'floor' is defined but never used
themeKind: ColorThemeKind,

Check failure on line 87 in src/panel/effects/snow.ts

GitHub Actions / build (18.x)

'themeKind' is defined but never used
): 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);
}
}
Loading

0 comments on commit f3df8bc

Please sign in to comment.