Merge pull request #5512 from matrix-org/travis/release/the/snow
[RELEASE] Add a snowfall chat effect (with /snowfall command)
This commit is contained in:
commit
f364c852fd
3 changed files with 177 additions and 1 deletions
|
@ -75,6 +75,20 @@ type FireworksOptions = {
|
||||||
*/
|
*/
|
||||||
gravity: number;
|
gravity: number;
|
||||||
}
|
}
|
||||||
|
type SnowfallOptions = {
|
||||||
|
/**
|
||||||
|
* The maximum number of snowflakes to render at a given time
|
||||||
|
*/
|
||||||
|
maxCount: number;
|
||||||
|
/**
|
||||||
|
* The amount of gravity to apply to the snowflakes
|
||||||
|
*/
|
||||||
|
gravity: number;
|
||||||
|
/**
|
||||||
|
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
|
||||||
|
*/
|
||||||
|
maxDrift: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This configuration defines room effects that can be triggered by custom message types and emojis
|
* This configuration defines room effects that can be triggered by custom message types and emojis
|
||||||
|
@ -99,12 +113,24 @@ export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
|
||||||
msgType: 'nic.custom.fireworks',
|
msgType: 'nic.custom.fireworks',
|
||||||
command: 'fireworks',
|
command: 'fireworks',
|
||||||
description: () => _td("Sends the given message with fireworks"),
|
description: () => _td("Sends the given message with fireworks"),
|
||||||
fallbackMessage: () => _t("sends fireworks") + "🎆",
|
fallbackMessage: () => _t("sends fireworks") + " 🎆",
|
||||||
options: {
|
options: {
|
||||||
maxCount: 500,
|
maxCount: 500,
|
||||||
gravity: 0.05,
|
gravity: 0.05,
|
||||||
},
|
},
|
||||||
} as Effect<FireworksOptions>,
|
} as Effect<FireworksOptions>,
|
||||||
|
{
|
||||||
|
emojis: ['❄', '🌨'],
|
||||||
|
msgType: 'io.element.effect.snowfall',
|
||||||
|
command: 'snowfall',
|
||||||
|
description: () => _td("Sends the given message with snowfall"),
|
||||||
|
fallbackMessage: () => _t("sends snowfall") + " ❄",
|
||||||
|
options: {
|
||||||
|
maxCount: 200,
|
||||||
|
gravity: 0.05,
|
||||||
|
maxDrift: 5,
|
||||||
|
},
|
||||||
|
} as Effect<SnowfallOptions>,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
148
src/effects/snowfall/index.ts
Normal file
148
src/effects/snowfall/index.ts
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import ICanvasEffect from '../ICanvasEffect';
|
||||||
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
|
|
||||||
|
export type SnowfallOptions = {
|
||||||
|
/**
|
||||||
|
* The maximum number of snowflakes to render at a given time
|
||||||
|
*/
|
||||||
|
maxCount: number;
|
||||||
|
/**
|
||||||
|
* The amount of gravity to apply to the snowflakes
|
||||||
|
*/
|
||||||
|
gravity: number;
|
||||||
|
/**
|
||||||
|
* The amount of drift (horizontal sway) to apply to the snowflakes. Each snowflake varies.
|
||||||
|
*/
|
||||||
|
maxDrift: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Snowflake = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
xCol: number;
|
||||||
|
diameter: number;
|
||||||
|
maximumDrift: number;
|
||||||
|
gravity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultOptions: SnowfallOptions = {
|
||||||
|
maxCount: 200,
|
||||||
|
gravity: 0.05,
|
||||||
|
maxDrift: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY_FRAME_INTERVAL = 15; // 15ms, roughly
|
||||||
|
|
||||||
|
export default class Snowfall implements ICanvasEffect {
|
||||||
|
private readonly options: SnowfallOptions;
|
||||||
|
|
||||||
|
constructor(options: { [key: string]: any }) {
|
||||||
|
this.options = {...DefaultOptions, ...options};
|
||||||
|
}
|
||||||
|
|
||||||
|
private context: CanvasRenderingContext2D | null = null;
|
||||||
|
private particles: Array<Snowflake> = [];
|
||||||
|
private lastAnimationTime: number;
|
||||||
|
|
||||||
|
public isRunning: boolean;
|
||||||
|
|
||||||
|
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
|
||||||
|
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 Snowflake, canvas.width, canvas.height));
|
||||||
|
}
|
||||||
|
this.isRunning = true;
|
||||||
|
requestAnimationFrame(this.renderLoop);
|
||||||
|
if (timeout) {
|
||||||
|
window.setTimeout(this.stop, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop = async () => {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetParticle = (particle: Snowflake, width: number, height: number): Snowflake => {
|
||||||
|
particle.x = Math.random() * width;
|
||||||
|
particle.y = Math.random() * -height;
|
||||||
|
particle.xCol = particle.x;
|
||||||
|
particle.diameter = (Math.random() * 7) + 4;
|
||||||
|
particle.maximumDrift = (Math.random() * this.options.maxDrift) + 3.5;
|
||||||
|
particle.gravity = this.options.gravity + (Math.random() * 6) + 4;
|
||||||
|
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.animateAndRenderSnowflakes();
|
||||||
|
}
|
||||||
|
requestAnimationFrame(this.renderLoop);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private animateAndRenderSnowflakes() {
|
||||||
|
if (!this.context || !this.context.canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const height = this.context.canvas.height;
|
||||||
|
for (const particle of arrayFastClone(this.particles)) {
|
||||||
|
particle.y += particle.gravity;
|
||||||
|
|
||||||
|
// We treat the drift as a sine function to have a more fluid-like movement instead
|
||||||
|
// of a pong-like movement off walls of the X column. This means that for
|
||||||
|
// $x=A\sin(\frac{2\pi}{P}y)$ we use the `maximumDrift` as the amplitude (A) and a
|
||||||
|
// large multiplier to create a very long waveform through P.
|
||||||
|
const peakDistance = 75 * particle.maximumDrift;
|
||||||
|
const PI2 = Math.PI * 2;
|
||||||
|
particle.x = particle.maximumDrift * Math.sin((PI2 / peakDistance) * particle.y);
|
||||||
|
particle.x += particle.xCol; // bring the particle to the right place
|
||||||
|
|
||||||
|
const radius = particle.diameter / 2;
|
||||||
|
this.context.save();
|
||||||
|
this.context.beginPath();
|
||||||
|
this.context.ellipse(particle.x, particle.y, radius, radius, 0, 0, 360);
|
||||||
|
this.context.fillStyle = '#eaeaea'; // grey so it shows up on the light theme
|
||||||
|
this.context.fill();
|
||||||
|
this.context.closePath();
|
||||||
|
this.context.restore();
|
||||||
|
|
||||||
|
// Remove any dead snowflakes
|
||||||
|
const maxBounds = radius * 4; // make sure it's *really* off screen
|
||||||
|
if (particle.y > (height + maxBounds)) {
|
||||||
|
const idx = this.particles.indexOf(particle);
|
||||||
|
this.particles.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -849,6 +849,8 @@
|
||||||
"sends confetti": "sends confetti",
|
"sends confetti": "sends confetti",
|
||||||
"Sends the given message with fireworks": "Sends the given message with fireworks",
|
"Sends the given message with fireworks": "Sends the given message with fireworks",
|
||||||
"sends fireworks": "sends fireworks",
|
"sends fireworks": "sends fireworks",
|
||||||
|
"Sends the given message with snowfall": "Sends the given message with snowfall",
|
||||||
|
"sends snowfall": "sends snowfall",
|
||||||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||||
"Video Call": "Video Call",
|
"Video Call": "Video Call",
|
||||||
|
|
Loading…
Reference in a new issue