@basmilius/sparkle 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1053 -28
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4840 -400
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
- package/src/aurora/consts.ts +3 -0
- package/src/aurora/index.ts +10 -0
- package/src/aurora/layer.ts +180 -0
- package/src/aurora/types.ts +13 -0
- package/src/balloons/consts.ts +3 -0
- package/src/balloons/index.ts +12 -0
- package/src/balloons/layer.ts +169 -0
- package/src/balloons/particle.ts +110 -0
- package/src/balloons/types.ts +14 -0
- package/src/bubbles/consts.ts +3 -0
- package/src/bubbles/index.ts +10 -0
- package/src/bubbles/layer.ts +246 -0
- package/src/bubbles/types.ts +21 -0
- package/src/canvas.ts +32 -1
- package/src/color.ts +19 -0
- package/src/confetti/consts.ts +13 -13
- package/src/confetti/index.ts +20 -2
- package/src/confetti/layer.ts +155 -0
- package/src/confetti/particle.ts +106 -0
- package/src/confetti/shapes.ts +104 -0
- package/src/confetti/types.ts +4 -1
- package/src/distance.ts +1 -1
- package/src/donuts/consts.ts +19 -0
- package/src/donuts/donut.ts +12 -0
- package/src/donuts/index.ts +9 -0
- package/src/donuts/layer.ts +301 -0
- package/src/effect.ts +107 -0
- package/src/fade.ts +87 -0
- package/src/fireflies/consts.ts +3 -0
- package/src/fireflies/index.ts +12 -0
- package/src/fireflies/layer.ts +169 -0
- package/src/fireflies/particle.ts +124 -0
- package/src/fireflies/types.ts +17 -0
- package/src/firepit/consts.ts +3 -0
- package/src/firepit/index.ts +10 -0
- package/src/firepit/layer.ts +193 -0
- package/src/firepit/types.ts +20 -0
- package/src/fireworks/create-explosion.ts +237 -0
- package/src/fireworks/explosion.ts +9 -9
- package/src/fireworks/firework.ts +9 -8
- package/src/fireworks/index.ts +19 -3
- package/src/fireworks/layer.ts +203 -0
- package/src/fireworks/spark.ts +9 -9
- package/src/fireworks/types.ts +2 -2
- package/src/glitter/consts.ts +13 -0
- package/src/glitter/index.ts +9 -0
- package/src/glitter/layer.ts +181 -0
- package/src/glitter/types.ts +33 -0
- package/src/index.ts +27 -0
- package/src/lanterns/consts.ts +13 -0
- package/src/lanterns/index.ts +9 -0
- package/src/lanterns/layer.ts +178 -0
- package/src/lanterns/types.ts +22 -0
- package/src/layer.ts +26 -0
- package/src/leaves/consts.ts +16 -0
- package/src/leaves/index.ts +9 -0
- package/src/leaves/layer.ts +258 -0
- package/src/leaves/types.ts +25 -0
- package/src/lightning/consts.ts +3 -0
- package/src/lightning/index.ts +11 -0
- package/src/lightning/layer.ts +41 -0
- package/src/lightning/system.ts +196 -0
- package/src/lightning/types.ts +20 -0
- package/src/matrix/consts.ts +5 -0
- package/src/matrix/index.ts +9 -0
- package/src/matrix/layer.ts +154 -0
- package/src/matrix/types.ts +17 -0
- package/src/orbits/consts.ts +13 -0
- package/src/orbits/index.ts +9 -0
- package/src/orbits/layer.ts +213 -0
- package/src/orbits/types.ts +27 -0
- package/src/particles/consts.ts +3 -0
- package/src/particles/index.ts +10 -0
- package/src/particles/layer.ts +360 -0
- package/src/particles/types.ts +10 -0
- package/src/petals/consts.ts +13 -0
- package/src/petals/index.ts +10 -0
- package/src/petals/layer.ts +174 -0
- package/src/petals/types.ts +15 -0
- package/src/plasma/consts.ts +3 -0
- package/src/plasma/index.ts +10 -0
- package/src/plasma/layer.ts +107 -0
- package/src/plasma/types.ts +5 -0
- package/src/rain/consts.ts +3 -0
- package/src/rain/index.ts +12 -0
- package/src/rain/layer.ts +194 -0
- package/src/rain/particle.ts +132 -0
- package/src/rain/types.ts +22 -0
- package/src/sandstorm/consts.ts +3 -0
- package/src/sandstorm/index.ts +10 -0
- package/src/sandstorm/layer.ts +152 -0
- package/src/sandstorm/types.ts +10 -0
- package/src/scene.ts +201 -0
- package/src/shooting-stars/index.ts +3 -0
- package/src/shooting-stars/system.ts +151 -0
- package/src/shooting-stars/types.ts +11 -0
- package/src/simulation-canvas.ts +83 -0
- package/src/snow/consts.ts +2 -2
- package/src/snow/index.ts +9 -2
- package/src/snow/{simulation.ts → layer.ts} +64 -89
- package/src/sparklers/consts.ts +3 -0
- package/src/sparklers/index.ts +16 -0
- package/src/sparklers/layer.ts +220 -0
- package/src/sparklers/particle.ts +89 -0
- package/src/sparklers/types.ts +13 -0
- package/src/stars/consts.ts +3 -0
- package/src/stars/index.ts +10 -0
- package/src/stars/layer.ts +139 -0
- package/src/stars/types.ts +12 -0
- package/src/streamers/consts.ts +14 -0
- package/src/streamers/index.ts +10 -0
- package/src/streamers/layer.ts +223 -0
- package/src/streamers/types.ts +14 -0
- package/src/trail.ts +140 -0
- package/src/waves/consts.ts +3 -0
- package/src/waves/index.ts +10 -0
- package/src/waves/layer.ts +164 -0
- package/src/waves/types.ts +10 -0
- package/src/wormhole/consts.ts +3 -0
- package/src/wormhole/index.ts +10 -0
- package/src/wormhole/layer.ts +197 -0
- package/src/wormhole/types.ts +10 -0
- package/src/confetti/simulation.ts +0 -221
- package/src/fireworks/simulation.ts +0 -493
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Shape } from './types';
|
|
2
|
+
|
|
3
|
+
const TWO_PI = Math.PI * 2;
|
|
4
|
+
|
|
5
|
+
export const SHAPE_PATHS: Record<Shape, Path2D> = {
|
|
6
|
+
bowtie: (() => {
|
|
7
|
+
const path = new Path2D();
|
|
8
|
+
path.moveTo(-1, -0.7);
|
|
9
|
+
path.lineTo(0, 0);
|
|
10
|
+
path.lineTo(-1, 0.7);
|
|
11
|
+
path.closePath();
|
|
12
|
+
path.moveTo(1, -0.7);
|
|
13
|
+
path.lineTo(0, 0);
|
|
14
|
+
path.lineTo(1, 0.7);
|
|
15
|
+
path.closePath();
|
|
16
|
+
return path;
|
|
17
|
+
})(),
|
|
18
|
+
circle: (() => {
|
|
19
|
+
const path = new Path2D();
|
|
20
|
+
path.ellipse(0, 0, 0.6, 1, 0, 0, TWO_PI);
|
|
21
|
+
return path;
|
|
22
|
+
})(),
|
|
23
|
+
crescent: (() => {
|
|
24
|
+
const path = new Path2D();
|
|
25
|
+
path.arc(0, 0, 1, 0, TWO_PI, false);
|
|
26
|
+
path.arc(0.45, 0, 0.9, 0, TWO_PI, true);
|
|
27
|
+
return path;
|
|
28
|
+
})(),
|
|
29
|
+
diamond: (() => {
|
|
30
|
+
const path = new Path2D();
|
|
31
|
+
path.moveTo(0, -1);
|
|
32
|
+
path.lineTo(0.6, 0);
|
|
33
|
+
path.lineTo(0, 1);
|
|
34
|
+
path.lineTo(-0.6, 0);
|
|
35
|
+
path.closePath();
|
|
36
|
+
return path;
|
|
37
|
+
})(),
|
|
38
|
+
heart: (() => {
|
|
39
|
+
const path = new Path2D();
|
|
40
|
+
path.moveTo(0, 1);
|
|
41
|
+
path.bezierCurveTo(-0.4, 0.55, -1, 0.1, -1, -0.35);
|
|
42
|
+
path.bezierCurveTo(-1, -0.8, -0.5, -1, 0, -0.6);
|
|
43
|
+
path.bezierCurveTo(0.5, -1, 1, -0.8, 1, -0.35);
|
|
44
|
+
path.bezierCurveTo(1, 0.1, 0.4, 0.55, 0, 1);
|
|
45
|
+
path.closePath();
|
|
46
|
+
return path;
|
|
47
|
+
})(),
|
|
48
|
+
hexagon: (() => {
|
|
49
|
+
const path = new Path2D();
|
|
50
|
+
for (let i = 0; i < 6; i++) {
|
|
51
|
+
const angle = (i * Math.PI / 3) - Math.PI / 2;
|
|
52
|
+
if (i === 0) {
|
|
53
|
+
path.moveTo(Math.cos(angle), Math.sin(angle));
|
|
54
|
+
} else {
|
|
55
|
+
path.lineTo(Math.cos(angle), Math.sin(angle));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
path.closePath();
|
|
59
|
+
return path;
|
|
60
|
+
})(),
|
|
61
|
+
ribbon: (() => {
|
|
62
|
+
const path = new Path2D();
|
|
63
|
+
path.rect(-0.2, -1, 0.4, 2);
|
|
64
|
+
return path;
|
|
65
|
+
})(),
|
|
66
|
+
ring: (() => {
|
|
67
|
+
const path = new Path2D();
|
|
68
|
+
path.arc(0, 0, 1, 0, TWO_PI, false);
|
|
69
|
+
path.arc(0, 0, 0.55, 0, TWO_PI, true);
|
|
70
|
+
return path;
|
|
71
|
+
})(),
|
|
72
|
+
square: (() => {
|
|
73
|
+
const path = new Path2D();
|
|
74
|
+
path.rect(-0.7, -0.7, 1.4, 1.4);
|
|
75
|
+
return path;
|
|
76
|
+
})(),
|
|
77
|
+
star: (() => {
|
|
78
|
+
const path = new Path2D();
|
|
79
|
+
for (let i = 0; i < 10; i++) {
|
|
80
|
+
const r = i % 2 === 0 ? 1 : 0.42;
|
|
81
|
+
const angle = (i * Math.PI / 5) - Math.PI / 2;
|
|
82
|
+
if (i === 0) {
|
|
83
|
+
path.moveTo(r * Math.cos(angle), r * Math.sin(angle));
|
|
84
|
+
} else {
|
|
85
|
+
path.lineTo(r * Math.cos(angle), r * Math.sin(angle));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
path.closePath();
|
|
89
|
+
return path;
|
|
90
|
+
})(),
|
|
91
|
+
triangle: (() => {
|
|
92
|
+
const path = new Path2D();
|
|
93
|
+
for (let i = 0; i < 3; i++) {
|
|
94
|
+
const angle = (i * 2 * Math.PI / 3) - Math.PI / 2;
|
|
95
|
+
if (i === 0) {
|
|
96
|
+
path.moveTo(Math.cos(angle), Math.sin(angle));
|
|
97
|
+
} else {
|
|
98
|
+
path.lineTo(Math.cos(angle), Math.sin(angle));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
path.closePath();
|
|
102
|
+
return path;
|
|
103
|
+
})()
|
|
104
|
+
};
|
package/src/confetti/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type Config = {
|
|
|
3
3
|
readonly colors: string[];
|
|
4
4
|
readonly decay: number;
|
|
5
5
|
readonly gravity: number;
|
|
6
|
+
readonly palette: Palette;
|
|
6
7
|
readonly particles: number;
|
|
7
8
|
readonly shapes: Shape[];
|
|
8
9
|
readonly spread: number;
|
|
@@ -48,6 +49,8 @@ export type ParticleConfig = {
|
|
|
48
49
|
readonly y: number;
|
|
49
50
|
};
|
|
50
51
|
|
|
52
|
+
export type Palette = 'classic' | 'pastel' | 'vibrant' | 'warm';
|
|
53
|
+
|
|
51
54
|
export type RGB = [r: number, g: number, b: number];
|
|
52
55
|
|
|
53
|
-
export type Shape = 'circle' | 'diamond' | 'ribbon' | 'square' | 'star' | 'triangle';
|
|
56
|
+
export type Shape = 'bowtie' | 'circle' | 'crescent' | 'diamond' | 'heart' | 'hexagon' | 'ribbon' | 'ring' | 'square' | 'star' | 'triangle';
|
package/src/distance.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Mulberry32, mulberry32 } from '@basmilius/utils';
|
|
2
|
+
import type { DonutsConfig } from './layer';
|
|
3
|
+
|
|
4
|
+
export const MULBERRY: Mulberry32 = mulberry32(13);
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CONFIG: DonutsConfig = {
|
|
7
|
+
background: '#a51955',
|
|
8
|
+
collisionPadding: 20,
|
|
9
|
+
colors: ['#bd1961', '#da287c'],
|
|
10
|
+
count: 12,
|
|
11
|
+
mouseAvoidance: false,
|
|
12
|
+
mouseAvoidanceRadius: 150,
|
|
13
|
+
mouseAvoidanceStrength: 0.03,
|
|
14
|
+
radiusRange: [60, 90],
|
|
15
|
+
repulsionStrength: 0.02,
|
|
16
|
+
rotationSpeedRange: [0.0005, 0.002],
|
|
17
|
+
speedRange: [0.15, 0.6],
|
|
18
|
+
thickness: 0.39
|
|
19
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Donuts } from './layer';
|
|
2
|
+
import type { DonutsConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createDonuts(config?: DonutsConfig): Effect<DonutsConfig> {
|
|
6
|
+
return new Donuts(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { DonutsConfig };
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { Effect } from '../effect';
|
|
2
|
+
import { DEFAULT_CONFIG, MULBERRY } from './consts';
|
|
3
|
+
import type { Donut } from './donut';
|
|
4
|
+
|
|
5
|
+
export interface DonutsConfig {
|
|
6
|
+
readonly background?: string;
|
|
7
|
+
readonly collisionPadding?: number;
|
|
8
|
+
readonly colors?: string[];
|
|
9
|
+
readonly count?: number;
|
|
10
|
+
readonly mouseAvoidance?: boolean;
|
|
11
|
+
readonly mouseAvoidanceRadius?: number;
|
|
12
|
+
readonly mouseAvoidanceStrength?: number;
|
|
13
|
+
readonly radiusRange?: [number, number];
|
|
14
|
+
readonly repulsionStrength?: number;
|
|
15
|
+
readonly rotationSpeedRange?: [number, number];
|
|
16
|
+
readonly scale?: number;
|
|
17
|
+
readonly speedRange?: [number, number];
|
|
18
|
+
readonly thickness?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Donuts extends Effect<DonutsConfig> {
|
|
22
|
+
readonly #background: string;
|
|
23
|
+
readonly #collisionPadding: number;
|
|
24
|
+
readonly #colors: string[];
|
|
25
|
+
readonly #count: number;
|
|
26
|
+
#mouseAvoidance: boolean;
|
|
27
|
+
#mouseAvoidanceRadius: number;
|
|
28
|
+
#mouseAvoidanceStrength: number;
|
|
29
|
+
readonly #radiusRange: [number, number];
|
|
30
|
+
#repulsionStrength: number;
|
|
31
|
+
readonly #rotationSpeedRange: [number, number];
|
|
32
|
+
readonly #scale: number;
|
|
33
|
+
readonly #speedRange: [number, number];
|
|
34
|
+
readonly #thickness: number;
|
|
35
|
+
readonly #onMouseMoveBound: (event: MouseEvent) => void;
|
|
36
|
+
readonly #onMouseLeaveBound: () => void;
|
|
37
|
+
#donuts: Donut[] = [];
|
|
38
|
+
#mouseX: number = -1;
|
|
39
|
+
#mouseY: number = -1;
|
|
40
|
+
#mouseOnCanvas: boolean = false;
|
|
41
|
+
#width: number = 960;
|
|
42
|
+
#height: number = 540;
|
|
43
|
+
#initialized: boolean = false;
|
|
44
|
+
|
|
45
|
+
constructor(config: DonutsConfig = {}) {
|
|
46
|
+
super();
|
|
47
|
+
|
|
48
|
+
const scale = config.scale ?? 1;
|
|
49
|
+
|
|
50
|
+
this.#background = config.background ?? DEFAULT_CONFIG.background!;
|
|
51
|
+
this.#collisionPadding = (config.collisionPadding ?? DEFAULT_CONFIG.collisionPadding!) * scale;
|
|
52
|
+
this.#colors = config.colors ?? DEFAULT_CONFIG.colors!;
|
|
53
|
+
this.#count = config.count ?? DEFAULT_CONFIG.count!;
|
|
54
|
+
this.#mouseAvoidance = config.mouseAvoidance ?? DEFAULT_CONFIG.mouseAvoidance!;
|
|
55
|
+
this.#mouseAvoidanceRadius = (config.mouseAvoidanceRadius ?? DEFAULT_CONFIG.mouseAvoidanceRadius!) * scale;
|
|
56
|
+
this.#mouseAvoidanceStrength = config.mouseAvoidanceStrength ?? DEFAULT_CONFIG.mouseAvoidanceStrength!;
|
|
57
|
+
this.#radiusRange = [
|
|
58
|
+
(config.radiusRange ?? DEFAULT_CONFIG.radiusRange!)[0] * scale,
|
|
59
|
+
(config.radiusRange ?? DEFAULT_CONFIG.radiusRange!)[1] * scale
|
|
60
|
+
];
|
|
61
|
+
this.#repulsionStrength = config.repulsionStrength ?? DEFAULT_CONFIG.repulsionStrength!;
|
|
62
|
+
this.#rotationSpeedRange = config.rotationSpeedRange ?? DEFAULT_CONFIG.rotationSpeedRange!;
|
|
63
|
+
this.#scale = scale;
|
|
64
|
+
this.#speedRange = [
|
|
65
|
+
(config.speedRange ?? DEFAULT_CONFIG.speedRange!)[0] * scale,
|
|
66
|
+
(config.speedRange ?? DEFAULT_CONFIG.speedRange!)[1] * scale
|
|
67
|
+
];
|
|
68
|
+
this.#thickness = config.thickness ?? DEFAULT_CONFIG.thickness!;
|
|
69
|
+
|
|
70
|
+
this.#onMouseMoveBound = (event: MouseEvent) => this.#onMouseMove(event);
|
|
71
|
+
this.#onMouseLeaveBound = () => this.#onMouseLeave();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onResize(width: number, height: number): void {
|
|
75
|
+
this.#width = width;
|
|
76
|
+
this.#height = height;
|
|
77
|
+
|
|
78
|
+
if (!this.#initialized) {
|
|
79
|
+
this.#initialized = true;
|
|
80
|
+
this.#donuts = [];
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < this.#count; i++) {
|
|
83
|
+
this.#donuts.push(this.#createNonOverlapping());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onMount(canvas: HTMLCanvasElement): void {
|
|
89
|
+
if (this.#mouseAvoidance) {
|
|
90
|
+
canvas.addEventListener('mousemove', this.#onMouseMoveBound, {passive: true});
|
|
91
|
+
canvas.addEventListener('mouseleave', this.#onMouseLeaveBound, {passive: true});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onUnmount(canvas: HTMLCanvasElement): void {
|
|
96
|
+
canvas.removeEventListener('mousemove', this.#onMouseMoveBound);
|
|
97
|
+
canvas.removeEventListener('mouseleave', this.#onMouseLeaveBound);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
configure(config: Partial<DonutsConfig>): void {
|
|
101
|
+
if (config.mouseAvoidance !== undefined) {
|
|
102
|
+
this.#mouseAvoidance = config.mouseAvoidance;
|
|
103
|
+
}
|
|
104
|
+
if (config.mouseAvoidanceRadius !== undefined) {
|
|
105
|
+
this.#mouseAvoidanceRadius = config.mouseAvoidanceRadius;
|
|
106
|
+
}
|
|
107
|
+
if (config.mouseAvoidanceStrength !== undefined) {
|
|
108
|
+
this.#mouseAvoidanceStrength = config.mouseAvoidanceStrength;
|
|
109
|
+
}
|
|
110
|
+
if (config.repulsionStrength !== undefined) {
|
|
111
|
+
this.#repulsionStrength = config.repulsionStrength;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
tick(dt: number, width: number, height: number): void {
|
|
116
|
+
this.#width = width;
|
|
117
|
+
this.#height = height;
|
|
118
|
+
|
|
119
|
+
this.#resolveCollisions(dt);
|
|
120
|
+
|
|
121
|
+
if (this.#mouseAvoidance && this.#mouseOnCanvas) {
|
|
122
|
+
this.#resolveMouseAvoidance(dt);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const donut of this.#donuts) {
|
|
126
|
+
this.#updateDonut(donut, dt);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
131
|
+
ctx.globalAlpha = 1;
|
|
132
|
+
ctx.fillStyle = this.#background;
|
|
133
|
+
ctx.fillRect(0, 0, width, height);
|
|
134
|
+
|
|
135
|
+
for (const donut of this.#donuts) {
|
|
136
|
+
const cos = Math.cos(donut.angle);
|
|
137
|
+
const sin = Math.sin(donut.angle);
|
|
138
|
+
ctx.setTransform(cos, sin, -sin, cos, donut.x, donut.y);
|
|
139
|
+
|
|
140
|
+
ctx.beginPath();
|
|
141
|
+
ctx.arc(0, 0, donut.outerRadius, 0, Math.PI * 2);
|
|
142
|
+
ctx.arc(0, 0, donut.innerRadius, 0, Math.PI * 2, true);
|
|
143
|
+
ctx.closePath();
|
|
144
|
+
|
|
145
|
+
ctx.fillStyle = donut.color;
|
|
146
|
+
ctx.fill();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ctx.resetTransform();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#updateDonut(donut: Donut, dt: number): void {
|
|
153
|
+
const currentSpeed = Math.sqrt(donut.vx * donut.vx + donut.vy * donut.vy);
|
|
154
|
+
|
|
155
|
+
if (currentSpeed > donut.speed) {
|
|
156
|
+
const damping = Math.pow(0.995, dt);
|
|
157
|
+
donut.vx *= damping;
|
|
158
|
+
donut.vy *= damping;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
donut.x += donut.vx * dt;
|
|
162
|
+
donut.y += donut.vy * dt;
|
|
163
|
+
donut.angle += donut.rotationSpeed * dt;
|
|
164
|
+
|
|
165
|
+
const limit = donut.outerRadius * 0.5;
|
|
166
|
+
const width = this.#width;
|
|
167
|
+
const height = this.#height;
|
|
168
|
+
|
|
169
|
+
if (donut.x < -limit) {
|
|
170
|
+
donut.x = -limit;
|
|
171
|
+
donut.vx = Math.abs(donut.vx);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (donut.x > width + limit) {
|
|
175
|
+
donut.x = width + limit;
|
|
176
|
+
donut.vx = -Math.abs(donut.vx);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (donut.y < -limit) {
|
|
180
|
+
donut.y = -limit;
|
|
181
|
+
donut.vy = Math.abs(donut.vy);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (donut.y > height + limit) {
|
|
185
|
+
donut.y = height + limit;
|
|
186
|
+
donut.vy = -Math.abs(donut.vy);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#onMouseMove(event: MouseEvent): void {
|
|
191
|
+
const target = event.currentTarget as HTMLCanvasElement;
|
|
192
|
+
const rect = target.getBoundingClientRect();
|
|
193
|
+
this.#mouseX = event.clientX - rect.left;
|
|
194
|
+
this.#mouseY = event.clientY - rect.top;
|
|
195
|
+
this.#mouseOnCanvas = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#onMouseLeave(): void {
|
|
199
|
+
this.#mouseOnCanvas = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#resolveMouseAvoidance(dt: number): void {
|
|
203
|
+
const radius = this.#mouseAvoidanceRadius;
|
|
204
|
+
const strength = this.#mouseAvoidanceStrength;
|
|
205
|
+
const mx = this.#mouseX;
|
|
206
|
+
const my = this.#mouseY;
|
|
207
|
+
|
|
208
|
+
for (const donut of this.#donuts) {
|
|
209
|
+
const dx = donut.x - mx;
|
|
210
|
+
const dy = donut.y - my;
|
|
211
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
212
|
+
const minDist = donut.outerRadius + radius;
|
|
213
|
+
|
|
214
|
+
if (dist < minDist && dist > 0) {
|
|
215
|
+
const overlap = minDist - dist;
|
|
216
|
+
const nx = dx / dist;
|
|
217
|
+
const ny = dy / dist;
|
|
218
|
+
const force = overlap * strength * dt;
|
|
219
|
+
|
|
220
|
+
donut.vx += nx * force;
|
|
221
|
+
donut.vy += ny * force;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#resolveCollisions(dt: number): void {
|
|
227
|
+
const padding = this.#collisionPadding;
|
|
228
|
+
const strength = this.#repulsionStrength;
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < this.#donuts.length; i++) {
|
|
231
|
+
for (let j = i + 1; j < this.#donuts.length; j++) {
|
|
232
|
+
const a = this.#donuts[i];
|
|
233
|
+
const b = this.#donuts[j];
|
|
234
|
+
const dx = b.x - a.x;
|
|
235
|
+
const dy = b.y - a.y;
|
|
236
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
237
|
+
const minDist = a.outerRadius + b.outerRadius + padding;
|
|
238
|
+
|
|
239
|
+
if (dist < minDist && dist > 0) {
|
|
240
|
+
const overlap = minDist - dist;
|
|
241
|
+
const nx = dx / dist;
|
|
242
|
+
const ny = dy / dist;
|
|
243
|
+
const force = overlap * strength * dt;
|
|
244
|
+
|
|
245
|
+
a.vx -= nx * force;
|
|
246
|
+
a.vy -= ny * force;
|
|
247
|
+
b.vx += nx * force;
|
|
248
|
+
b.vy += ny * force;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#createDonut(): Donut {
|
|
255
|
+
const outerRadius = this.#rand(this.#radiusRange[0], this.#radiusRange[1]);
|
|
256
|
+
const innerRadius = outerRadius * (1 - this.#thickness);
|
|
257
|
+
const speed = this.#rand(this.#speedRange[0], this.#speedRange[1]);
|
|
258
|
+
const direction = MULBERRY.next() * Math.PI * 2;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
outerRadius,
|
|
262
|
+
innerRadius,
|
|
263
|
+
x: this.#rand(-outerRadius, this.#width + outerRadius),
|
|
264
|
+
y: this.#rand(-outerRadius, this.#height + outerRadius),
|
|
265
|
+
angle: MULBERRY.next() * Math.PI * 2,
|
|
266
|
+
speed,
|
|
267
|
+
rotationSpeed: this.#rand(this.#rotationSpeedRange[0], this.#rotationSpeedRange[1]) * (MULBERRY.next() > 0.5 ? 1 : -1),
|
|
268
|
+
color: this.#colors[Math.floor(MULBERRY.next() * this.#colors.length)],
|
|
269
|
+
vx: Math.cos(direction) * speed,
|
|
270
|
+
vy: Math.sin(direction) * speed
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#createNonOverlapping(maxAttempts: number = 200): Donut {
|
|
275
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
276
|
+
const donut = this.#createDonut();
|
|
277
|
+
|
|
278
|
+
if (!this.#overlapsAny(donut)) {
|
|
279
|
+
return donut;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return this.#createDonut();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#overlapsAny(donut: Donut): boolean {
|
|
287
|
+
const minDist = this.#collisionPadding;
|
|
288
|
+
|
|
289
|
+
return this.#donuts.some((other) => {
|
|
290
|
+
const dx = donut.x - other.x;
|
|
291
|
+
const dy = donut.y - other.y;
|
|
292
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
293
|
+
|
|
294
|
+
return dist < donut.outerRadius + other.outerRadius + minDist;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#rand(min: number, max: number): number {
|
|
299
|
+
return MULBERRY.next() * (max - min) + min;
|
|
300
|
+
}
|
|
301
|
+
}
|
package/src/effect.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SimulationCanvas } from './simulation-canvas';
|
|
2
|
+
import type { EdgeFade, EdgeFadeSide, SimulationLayer } from './layer';
|
|
3
|
+
|
|
4
|
+
export type { EdgeFade, EdgeFadeSide };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base class for all visual effects. Implements the internal SimulationLayer interface
|
|
8
|
+
* so that effects can be used both standalone (via mount()) and composed in a Scene.
|
|
9
|
+
*
|
|
10
|
+
* @example Standalone usage
|
|
11
|
+
* const snow = new Snow({ particles: 200 });
|
|
12
|
+
* snow.mount(canvas).start();
|
|
13
|
+
*
|
|
14
|
+
* @example Scene composition
|
|
15
|
+
* const scene = new Scene()
|
|
16
|
+
* .mount(canvas)
|
|
17
|
+
* .layer(new Aurora())
|
|
18
|
+
* .layer(new Snow())
|
|
19
|
+
* .start();
|
|
20
|
+
*/
|
|
21
|
+
export abstract class Effect<TConfig = Record<string, unknown>> implements SimulationLayer {
|
|
22
|
+
#canvas: SimulationCanvas | null = null;
|
|
23
|
+
fade: EdgeFade | null = null;
|
|
24
|
+
|
|
25
|
+
abstract tick(dt: number, width: number, height: number): void;
|
|
26
|
+
|
|
27
|
+
abstract draw(ctx: CanvasRenderingContext2D, width: number, height: number): void;
|
|
28
|
+
|
|
29
|
+
configure(_config: Partial<TConfig>): void {
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onResize(_width: number, _height: number): void {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMount(_canvas: HTMLCanvasElement): void {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onUnmount(_canvas: HTMLCanvasElement): void {
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Apply an edge fade mask when rendering this effect standalone or in a Scene.
|
|
43
|
+
*/
|
|
44
|
+
withFade(fade: EdgeFade): this {
|
|
45
|
+
this.fade = fade;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mount this effect to a canvas element or CSS selector, creating the render loop.
|
|
51
|
+
* Must be called before start().
|
|
52
|
+
*/
|
|
53
|
+
mount(canvas: HTMLCanvasElement | string, options: CanvasRenderingContext2DSettings = {colorSpace: 'display-p3'}): this {
|
|
54
|
+
if (typeof canvas === 'string') {
|
|
55
|
+
const el = document.querySelector<HTMLCanvasElement>(canvas);
|
|
56
|
+
|
|
57
|
+
if (!el) {
|
|
58
|
+
throw new Error(`Effect.mount(): no element found for selector "${canvas}".`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
canvas = el;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.#canvas = new SimulationCanvas(canvas, this as unknown as SimulationLayer, 60, options);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove this effect from its canvas and clean up the render loop.
|
|
70
|
+
*/
|
|
71
|
+
unmount(): this {
|
|
72
|
+
this.#canvas?.destroy();
|
|
73
|
+
this.#canvas = null;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start the render loop. Call mount() first.
|
|
79
|
+
*/
|
|
80
|
+
start(): this {
|
|
81
|
+
this.#canvas?.start();
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pause rendering without destroying state. Use resume() to continue.
|
|
87
|
+
*/
|
|
88
|
+
pause(): this {
|
|
89
|
+
this.#canvas?.pause();
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resume rendering after a pause().
|
|
95
|
+
*/
|
|
96
|
+
resume(): this {
|
|
97
|
+
this.#canvas?.resume();
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Stop rendering and call onUnmount(). Safe to call multiple times.
|
|
103
|
+
*/
|
|
104
|
+
destroy(): void {
|
|
105
|
+
this.unmount();
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/fade.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { EdgeFade, EdgeFadeSide } from './layer';
|
|
2
|
+
|
|
3
|
+
function parseSide(side: EdgeFadeSide): [number, number] {
|
|
4
|
+
return typeof side === 'number' ? [0, side] : side;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function applyEdgeFade(ctx: CanvasRenderingContext2D, width: number, height: number, fade: EdgeFade): void {
|
|
8
|
+
ctx.globalCompositeOperation = 'destination-out';
|
|
9
|
+
|
|
10
|
+
if (fade.top !== undefined) {
|
|
11
|
+
const [near, far] = parseSide(fade.top);
|
|
12
|
+
const nearPx = near * height;
|
|
13
|
+
const farPx = far * height;
|
|
14
|
+
|
|
15
|
+
if (nearPx > 0) {
|
|
16
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
17
|
+
ctx.fillRect(0, 0, width, nearPx);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (farPx > nearPx) {
|
|
21
|
+
const gradient = ctx.createLinearGradient(0, nearPx, 0, farPx);
|
|
22
|
+
gradient.addColorStop(0, 'rgba(0,0,0,1)');
|
|
23
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
|
24
|
+
ctx.fillStyle = gradient;
|
|
25
|
+
ctx.fillRect(0, nearPx, width, farPx - nearPx);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (fade.bottom !== undefined) {
|
|
30
|
+
const [near, far] = parseSide(fade.bottom);
|
|
31
|
+
const nearPx = near * height;
|
|
32
|
+
const farPx = far * height;
|
|
33
|
+
|
|
34
|
+
if (nearPx > 0) {
|
|
35
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
36
|
+
ctx.fillRect(0, height - nearPx, width, nearPx);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (farPx > nearPx) {
|
|
40
|
+
const gradient = ctx.createLinearGradient(0, height - farPx, 0, height - nearPx);
|
|
41
|
+
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
|
42
|
+
gradient.addColorStop(1, 'rgba(0,0,0,1)');
|
|
43
|
+
ctx.fillStyle = gradient;
|
|
44
|
+
ctx.fillRect(0, height - farPx, width, farPx - nearPx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (fade.left !== undefined) {
|
|
49
|
+
const [near, far] = parseSide(fade.left);
|
|
50
|
+
const nearPx = near * width;
|
|
51
|
+
const farPx = far * width;
|
|
52
|
+
|
|
53
|
+
if (nearPx > 0) {
|
|
54
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
55
|
+
ctx.fillRect(0, 0, nearPx, height);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (farPx > nearPx) {
|
|
59
|
+
const gradient = ctx.createLinearGradient(nearPx, 0, farPx, 0);
|
|
60
|
+
gradient.addColorStop(0, 'rgba(0,0,0,1)');
|
|
61
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
|
62
|
+
ctx.fillStyle = gradient;
|
|
63
|
+
ctx.fillRect(nearPx, 0, farPx - nearPx, height);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fade.right !== undefined) {
|
|
68
|
+
const [near, far] = parseSide(fade.right);
|
|
69
|
+
const nearPx = near * width;
|
|
70
|
+
const farPx = far * width;
|
|
71
|
+
|
|
72
|
+
if (nearPx > 0) {
|
|
73
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
74
|
+
ctx.fillRect(width - nearPx, 0, nearPx, height);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (farPx > nearPx) {
|
|
78
|
+
const gradient = ctx.createLinearGradient(width - farPx, 0, width - nearPx, 0);
|
|
79
|
+
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
|
80
|
+
gradient.addColorStop(1, 'rgba(0,0,0,1)');
|
|
81
|
+
ctx.fillStyle = gradient;
|
|
82
|
+
ctx.fillRect(width - farPx, 0, farPx - nearPx, height);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Fireflies } from './layer';
|
|
2
|
+
import type { FirefliesConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createFireflies(config?: FirefliesConfig): Effect<FirefliesConfig> {
|
|
6
|
+
return new Fireflies(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { FireflyParticle, createFireflySprite } from './particle';
|
|
10
|
+
export type { FirefliesConfig };
|
|
11
|
+
export type { FireflyParticleConfig } from './particle';
|
|
12
|
+
export type { Firefly } from './types';
|