forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfetti.ts
More file actions
54 lines (49 loc) · 1.99 KB
/
Copy pathconfetti.ts
File metadata and controls
54 lines (49 loc) · 1.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Where the pieces of a confetti burst go.
*
* Deterministic from a seed rather than `Math.random`, so a burst renders the
* same on the server and the client — a random layout would differ between the
* two and React would discard the markup as a hydration mismatch. It also
* makes the spread testable.
*/
export interface ConfettiParticle {
/** Offset from the burst origin, in pixels. */
x: number;
y: number;
size: number;
/** Height as a multiple of width; confetti is rectangular, not square. */
aspect: number;
rotate: number;
duration: number;
color: string;
}
/**
* Neutral palette. The brand accent is white (INV-UI-1), so the pieces are
* white and greys at varying weight rather than a rainbow — the burst reads as
* the surface breaking up, not as a party favour from another product.
*/
const COLORS = ['#FFFFFF', '#E5E5E5', '#B0B0B0', '#888888', '#35343B'];
/** Deterministic unit noise for index `i`. Cheap, and good enough for scatter. */
function noise(i: number, salt: number): number {
const x = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453;
return x - Math.floor(x);
}
export function confettiParticles(count: number, seed = 0): ConfettiParticle[] {
return Array.from({ length: count }, (_, i) => {
// Spread around the full circle by index so the pieces cannot clump, with
// the noise only jittering each one off its share of the arc.
const angle = (i / count) * Math.PI * 2 + (noise(i, seed) - 0.5) * 0.6;
const distance = 26 + noise(i, seed + 1) * 46;
return {
x: Math.cos(angle) * distance,
// Biased upward: confetti thrown from a point reads better rising than
// sinking, and the banner it replaces sits at the bottom of the rail.
y: Math.sin(angle) * distance - 12,
size: 3 + noise(i, seed + 2) * 4,
aspect: 1.6 + noise(i, seed + 3),
rotate: (noise(i, seed + 4) - 0.5) * 540,
duration: 0.5 + noise(i, seed + 5) * 0.35,
color: COLORS[i % COLORS.length],
};
});
}