Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a7fd408

Browse files
authored
Merge branch 'develop' into InviteDialog
2 parents f87511e + 61076c3 commit a7fd408

File tree

4 files changed

+206
-1
lines changed

4 files changed

+206
-1
lines changed

res/css/views/elements/_AccessibleButton.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ limitations under the License.
3939
justify-content: center;
4040
font-size: $font-14px;
4141
border: none; // override default <button /> styles
42+
word-break: keep-all; // prevent button text in Chinese/Japanese/Korean (CJK) from being collapsed
4243

4344
&.mx_AccessibleButton_kind_primary_sm,
4445
&.mx_AccessibleButton_kind_danger_sm,

src/effects/hearts/index.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
Copyright 2022 Arseny Uskov
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
import ICanvasEffect from '../ICanvasEffect';
18+
import { arrayFastClone } from "../../utils/arrays";
19+
20+
export type HeartOptions = {
21+
/**
22+
* The maximum number of hearts to render at a given time
23+
*/
24+
maxCount: number;
25+
/**
26+
* The amount of gravity to apply to the hearts
27+
*/
28+
gravity: number;
29+
/**
30+
* The maximum amount of drift (horizontal sway) to apply to the hearts. Each heart varies.
31+
*/
32+
maxDrift: number;
33+
/**
34+
* The maximum amount of tilt to apply to the heart. Each heart varies.
35+
*/
36+
maxRot: number;
37+
};
38+
39+
type Heart = {
40+
x: number;
41+
y: number;
42+
xCol: number;
43+
scale: number;
44+
maximumDrift: number;
45+
maximumRot: number;
46+
gravity: number;
47+
color: string;
48+
};
49+
50+
export const DefaultOptions: HeartOptions = {
51+
maxCount: 120,
52+
gravity: 3.2,
53+
maxDrift: 5,
54+
maxRot: 5,
55+
};
56+
57+
const KEY_FRAME_INTERVAL = 15; // 15ms, roughly
58+
59+
export default class Hearts implements ICanvasEffect {
60+
private readonly options: HeartOptions;
61+
62+
constructor(options: { [key: string]: any }) {
63+
this.options = { ...DefaultOptions, ...options };
64+
}
65+
66+
private context: CanvasRenderingContext2D | null = null;
67+
private particles: Array<Heart> = [];
68+
private lastAnimationTime: number;
69+
70+
private colours = [
71+
'rgba(194,210,224,1)',
72+
'rgba(235,214,219,1)',
73+
'rgba(255,211,45,1)',
74+
'rgba(255,190,174,1)',
75+
'rgba(255,173,226,1)',
76+
'rgba(242,114,171,1)',
77+
'rgba(228,55,116,1)',
78+
'rgba(255,86,130,1)',
79+
'rgba(244,36,57,1)',
80+
'rgba(247,126,157,1)',
81+
'rgba(243,142,140,1)',
82+
'rgba(252,116,183,1)'];
83+
84+
public isRunning: boolean;
85+
86+
public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
87+
if (!canvas) {
88+
return;
89+
}
90+
this.context = canvas.getContext('2d');
91+
this.particles = [];
92+
const count = this.options.maxCount;
93+
while (this.particles.length < count) {
94+
this.particles.push(this.resetParticle({} as Heart, canvas.width, canvas.height));
95+
}
96+
this.isRunning = true;
97+
requestAnimationFrame(this.renderLoop);
98+
if (timeout) {
99+
window.setTimeout(this.stop, timeout);
100+
}
101+
};
102+
103+
public stop = async () => {
104+
this.isRunning = false;
105+
};
106+
107+
private resetParticle = (particle: Heart, width: number, height: number): Heart => {
108+
particle.color = this.colours[(Math.random() * this.colours.length) | 0];
109+
particle.x = Math.random() * width;
110+
particle.y = Math.random() * height + height;
111+
particle.xCol = particle.x;
112+
particle.scale = (Math.random() * 0.07) + 0.04;
113+
particle.maximumDrift = (Math.random() * this.options.maxDrift) + 3.5;
114+
particle.maximumRot = (Math.random() * this.options.maxRot) + 3.5;
115+
particle.gravity = this.options.gravity + (Math.random() * 4.8);
116+
return particle;
117+
};
118+
119+
private renderLoop = (): void => {
120+
if (!this.context || !this.context.canvas) {
121+
return;
122+
}
123+
if (this.particles.length === 0) {
124+
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
125+
} else {
126+
const timeDelta = Date.now() - this.lastAnimationTime;
127+
if (timeDelta >= KEY_FRAME_INTERVAL || !this.lastAnimationTime) {
128+
// Clear the screen first
129+
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
130+
131+
this.lastAnimationTime = Date.now();
132+
this.animateAndRenderHearts();
133+
}
134+
requestAnimationFrame(this.renderLoop);
135+
}
136+
};
137+
138+
private animateAndRenderHearts() {
139+
if (!this.context || !this.context.canvas) {
140+
return;
141+
}
142+
for (const particle of arrayFastClone(this.particles)) {
143+
particle.y -= particle.gravity;
144+
145+
// We treat the drift as a sine function to have a more fluid-like movement instead
146+
// of a pong-like movement off walls of the X column. This means that for
147+
// $x=A\sin(\frac{2\pi}{P}y)$ we use the `maximumDrift` as the amplitude (A) and a
148+
// large multiplier to create a very long waveform through P.
149+
const peakDistance = 75 * particle.maximumDrift;
150+
const PI2 = Math.PI * 2;
151+
particle.x = 6 * particle.maximumDrift * Math.sin(0.7 * (PI2 / peakDistance) * particle.y);
152+
particle.x += particle.xCol; // bring the particle to the right place
153+
154+
const posScale = 1 / particle.scale;
155+
const x = particle.x * posScale;
156+
const y = particle.y * posScale;
157+
158+
this.context.save();
159+
this.context.scale(particle.scale, particle.scale);
160+
this.context.beginPath();
161+
162+
// Rotate the heart about its centre.
163+
// The tilt of the heart is modelled similarly to its horizontal drift,
164+
// using a sine function.
165+
this.context.translate(248 + x, 215 + y);
166+
this.context.rotate((1 / 10) * particle.maximumRot * Math.sin((PI2 / peakDistance) * particle.y * 0.8));
167+
this.context.translate(-248 - x, -215 - y);
168+
169+
// Use bezier curves to draw a heart using pre-calculated coordinates.
170+
this.context.moveTo(140 + x, 20 + y);
171+
this.context.bezierCurveTo(73 + x, 20 + y, 20 + x, 74 + y, 20 + x, 140 + y);
172+
this.context.bezierCurveTo(20 + x, 275 + y, 156 + x, 310 + y, 248 + x, 443 + y);
173+
this.context.bezierCurveTo(336 + x, 311 + y, 477 + x, 270 + y, 477 + x, 140 + y);
174+
this.context.bezierCurveTo(477 + x, 74 + y, 423 + x, 20 + y, 357 + x, 20 + y);
175+
this.context.bezierCurveTo(309 + x, 20 + y, 267 + x, 48 + y, 248 + x, 89 + y);
176+
this.context.bezierCurveTo(229 + x, 48 + y, 188 + x, 20 + y, 140 + x, 20 + y);
177+
this.context.closePath();
178+
179+
this.context.fillStyle = particle.color;
180+
this.context.fill();
181+
182+
this.context.restore();
183+
184+
// Remove any dead hearts after a 100px wide margin.
185+
if (particle.y < -100) {
186+
const idx = this.particles.indexOf(particle);
187+
this.particles.splice(idx, 1);
188+
}
189+
}
190+
}
191+
}

