matrix-react-sdk
Version:
SDK for matrix.org using React
124 lines (107 loc) • 3.92 kB
text/typescript
/*
Copyright 2024 New Vector Ltd.
Copyright 2020-2023 The Matrix.org Foundation C.I.C.
Copyright 2021 Josias Allestad
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import ICanvasEffect from "../ICanvasEffect";
import { arrayFastClone } from "../../utils/arrays";
export type RainfallOptions = {
/**
* The maximum number of raindrops to render at a given time
*/
maxCount: number;
/**
* The speed of the raindrops
*/
speed: number;
};
type Raindrop = {
x: number;
y: number;
height: number;
width: number;
speed: number;
};
export const DefaultOptions: RainfallOptions = {
maxCount: 600,
speed: 12,
};
const KEY_FRAME_INTERVAL = 15;
export default class Rainfall implements ICanvasEffect {
private readonly options: RainfallOptions;
public constructor(options: { [key: string]: any }) {
this.options = { ...DefaultOptions, ...options };
}
private context: CanvasRenderingContext2D | null = null;
private particles: Array<Raindrop> = [];
private lastAnimationTime = 0;
public isRunning = false;
public start = async (canvas: HTMLCanvasElement, timeout = 3000): Promise<void> => {
if (!canvas) {
return;
}
this.context = canvas.getContext("2d");
this.particles = [];
const count = this.options.maxCount;
while (this.particles.length < count) {
this.particles.push(this.resetParticle({} as Raindrop, canvas.width, canvas.height));
}
this.isRunning = true;
requestAnimationFrame(this.renderLoop);
if (timeout) {
window.setTimeout(this.stop, timeout);
}
};
public stop = async (): Promise<void> => {
this.isRunning = false;
};
private resetParticle = (particle: Raindrop, width: number, height: number): Raindrop => {
particle.x = Math.random() * width;
particle.y = Math.random() * -height;
particle.width = Math.random() * 1.5;
particle.height = particle.width * 15 + 4;
particle.speed = (Math.random() * this.options.speed * 4) / 5 + this.options.speed;
return particle;
};
private renderLoop = (): void => {
if (!this.context || !this.context.canvas) {
return;
}
if (this.particles.length === 0) {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
} else {
const timeDelta = Date.now() - this.lastAnimationTime;
if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) {
// Clear the screen first
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
this.lastAnimationTime = Date.now();
this.animateAndRenderRaindrops();
}
requestAnimationFrame(this.renderLoop);
}
};
private animateAndRenderRaindrops = (): void => {
if (!this.context || !this.context.canvas) {
return;
}
const height = this.context.canvas.height;
for (const particle of arrayFastClone(this.particles)) {
particle.y += particle.speed;
this.context.save();
this.context.beginPath();
this.context.rect(particle.x, particle.y, particle.width, particle.height);
this.context.fillStyle = "#5dadec"; // light blue
this.context.fill();
this.context.closePath();
this.context.restore();
// Remove dead raindrops
const maxBounds = height * 2;
if (particle.y > height + maxBounds) {
const idx = this.particles.indexOf(particle);
this.particles.splice(idx, 1);
}
}
};
}