@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
|
@@ -1,49 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseColor } from '../color';
|
|
2
|
+
import { Effect } from '../effect';
|
|
2
3
|
import { MULBERRY } from './consts';
|
|
3
4
|
import type { Snowflake } from './snowflake';
|
|
4
5
|
|
|
5
|
-
export interface
|
|
6
|
+
export interface SnowConfig {
|
|
6
7
|
readonly fillStyle?: string;
|
|
7
8
|
readonly particles?: number;
|
|
8
9
|
readonly scale?: number;
|
|
9
10
|
readonly size?: number;
|
|
10
11
|
readonly speed?: number;
|
|
11
|
-
readonly canvasOptions?: CanvasRenderingContext2DSettings;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const SPRITE_SIZE = 64;
|
|
15
15
|
const SPRITE_CENTER = SPRITE_SIZE / 2;
|
|
16
16
|
const SPRITE_RADIUS = SPRITE_SIZE / 2;
|
|
17
17
|
|
|
18
|
-
export class
|
|
18
|
+
export class Snow extends Effect<SnowConfig> {
|
|
19
19
|
readonly #scale: number;
|
|
20
20
|
readonly #size: number;
|
|
21
|
-
|
|
21
|
+
#speed: number;
|
|
22
22
|
readonly #baseOpacity: number;
|
|
23
23
|
#maxParticles: number;
|
|
24
24
|
#time: number = 0;
|
|
25
25
|
#ratio: number = 1;
|
|
26
26
|
#snowflakes: Snowflake[] = [];
|
|
27
27
|
#sprites: HTMLCanvasElement[] = [];
|
|
28
|
+
#height: number = 540;
|
|
28
29
|
|
|
29
|
-
constructor(
|
|
30
|
-
super(
|
|
30
|
+
constructor(config: SnowConfig = {}) {
|
|
31
|
+
super();
|
|
31
32
|
|
|
32
33
|
this.#scale = config.scale ?? 1;
|
|
33
34
|
this.#maxParticles = config.particles ?? 200;
|
|
34
35
|
this.#size = (config.size ?? 9) * this.#scale;
|
|
35
36
|
this.#speed = config.speed ?? 2;
|
|
36
37
|
|
|
37
|
-
const {r, g, b, a} =
|
|
38
|
+
const {r, g, b, a} = parseColor(config.fillStyle ?? 'rgb(255 255 255 / .75)');
|
|
38
39
|
this.#baseOpacity = a;
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
this.canvas.style.top = '0';
|
|
42
|
-
this.canvas.style.left = '0';
|
|
43
|
-
this.canvas.style.height = '100%';
|
|
44
|
-
this.canvas.style.width = '100%';
|
|
45
|
-
|
|
46
|
-
if (this.isSmall) {
|
|
41
|
+
if (innerWidth < 991) {
|
|
47
42
|
this.#maxParticles = Math.floor(this.#maxParticles / 2);
|
|
48
43
|
}
|
|
49
44
|
|
|
@@ -54,77 +49,37 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
|
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const ctx = this.context;
|
|
62
|
-
ctx.clearRect(0, 0, this.width, this.height);
|
|
63
|
-
|
|
64
|
-
for (const snowflake of this.#snowflakes) {
|
|
65
|
-
const px = snowflake.x * this.width;
|
|
66
|
-
const py = snowflake.y * this.height;
|
|
67
|
-
const displayRadius = snowflake.radius * snowflake.depth * this.#ratio;
|
|
68
|
-
const displaySize = displayRadius * 2;
|
|
69
|
-
|
|
70
|
-
if (displaySize < 0.5) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
ctx.globalAlpha = this.#baseOpacity * (0.15 + snowflake.depth * 0.85);
|
|
75
|
-
|
|
76
|
-
if (snowflake.spriteIndex === 3) {
|
|
77
|
-
ctx.save();
|
|
78
|
-
ctx.translate(px, py);
|
|
79
|
-
ctx.rotate(snowflake.rotation);
|
|
80
|
-
ctx.drawImage(
|
|
81
|
-
this.#sprites[snowflake.spriteIndex],
|
|
82
|
-
-displayRadius,
|
|
83
|
-
-displayRadius,
|
|
84
|
-
displaySize,
|
|
85
|
-
displaySize
|
|
86
|
-
);
|
|
87
|
-
ctx.restore();
|
|
88
|
-
} else {
|
|
89
|
-
ctx.drawImage(
|
|
90
|
-
this.#sprites[snowflake.spriteIndex],
|
|
91
|
-
px - displayRadius,
|
|
92
|
-
py - displayRadius,
|
|
93
|
-
displaySize,
|
|
94
|
-
displaySize
|
|
95
|
-
);
|
|
96
|
-
}
|
|
52
|
+
configure(config: Partial<SnowConfig>): void {
|
|
53
|
+
if (config.speed !== undefined) {
|
|
54
|
+
this.#speed = config.speed;
|
|
97
55
|
}
|
|
56
|
+
}
|
|
98
57
|
|
|
99
|
-
|
|
58
|
+
onResize(_width: number, height: number): void {
|
|
59
|
+
this.#height = height;
|
|
100
60
|
}
|
|
101
61
|
|
|
102
|
-
tick(): void {
|
|
103
|
-
|
|
62
|
+
tick(dt: number, _width: number, height: number): void {
|
|
63
|
+
this.#height = height;
|
|
104
64
|
|
|
105
|
-
|
|
65
|
+
const speedFactor = height / (420 * this.#ratio) / this.#speed;
|
|
66
|
+
|
|
67
|
+
this.#time += 0.015 * speedFactor * dt;
|
|
106
68
|
|
|
107
|
-
// Multi-frequency wind for organic movement
|
|
108
69
|
const wind = Math.sin(this.#time * 0.7) * 0.5
|
|
109
|
-
|
|
110
|
-
|
|
70
|
+
+ Math.sin(this.#time * 1.9 + 3) * 0.25
|
|
71
|
+
+ Math.sin(this.#time * 4.3 + 1) * 0.1;
|
|
111
72
|
|
|
112
73
|
for (let index = 0; index < this.#snowflakes.length; index++) {
|
|
113
74
|
const snowflake = this.#snowflakes[index];
|
|
114
75
|
|
|
115
|
-
// Individual swing oscillation
|
|
116
76
|
const swing = Math.sin(this.#time * snowflake.swingFrequency + snowflake.swingOffset) * snowflake.swingAmplitude;
|
|
117
77
|
|
|
118
|
-
|
|
119
|
-
snowflake.
|
|
120
|
-
|
|
121
|
-
// Vertical: individual speed + depth + size influence
|
|
122
|
-
snowflake.y += (snowflake.fallSpeed * 2 + snowflake.depth + snowflake.radius * 0.15) / (700 * speedFactor);
|
|
78
|
+
snowflake.x += (swing + wind * snowflake.depth * 2) * dt / (4000 * speedFactor);
|
|
79
|
+
snowflake.y += (snowflake.fallSpeed * 2 + snowflake.depth + snowflake.radius * 0.15) * dt / (700 * speedFactor);
|
|
123
80
|
|
|
124
|
-
|
|
125
|
-
snowflake.rotation += snowflake.rotationSpeed / speedFactor;
|
|
81
|
+
snowflake.rotation += snowflake.rotationSpeed * dt / speedFactor;
|
|
126
82
|
|
|
127
|
-
// Recycle out-of-bounds particles
|
|
128
83
|
if (snowflake.x > 1.15 || snowflake.x < -0.15 || snowflake.y > 1.05) {
|
|
129
84
|
const recycled = this.#createSnowflake(false);
|
|
130
85
|
|
|
@@ -147,26 +102,52 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
|
|
|
147
102
|
}
|
|
148
103
|
}
|
|
149
104
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
105
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
106
|
+
|
|
107
|
+
for (const snowflake of this.#snowflakes) {
|
|
108
|
+
const px = snowflake.x * width;
|
|
109
|
+
const py = snowflake.y * height;
|
|
110
|
+
const displayRadius = snowflake.radius * snowflake.depth * this.#ratio;
|
|
111
|
+
const displaySize = displayRadius * 2;
|
|
112
|
+
|
|
113
|
+
if (displaySize < 0.5) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ctx.globalAlpha = this.#baseOpacity * (0.15 + snowflake.depth * 0.85);
|
|
118
|
+
|
|
119
|
+
if (snowflake.spriteIndex === 3) {
|
|
120
|
+
const cos = Math.cos(snowflake.rotation);
|
|
121
|
+
const sin = Math.sin(snowflake.rotation);
|
|
122
|
+
ctx.setTransform(cos, sin, -sin, cos, px, py);
|
|
123
|
+
ctx.drawImage(
|
|
124
|
+
this.#sprites[snowflake.spriteIndex],
|
|
125
|
+
-displayRadius,
|
|
126
|
+
-displayRadius,
|
|
127
|
+
displaySize,
|
|
128
|
+
displaySize
|
|
129
|
+
);
|
|
130
|
+
ctx.resetTransform();
|
|
131
|
+
} else {
|
|
132
|
+
ctx.drawImage(
|
|
133
|
+
this.#sprites[snowflake.spriteIndex],
|
|
134
|
+
px - displayRadius,
|
|
135
|
+
py - displayRadius,
|
|
136
|
+
displaySize,
|
|
137
|
+
displaySize
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ctx.globalAlpha = 1;
|
|
159
143
|
}
|
|
160
144
|
|
|
161
145
|
#createSprites(r: number, g: number, b: number): HTMLCanvasElement[] {
|
|
162
146
|
const sprites: HTMLCanvasElement[] = [];
|
|
163
147
|
|
|
164
148
|
const gradientProfiles: [number, number][][] = [
|
|
165
|
-
// 0: Soft glow
|
|
166
149
|
[[0, 0.8], [0.3, 0.4], [0.7, 0.1], [1, 0]],
|
|
167
|
-
// 1: Bright center
|
|
168
150
|
[[0, 1], [0.15, 0.7], [0.5, 0.2], [1, 0]],
|
|
169
|
-
// 2: Compact dot
|
|
170
151
|
[[0, 0.9], [0.25, 0.5], [0.5, 0.1], [1, 0]]
|
|
171
152
|
];
|
|
172
153
|
|
|
@@ -193,7 +174,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
|
|
|
193
174
|
sprites.push(canvas);
|
|
194
175
|
}
|
|
195
176
|
|
|
196
|
-
// 3: Crystal snowflake
|
|
197
177
|
sprites.push(this.#createCrystalSprite(r, g, b));
|
|
198
178
|
|
|
199
179
|
return sprites;
|
|
@@ -205,7 +185,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
|
|
|
205
185
|
canvas.height = SPRITE_SIZE;
|
|
206
186
|
const ctx = canvas.getContext('2d')!;
|
|
207
187
|
|
|
208
|
-
// Soft glow base
|
|
209
188
|
const glow = ctx.createRadialGradient(
|
|
210
189
|
SPRITE_CENTER, SPRITE_CENTER, 0,
|
|
211
190
|
SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS
|
|
@@ -219,7 +198,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
|
|
|
219
198
|
ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, Math.PI * 2);
|
|
220
199
|
ctx.fill();
|
|
221
200
|
|
|
222
|
-
// Crystal arms
|
|
223
201
|
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.7)`;
|
|
224
202
|
ctx.lineWidth = 1.5;
|
|
225
203
|
ctx.lineCap = 'round';
|
|
@@ -231,13 +209,11 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
|
|
|
231
209
|
const tipX = SPRITE_CENTER + Math.cos(angle) * armLength;
|
|
232
210
|
const tipY = SPRITE_CENTER + Math.sin(angle) * armLength;
|
|
233
211
|
|
|
234
|
-
// Main arm
|
|
235
212
|
ctx.beginPath();
|
|
236
213
|
ctx.moveTo(SPRITE_CENTER, SPRITE_CENTER);
|
|
237
214
|
ctx.lineTo(tipX, tipY);
|
|
238
215
|
ctx.stroke();
|
|
239
216
|
|
|
240
|
-
// Side branches at 40% and 65% along the arm
|
|
241
217
|
for (const position of [0.4, 0.65]) {
|
|
242
218
|
const branchX = SPRITE_CENTER + Math.cos(angle) * armLength * position;
|
|
243
219
|
const branchY = SPRITE_CENTER + Math.sin(angle) * armLength * position;
|
|
@@ -256,7 +232,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
|
|
|
256
232
|
}
|
|
257
233
|
}
|
|
258
234
|
|
|
259
|
-
// Center dot
|
|
260
235
|
const centerGlow = ctx.createRadialGradient(
|
|
261
236
|
SPRITE_CENTER, SPRITE_CENTER, 0,
|
|
262
237
|
SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS * 0.12
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Sparklers } from './layer';
|
|
2
|
+
import type { SparklersConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export interface SparklersInstance extends Effect<SparklersConfig> {
|
|
6
|
+
moveTo(x: number, y: number): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createSparklers(config?: SparklersConfig): SparklersInstance {
|
|
10
|
+
return new Sparklers(config) as SparklersInstance;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { SparklerParticle } from './particle';
|
|
14
|
+
export type { SparklersConfig };
|
|
15
|
+
export type { SparklerParticleConfig } from './particle';
|
|
16
|
+
export type { SparklerSpark } from './types';
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { MULBERRY } from './consts';
|
|
4
|
+
import type { SparklerSpark } from './types';
|
|
5
|
+
|
|
6
|
+
export interface SparklersConfig {
|
|
7
|
+
readonly emitRate?: number;
|
|
8
|
+
readonly maxSparks?: number;
|
|
9
|
+
readonly colors?: string[];
|
|
10
|
+
readonly speed?: [number, number];
|
|
11
|
+
readonly friction?: number;
|
|
12
|
+
readonly gravity?: number;
|
|
13
|
+
readonly decay?: [number, number];
|
|
14
|
+
readonly trailLength?: number;
|
|
15
|
+
readonly hoverMode?: boolean;
|
|
16
|
+
readonly scale?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_COLORS = ['#ffcc33', '#ff9900', '#ffffff', '#ffee88'];
|
|
20
|
+
|
|
21
|
+
export class Sparklers extends Effect<SparklersConfig> {
|
|
22
|
+
#scale: number;
|
|
23
|
+
#emitRate: number;
|
|
24
|
+
readonly #maxSparks: number;
|
|
25
|
+
readonly #colorRGBs: [number, number, number][];
|
|
26
|
+
readonly #speedRange: [number, number];
|
|
27
|
+
#friction: number;
|
|
28
|
+
#gravity: number;
|
|
29
|
+
readonly #decayRange: [number, number];
|
|
30
|
+
#trailLength: number;
|
|
31
|
+
#hoverMode: boolean;
|
|
32
|
+
readonly #onMouseMoveBound: (evt: MouseEvent) => void;
|
|
33
|
+
readonly #onMouseLeaveBound: () => void;
|
|
34
|
+
#emitX: number = 0.5;
|
|
35
|
+
#emitY: number = 0.5;
|
|
36
|
+
#mouseOnCanvas: boolean = false;
|
|
37
|
+
#sparks: SparklerSpark[] = [];
|
|
38
|
+
#mountedCanvas: HTMLCanvasElement | null = null;
|
|
39
|
+
#cachedRect: DOMRect | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(config: SparklersConfig = {}) {
|
|
42
|
+
super();
|
|
43
|
+
|
|
44
|
+
this.#scale = config.scale ?? 1;
|
|
45
|
+
this.#emitRate = config.emitRate ?? 8;
|
|
46
|
+
this.#maxSparks = config.maxSparks ?? 300;
|
|
47
|
+
this.#speedRange = config.speed ?? [2, 8];
|
|
48
|
+
this.#friction = config.friction ?? 0.96;
|
|
49
|
+
this.#gravity = config.gravity ?? 0.8;
|
|
50
|
+
this.#decayRange = config.decay ?? [0.02, 0.05];
|
|
51
|
+
this.#trailLength = config.trailLength ?? 3;
|
|
52
|
+
this.#hoverMode = config.hoverMode ?? false;
|
|
53
|
+
|
|
54
|
+
const colors = config.colors ?? DEFAULT_COLORS;
|
|
55
|
+
this.#colorRGBs = colors.map(c => hexToRGB(c));
|
|
56
|
+
|
|
57
|
+
this.#onMouseMoveBound = this.#onMouseMove.bind(this);
|
|
58
|
+
this.#onMouseLeaveBound = this.#onMouseLeave.bind(this);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
moveTo(x: number, y: number): void {
|
|
62
|
+
this.#emitX = x;
|
|
63
|
+
this.#emitY = y;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onMount(canvas: HTMLCanvasElement): void {
|
|
67
|
+
this.#mountedCanvas = canvas;
|
|
68
|
+
this.#cachedRect = canvas.getBoundingClientRect();
|
|
69
|
+
|
|
70
|
+
if (this.#hoverMode) {
|
|
71
|
+
canvas.addEventListener('mousemove', this.#onMouseMoveBound, {passive: true});
|
|
72
|
+
canvas.addEventListener('mouseleave', this.#onMouseLeaveBound, {passive: true});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
onUnmount(canvas: HTMLCanvasElement): void {
|
|
77
|
+
canvas.removeEventListener('mousemove', this.#onMouseMoveBound);
|
|
78
|
+
canvas.removeEventListener('mouseleave', this.#onMouseLeaveBound);
|
|
79
|
+
this.#mountedCanvas = null;
|
|
80
|
+
this.#cachedRect = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onResize(): void {
|
|
84
|
+
if (this.#mountedCanvas) {
|
|
85
|
+
this.#cachedRect = this.#mountedCanvas.getBoundingClientRect();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
configure(config: Partial<SparklersConfig>): void {
|
|
90
|
+
if (config.scale !== undefined) {
|
|
91
|
+
this.#scale = config.scale;
|
|
92
|
+
}
|
|
93
|
+
if (config.emitRate !== undefined) {
|
|
94
|
+
this.#emitRate = config.emitRate;
|
|
95
|
+
}
|
|
96
|
+
if (config.friction !== undefined) {
|
|
97
|
+
this.#friction = config.friction;
|
|
98
|
+
}
|
|
99
|
+
if (config.gravity !== undefined) {
|
|
100
|
+
this.#gravity = config.gravity;
|
|
101
|
+
}
|
|
102
|
+
if (config.trailLength !== undefined) {
|
|
103
|
+
this.#trailLength = config.trailLength;
|
|
104
|
+
}
|
|
105
|
+
if (config.hoverMode !== undefined) {
|
|
106
|
+
this.#hoverMode = config.hoverMode;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
tick(dt: number, width: number, height: number): void {
|
|
111
|
+
if (!this.#hoverMode || this.#mouseOnCanvas) {
|
|
112
|
+
const emitCount = Math.min(this.#emitRate, this.#maxSparks - this.#sparks.length);
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < emitCount; i++) {
|
|
115
|
+
this.#sparks.push(this.#createSpark(width, height));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const frictionFactor = Math.pow(this.#friction, dt);
|
|
120
|
+
let alive = 0;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < this.#sparks.length; i++) {
|
|
123
|
+
const spark = this.#sparks[i];
|
|
124
|
+
|
|
125
|
+
spark.trail.push({x: spark.x, y: spark.y});
|
|
126
|
+
|
|
127
|
+
if (spark.trail.length > this.#trailLength) {
|
|
128
|
+
spark.trail.shift();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
spark.vx *= frictionFactor;
|
|
132
|
+
spark.vy *= frictionFactor;
|
|
133
|
+
spark.vy += this.#gravity * this.#scale * dt;
|
|
134
|
+
|
|
135
|
+
spark.x += spark.vx * dt;
|
|
136
|
+
spark.y += spark.vy * dt;
|
|
137
|
+
|
|
138
|
+
spark.alpha -= spark.decay * dt;
|
|
139
|
+
|
|
140
|
+
if (spark.alpha > 0) {
|
|
141
|
+
this.#sparks[alive++] = spark;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.#sparks.length = alive;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
149
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
150
|
+
|
|
151
|
+
if (!this.#hoverMode || this.#mouseOnCanvas) {
|
|
152
|
+
const cx = this.#emitX * width;
|
|
153
|
+
const cy = this.#emitY * height;
|
|
154
|
+
const glowRadius = 15 * this.#scale;
|
|
155
|
+
const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, glowRadius);
|
|
156
|
+
glow.addColorStop(0, 'rgba(255, 220, 100, 0.8)');
|
|
157
|
+
glow.addColorStop(0.3, 'rgba(255, 180, 50, 0.3)');
|
|
158
|
+
glow.addColorStop(1, 'rgba(255, 150, 0, 0)');
|
|
159
|
+
ctx.fillStyle = glow;
|
|
160
|
+
ctx.beginPath();
|
|
161
|
+
ctx.arc(cx, cy, glowRadius, 0, Math.PI * 2);
|
|
162
|
+
ctx.fill();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const spark of this.#sparks) {
|
|
166
|
+
const [r, g, b] = spark.color;
|
|
167
|
+
|
|
168
|
+
for (let t = 0; t < spark.trail.length; t++) {
|
|
169
|
+
const trailAlpha = spark.alpha * (t / spark.trail.length) * 0.5;
|
|
170
|
+
|
|
171
|
+
if (trailAlpha < 0.01) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const trailSize = spark.size * (t / spark.trail.length) * this.#scale;
|
|
176
|
+
|
|
177
|
+
ctx.beginPath();
|
|
178
|
+
ctx.arc(spark.trail[t].x, spark.trail[t].y, trailSize, 0, Math.PI * 2);
|
|
179
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${trailAlpha})`;
|
|
180
|
+
ctx.fill();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ctx.beginPath();
|
|
184
|
+
ctx.arc(spark.x, spark.y, spark.size * this.#scale, 0, Math.PI * 2);
|
|
185
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${spark.alpha})`;
|
|
186
|
+
ctx.fill();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#onMouseMove(evt: MouseEvent): void {
|
|
193
|
+
const rect = this.#cachedRect ?? (evt.currentTarget as HTMLCanvasElement).getBoundingClientRect();
|
|
194
|
+
this.#emitX = (evt.clientX - rect.left) / rect.width;
|
|
195
|
+
this.#emitY = (evt.clientY - rect.top) / rect.height;
|
|
196
|
+
this.#mouseOnCanvas = true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#onMouseLeave(): void {
|
|
200
|
+
this.#mouseOnCanvas = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#createSpark(width: number, height: number): SparklerSpark {
|
|
204
|
+
const angle = MULBERRY.next() * Math.PI * 2;
|
|
205
|
+
const speed = this.#speedRange[0] + MULBERRY.next() * (this.#speedRange[1] - this.#speedRange[0]);
|
|
206
|
+
const colorIndex = Math.floor(MULBERRY.next() * this.#colorRGBs.length);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
x: this.#emitX * width,
|
|
210
|
+
y: this.#emitY * height,
|
|
211
|
+
vx: Math.cos(angle) * speed * this.#scale,
|
|
212
|
+
vy: Math.sin(angle) * speed * this.#scale,
|
|
213
|
+
alpha: 0.8 + MULBERRY.next() * 0.2,
|
|
214
|
+
color: this.#colorRGBs[colorIndex],
|
|
215
|
+
size: 1 + MULBERRY.next() * 2,
|
|
216
|
+
decay: this.#decayRange[0] + MULBERRY.next() * (this.#decayRange[1] - this.#decayRange[0]),
|
|
217
|
+
trail: []
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Point } from '../point';
|
|
2
|
+
|
|
3
|
+
export interface SparklerParticleConfig {
|
|
4
|
+
readonly decay?: number;
|
|
5
|
+
readonly friction?: number;
|
|
6
|
+
readonly gravity?: number;
|
|
7
|
+
readonly scale?: number;
|
|
8
|
+
readonly size?: number;
|
|
9
|
+
readonly trailLength?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class SparklerParticle {
|
|
13
|
+
readonly #color: [number, number, number];
|
|
14
|
+
readonly #decay: number;
|
|
15
|
+
readonly #friction: number;
|
|
16
|
+
readonly #gravity: number;
|
|
17
|
+
readonly #scale: number;
|
|
18
|
+
readonly #size: number;
|
|
19
|
+
readonly #trailLength: number;
|
|
20
|
+
readonly #trail: Point[] = [];
|
|
21
|
+
#x: number;
|
|
22
|
+
#y: number;
|
|
23
|
+
#vx: number;
|
|
24
|
+
#vy: number;
|
|
25
|
+
#alpha: number = 1;
|
|
26
|
+
|
|
27
|
+
get isDead(): boolean {
|
|
28
|
+
return this.#alpha <= 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get position(): Point {
|
|
32
|
+
return {x: this.#x, y: this.#y};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(position: Point, velocity: Point, color: [number, number, number], config: SparklerParticleConfig = {}) {
|
|
36
|
+
this.#x = position.x;
|
|
37
|
+
this.#y = position.y;
|
|
38
|
+
this.#vx = velocity.x;
|
|
39
|
+
this.#vy = velocity.y;
|
|
40
|
+
this.#color = color;
|
|
41
|
+
this.#decay = config.decay ?? (0.02 + Math.random() * 0.03);
|
|
42
|
+
this.#friction = config.friction ?? 0.96;
|
|
43
|
+
this.#gravity = config.gravity ?? 0.8;
|
|
44
|
+
this.#scale = config.scale ?? 1;
|
|
45
|
+
this.#size = config.size ?? (1 + Math.random() * 2);
|
|
46
|
+
this.#trailLength = config.trailLength ?? 3;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
draw(ctx: CanvasRenderingContext2D): void {
|
|
50
|
+
const [r, g, b] = this.#color;
|
|
51
|
+
|
|
52
|
+
for (let t = 0; t < this.#trail.length; t++) {
|
|
53
|
+
const trailAlpha = this.#alpha * (t / this.#trail.length) * 0.5;
|
|
54
|
+
|
|
55
|
+
if (trailAlpha < 0.01) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const trailSize = this.#size * (t / this.#trail.length) * this.#scale;
|
|
60
|
+
|
|
61
|
+
ctx.beginPath();
|
|
62
|
+
ctx.arc(this.#trail[t].x, this.#trail[t].y, trailSize, 0, Math.PI * 2);
|
|
63
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${trailAlpha})`;
|
|
64
|
+
ctx.fill();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ctx.beginPath();
|
|
68
|
+
ctx.arc(this.#x, this.#y, this.#size * this.#scale, 0, Math.PI * 2);
|
|
69
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${this.#alpha})`;
|
|
70
|
+
ctx.fill();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
tick(dt: number = 1): void {
|
|
74
|
+
this.#trail.push({x: this.#x, y: this.#y});
|
|
75
|
+
|
|
76
|
+
if (this.#trail.length > this.#trailLength) {
|
|
77
|
+
this.#trail.shift();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.#vx *= Math.pow(this.#friction, dt);
|
|
81
|
+
this.#vy *= Math.pow(this.#friction, dt);
|
|
82
|
+
this.#vy += this.#gravity * this.#scale * dt;
|
|
83
|
+
|
|
84
|
+
this.#x += this.#vx * dt;
|
|
85
|
+
this.#y += this.#vy * dt;
|
|
86
|
+
|
|
87
|
+
this.#alpha -= this.#decay * dt;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Stars } from './layer';
|
|
2
|
+
import type { StarsConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createStars(config?: StarsConfig): Effect<StarsConfig> {
|
|
6
|
+
return new Stars(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { StarsConfig };
|
|
10
|
+
export type { Star, StarMode, ShootingStar } from './types';
|