From 380b7296922d83e168703372b88fb1339965384e Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Thu, 17 Dec 2020 16:57:59 +0100 Subject: [PATCH 01/14] fireworks effect added --- src/effects/fireworks/index.ts | 167 +++++++++++++++++++++++++++++++++ src/effects/index.ts | 17 ++++ src/i18n/strings/en_EN.json | 2 + 3 files changed, 186 insertions(+) create mode 100644 src/effects/fireworks/index.ts diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts new file mode 100644 index 0000000000..7c84e31f1d --- /dev/null +++ b/src/effects/fireworks/index.ts @@ -0,0 +1,167 @@ +/* + Copyright 2020 Nurjin Jafar + Copyright 2020 Nordeck IT + Consulting GmbH. + + 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'; + +export type FireworksOptions = { + /** + * the maximum number of the fireworks + */ + maxCount: number, + /** + * the alpha opacity of the fireworks (between 0 and 1, where 1 is opaque and 0 is invisible) + */ + gravity: number, + probability: number, +} + +type FireworksParticle = { + color: string, + x: number, + y: number, + vx: number, + vy: number, + alpha: number, + w: number, + h: number +} + +export const DefaultOptions: FireworksOptions = { + maxCount: 500, + gravity: 0.05, + probability: 0.04, +}; + +export default class Fireworks implements ICanvasEffect { + private readonly options: FireworksOptions; + + constructor(options: { [key: string]: any }) { + this.options = {...DefaultOptions, ...options}; + } + + private context: CanvasRenderingContext2D | null = null; + private supportsAnimationFrame = window.requestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000/60); + }; + private particles: Array = []; + public isRunning: boolean; + + public start = async (canvas: HTMLCanvasElement, timeout = 4000) => { + if (!canvas) { + return; + } + this.isRunning = true; + this.context = canvas.getContext('2d'); + this.supportsAnimationFrame.call(window, this.updateWorld); + if (timeout) { + window.setTimeout(this.stop, timeout); + } + } + + private updateWorld = () => { + if (!this.isRunning) return; + this.update(); + this.paint(); + this.supportsAnimationFrame.call(window, this.updateWorld); + } + private update = () => { + if (this.particles.length < this.options.maxCount && Math.random() < this.options.probability) { + this.createFirework(); + } + const alive = []; + for (let i=0; i { + if (!this.context || !this.context.canvas) return; + this.context.globalCompositeOperation = 'source-over'; + this.context.fillStyle = "rgba(0,0,0,0.2)"; + this.context.canvas.style.opacity = "0.8"; + this.context.fillRect(0, 0, this.context.canvas.width, this.context.canvas.height); + this.context.globalCompositeOperation = 'lighter'; + for (let i=0; i { + if (!this.context || !this.context.canvas) return; + const width = this.context.canvas.width; + const height = this.context.canvas.height; + const xPoint = Math.random() * (width - 200) + 100; + const yPoint = Math.random() * (height - 200) + 100; + const nFire = Math.random() * 50 + 100; + const color = "rgb("+(~~(Math.random()*200+55))+"," + +(~~(Math.random()*200+55))+","+(~~(Math.random()*200+55))+")"; + for (let i=0; i{}; + particle.color = color; + particle.w = particle.h = Math.random() * 4 + 1; + particle.x = xPoint - particle.w / 2; + particle.y = yPoint - particle.h / 2; + particle.vx = (Math.random()-0.5)*10; + particle.vy = (Math.random()-0.5)*10; + particle.alpha = Math.random()*.5+.5; + const vy = Math.sqrt(25 - particle.vx * particle.vx); + if (Math.abs(particle.vy) > vy) { + particle.vy = particle.vy > 0 ? vy: -vy; + } + this.particles.push(particle); + } + } + + public stop = async () => { + this.isRunning = false; + this.particles = []; + this.context.canvas.style.opacity = "1"; + this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); + } + + private drawParticle = (particle: FireworksParticle): void => { + if (!this.context || !this.context.canvas) { + return; + } + this.context.save(); + this.context.beginPath(); + + this.context.translate(particle.x + particle.w / 2, particle.y + particle.h / 2); + this.context.arc(0, 0, particle.w, 0, Math.PI * 2); + this.context.fillStyle = particle.color; + this.context.globalAlpha = particle.alpha; + + this.context.closePath(); + this.context.fill(); + this.context.restore(); + } + + + private move = (particle: FireworksParticle) => { + particle.x += particle.vx; + particle.vy += this.options.gravity; + particle.y += particle.vy; + particle.alpha -= 0.01; + return !(particle.x <= -particle.w || particle.x >= screen.width || + particle.y >= screen.height || + particle.alpha <= 0); + } +} diff --git a/src/effects/index.ts b/src/effects/index.ts index 16a0851070..27ed73622f 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -64,6 +64,11 @@ type ConfettiOptions = { * use gradient instead of solid particle color */ gradient: boolean, +}; +type FireworksOptions = { + maxCount: number, + gravity: number, + probability: number, } /** @@ -84,6 +89,18 @@ export const CHAT_EFFECTS: Array> = [ gradient: false, }, } as Effect, + { + emojis: ['🎆'], + msgType: 'nic.custom.fireworks', + command: 'fireworks', + description: () => _td("Sends the given message with fireworks"), + fallbackMessage: () => _t("sends fireworks") + "🎆", + options: { + maxCount: 500, + gravity: 0.05, + probability: 0.04, + }, + } as Effect, ]; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1b5d4b6ec4..6d4c0dad4d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -848,6 +848,8 @@ "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", "Sends the given message with confetti": "Sends the given message with confetti", "sends confetti": "sends confetti", + "Sends the given message with fireworks": "Sends the given message with fireworks", + "sends fireworks": "sends fireworks", "Video Call": "Video Call", "Voice Call": "Voice Call", "Fill Screen": "Fill Screen", From 98a1b7a2d8bc71bbb274b66e20f18fe6aaefc3f9 Mon Sep 17 00:00:00 2001 From: Emmanouil Kampitakis Date: Thu, 10 Dec 2020 12:19:13 +0100 Subject: [PATCH 02/14] Add the tableflip slashcommand Signed-off-by: Emmanouil Kampitakis --- src/SlashCommands.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index e2ae875ac3..8c86b8bcb6 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -166,6 +166,19 @@ export const Commands = [ }, category: CommandCategories.messages, }), + new Command({ + command: 'tableflip', + args: '', + description: _td('Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message'), + runFn: function(roomId, args) { + let message = '(╯°□°)╯︵ ┻━┻'; + if (args) { + message = message + ' ' + args; + } + return success(MatrixClientPeg.get().sendTextMessage(roomId, message)); + }, + category: CommandCategories.messages, + }), new Command({ command: 'lenny', args: '', From f4d366821404a8cd535cdf47cf6e421d9d25276e Mon Sep 17 00:00:00 2001 From: Emmanouil Kampitakis Date: Thu, 10 Dec 2020 12:19:30 +0100 Subject: [PATCH 03/14] Add the unflip slashcommand Signed-off-by: Emmanouil Kampitakis --- src/SlashCommands.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 8c86b8bcb6..79c21c4af5 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -179,6 +179,19 @@ export const Commands = [ }, category: CommandCategories.messages, }), + new Command({ + command: 'unflip', + args: '', + description: _td('Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message'), + runFn: function(roomId, args) { + let message = '┬──┬ ノ( ゜-゜ノ)'; + if (args) { + message = message + ' ' + args; + } + return success(MatrixClientPeg.get().sendTextMessage(roomId, message)); + }, + category: CommandCategories.messages, + }), new Command({ command: 'lenny', args: '', From 2488a8f1eecda73e61b2aed42fe102c10900df7c Mon Sep 17 00:00:00 2001 From: Emmanouil Kampitakis Date: Wed, 16 Dec 2020 11:48:00 +0100 Subject: [PATCH 04/14] Add string-translation for english Signed-off-by: Emmanouil Kampitakis --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eb1d0a632e..f8ef44763d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -411,6 +411,8 @@ "Command error": "Command error", "Usage": "Usage", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", + "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message", + "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message", "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message", "Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown", "Sends a message as html, without interpreting it as markdown": "Sends a message as html, without interpreting it as markdown", From 453575d70acc51b16cb4374b9b9e6fd93a4978af Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Dec 2020 13:46:58 +0000 Subject: [PATCH 05/14] Don't play call end sound for calls that never started --- src/CallHandler.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index fac4d6fc4e..7160657c07 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -341,7 +341,8 @@ export default class CallHandler { title: _t("Answered Elsewhere"), description: _t("The call was answered on another device."), }); - } else { + } else if (oldState !== CallState.Fledgling) { + // don't play the end-call sound for calls that never got off the ground this.play(AudioID.CallEnd); } } From 88d3de710a96d280bd84cd54aa1f6abe3dbd1d77 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Fri, 18 Dec 2020 14:51:58 +0100 Subject: [PATCH 06/14] comments added --- src/effects/fireworks/index.ts | 19 +++++++++++++++++-- src/effects/index.ts | 6 ++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts index 7c84e31f1d..3cc209eda3 100644 --- a/src/effects/fireworks/index.ts +++ b/src/effects/fireworks/index.ts @@ -19,23 +19,38 @@ import ICanvasEffect from '../ICanvasEffect'; export type FireworksOptions = { /** - * the maximum number of the fireworks + * max fireworks count */ maxCount: number, /** - * the alpha opacity of the fireworks (between 0 and 1, where 1 is opaque and 0 is invisible) + * gravity value that firework adds to shift from it's start position */ gravity: number, probability: number, } type FireworksParticle = { + /** + * color + */ color: string, + /** + * x,y are the point where the particle starts to position on canvas + */ x: number, y: number, + /** + * vx,vy shift values from x and y + */ vx: number, vy: number, + /** + * the alpha opacity of the firework particle (between 0 and 1, where 1 is opaque and 0 is invisible) + */ alpha: number, + /** + * w,h width and height + */ w: number, h: number } diff --git a/src/effects/index.ts b/src/effects/index.ts index 27ed73622f..07aaa5e736 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -66,7 +66,13 @@ type ConfettiOptions = { gradient: boolean, }; type FireworksOptions = { + /** + * max fireworks count + */ maxCount: number, + /** + * gravity value that firework adds to shift from it's start position + */ gravity: number, probability: number, } From 23058112713eb06a459cfd727b96d5becee55d07 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Fri, 18 Dec 2020 15:30:50 +0100 Subject: [PATCH 07/14] changed canvas globalCompositeOperation value and removed probability --- src/effects/fireworks/index.ts | 9 +++------ src/effects/index.ts | 2 -- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts index 3cc209eda3..b9898b8b22 100644 --- a/src/effects/fireworks/index.ts +++ b/src/effects/fireworks/index.ts @@ -26,7 +26,6 @@ export type FireworksOptions = { * gravity value that firework adds to shift from it's start position */ gravity: number, - probability: number, } type FireworksParticle = { @@ -58,7 +57,6 @@ type FireworksParticle = { export const DefaultOptions: FireworksOptions = { maxCount: 500, gravity: 0.05, - probability: 0.04, }; export default class Fireworks implements ICanvasEffect { @@ -95,7 +93,7 @@ export default class Fireworks implements ICanvasEffect { this.supportsAnimationFrame.call(window, this.updateWorld); } private update = () => { - if (this.particles.length < this.options.maxCount && Math.random() < this.options.probability) { + if (this.particles.length < this.options.maxCount) { this.createFirework(); } const alive = []; @@ -109,9 +107,8 @@ export default class Fireworks implements ICanvasEffect { private paint = () => { if (!this.context || !this.context.canvas) return; - this.context.globalCompositeOperation = 'source-over'; - this.context.fillStyle = "rgba(0,0,0,0.2)"; - this.context.canvas.style.opacity = "0.8"; + this.context.globalCompositeOperation = 'destination-out'; + this.context.fillStyle = "rgba(0,0,0,0.5)"; this.context.fillRect(0, 0, this.context.canvas.width, this.context.canvas.height); this.context.globalCompositeOperation = 'lighter'; for (let i=0; i> = [ options: { maxCount: 500, gravity: 0.05, - probability: 0.04, }, } as Effect, ]; From 3d1327ecec3d601480aac74e03afbc27facc5f09 Mon Sep 17 00:00:00 2001 From: nurjinn jafar Date: Fri, 18 Dec 2020 15:32:44 +0100 Subject: [PATCH 08/14] removed unnecessary opacity --- src/effects/fireworks/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts index b9898b8b22..ee32c9ac9c 100644 --- a/src/effects/fireworks/index.ts +++ b/src/effects/fireworks/index.ts @@ -145,7 +145,6 @@ export default class Fireworks implements ICanvasEffect { public stop = async () => { this.isRunning = false; this.particles = []; - this.context.canvas.style.opacity = "1"; this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); } From 77ce8a9e39b0ba5384e79b680efc04d0794872ce Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 18 Dec 2020 18:39:16 -0700 Subject: [PATCH 09/14] Fix minor lint problems --- src/effects/fireworks/index.ts | 21 +++++++++++---------- src/effects/index.ts | 14 +++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts index ee32c9ac9c..8f6cc5d82f 100644 --- a/src/effects/fireworks/index.ts +++ b/src/effects/fireworks/index.ts @@ -21,37 +21,37 @@ export type FireworksOptions = { /** * max fireworks count */ - maxCount: number, + maxCount: number; /** * gravity value that firework adds to shift from it's start position */ - gravity: number, + gravity: number; } type FireworksParticle = { /** * color */ - color: string, + color: string; /** * x,y are the point where the particle starts to position on canvas */ - x: number, - y: number, + x: number; + y: number; /** * vx,vy shift values from x and y */ - vx: number, - vy: number, + vx: number; + vy: number; /** * the alpha opacity of the firework particle (between 0 and 1, where 1 is opaque and 0 is invisible) */ - alpha: number, + alpha: number; /** * w,h width and height */ - w: number, - h: number + w: number; + h: number; } export const DefaultOptions: FireworksOptions = { @@ -92,6 +92,7 @@ export default class Fireworks implements ICanvasEffect { this.paint(); this.supportsAnimationFrame.call(window, this.updateWorld); } + private update = () => { if (this.particles.length < this.options.maxCount) { this.createFirework(); diff --git a/src/effects/index.ts b/src/effects/index.ts index 990d1e68ef..27f1902d7c 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -47,33 +47,33 @@ type ConfettiOptions = { /** * max confetti count */ - maxCount: number, + maxCount: number; /** * particle animation speed */ - speed: number, + speed: number; /** * the confetti animation frame interval in milliseconds */ - frameInterval: number, + frameInterval: number; /** * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible) */ - alpha: number, + alpha: number; /** * use gradient instead of solid particle color */ - gradient: boolean, + gradient: boolean; }; type FireworksOptions = { /** * max fireworks count */ - maxCount: number, + maxCount: number; /** * gravity value that firework adds to shift from it's start position */ - gravity: number, + gravity: number; } /** From 4ba89cc437e821016e5bb4eb7e08bb1eb8d84d09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 18 Dec 2020 18:40:11 -0700 Subject: [PATCH 10/14] Lower effect time by 1 second It feels like it runs a bit too long --- src/effects/fireworks/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts index 8f6cc5d82f..15ba286503 100644 --- a/src/effects/fireworks/index.ts +++ b/src/effects/fireworks/index.ts @@ -74,7 +74,7 @@ export default class Fireworks implements ICanvasEffect { private particles: Array = []; public isRunning: boolean; - public start = async (canvas: HTMLCanvasElement, timeout = 4000) => { + public start = async (canvas: HTMLCanvasElement, timeout = 3000) => { if (!canvas) { return; } From b2825e8718e35e2dadb3a2c0a6b41eef558ccec4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 18 Dec 2020 18:40:30 -0700 Subject: [PATCH 11/14] Use the same requestAnimationFrame as confetti --- src/effects/fireworks/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts index 15ba286503..4ca5c59fa7 100644 --- a/src/effects/fireworks/index.ts +++ b/src/effects/fireworks/index.ts @@ -67,10 +67,7 @@ export default class Fireworks implements ICanvasEffect { } private context: CanvasRenderingContext2D | null = null; - private supportsAnimationFrame = window.requestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000/60); - }; + private supportsAnimationFrame = window.requestAnimationFrame; private particles: Array = []; public isRunning: boolean; From 1f95acc73919325aa3571c87c1f0f19cd46d5c71 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 18 Dec 2020 18:41:01 -0700 Subject: [PATCH 12/14] Run the effect to completion rather than chopping it off --- src/effects/fireworks/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/effects/fireworks/index.ts b/src/effects/fireworks/index.ts index 4ca5c59fa7..96a27531af 100644 --- a/src/effects/fireworks/index.ts +++ b/src/effects/fireworks/index.ts @@ -84,14 +84,14 @@ export default class Fireworks implements ICanvasEffect { } private updateWorld = () => { - if (!this.isRunning) return; + if (!this.isRunning && this.particles.length === 0) return; this.update(); this.paint(); this.supportsAnimationFrame.call(window, this.updateWorld); } private update = () => { - if (this.particles.length < this.options.maxCount) { + if (this.particles.length < this.options.maxCount && this.isRunning) { this.createFirework(); } const alive = []; @@ -142,8 +142,6 @@ export default class Fireworks implements ICanvasEffect { public stop = async () => { this.isRunning = false; - this.particles = []; - this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); } private drawParticle = (particle: FireworksParticle): void => { From 9263bf7cdaa1d4b90ef2c753b05e77f3495e5ab3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 20 Dec 2020 23:29:03 -0700 Subject: [PATCH 13/14] Add a snowfall chat effect (with /snowfall command) --- src/effects/index.ts | 28 ++++++- src/effects/snowfall/index.ts | 151 ++++++++++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 2 + 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/effects/snowfall/index.ts diff --git a/src/effects/index.ts b/src/effects/index.ts index 27f1902d7c..a22948ebcf 100644 --- a/src/effects/index.ts +++ b/src/effects/index.ts @@ -75,6 +75,20 @@ type FireworksOptions = { */ 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 @@ -99,12 +113,24 @@ export const CHAT_EFFECTS: Array> = [ msgType: 'nic.custom.fireworks', command: 'fireworks', description: () => _td("Sends the given message with fireworks"), - fallbackMessage: () => _t("sends fireworks") + "🎆", + fallbackMessage: () => _t("sends fireworks") + " 🎆", options: { maxCount: 500, gravity: 0.05, }, } as Effect, + { + 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, ]; diff --git a/src/effects/snowfall/index.ts b/src/effects/snowfall/index.ts new file mode 100644 index 0000000000..a91d17379b --- /dev/null +++ b/src/effects/snowfall/index.ts @@ -0,0 +1,151 @@ +/* + 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 = []; + 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)) { + const leftMax = particle.xCol - particle.maximumDrift; + const rightMax = particle.xCol + particle.maximumDrift; + + 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); + } + } + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2fb70ecdb1..12d8171439 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -851,6 +851,8 @@ "sends confetti": "sends confetti", "Sends the given message with fireworks": "Sends the given message with 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 Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", "Video Call": "Video Call", From d53ee58b18a04bedfdb0db13915f3d3606be0ab5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 21 Dec 2020 00:18:58 -0700 Subject: [PATCH 14/14] Appease the linter --- src/effects/snowfall/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/effects/snowfall/index.ts b/src/effects/snowfall/index.ts index a91d17379b..2affb50405 100644 --- a/src/effects/snowfall/index.ts +++ b/src/effects/snowfall/index.ts @@ -117,9 +117,6 @@ export default class Snowfall implements ICanvasEffect { } const height = this.context.canvas.height; for (const particle of arrayFastClone(this.particles)) { - const leftMax = particle.xCol - particle.maximumDrift; - const rightMax = particle.xCol + particle.maximumDrift; - particle.y += particle.gravity; // We treat the drift as a sine function to have a more fluid-like movement instead