src/effects/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { FireworksOptions } from "./fireworks";
2121
import { RainfallOptions } from "./rainfall";
2222
import { SnowfallOptions } from "./snowfall";
2323
import { SpaceInvadersOptions } from "./spaceinvaders";
24+
import { HeartOptions } from "./hearts";
2425

2526
/**
2627
* This configuration defines room effects that can be triggered by custom message types and emojis
@@ -85,5 +86,15 @@ export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
8586
gravity: 0.01,
8687
},
8788
} as Effect<SpaceInvadersOptions>,
89+
{
90+
emojis: ["💝"],
91+
msgType: "io.element.effect.hearts",
92+
command: "hearts",
93+
description: () => _td("Sends the given message with hearts"),
94+
fallbackMessage: () => _t("sends hearts") + " 💝",
95+
options: {
96+
maxCount: 120,
97+
gravity: 3.2,
98+
},
99+
} as Effect<HeartOptions>,
88100
];
89-

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,8 @@
988988
"sends snowfall": "sends snowfall",
989989
"Sends the given message with a space themed effect": "Sends the given message with a space themed effect",
990990
"sends space invaders": "sends space invaders",
991+
"Sends the given message with hearts": "Sends the given message with hearts",
992+
"sends hearts": "sends hearts",
991993
"Server error": "Server error",
992994
"Command error": "Command error",
993995
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",

0 commit comments

Comments
 (0)