@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,237 @@
|
|
|
1
|
+
import type { Point } from '../point';
|
|
2
|
+
import { Explosion } from './explosion';
|
|
3
|
+
import { EXPLOSION_CONFIGS, type ExplosionType, type FireworkVariant } from './types';
|
|
4
|
+
|
|
5
|
+
function between(rng: () => number, min: number, max: number): number {
|
|
6
|
+
return min + rng() * (max - min);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates an array of {@link Explosion} particles for the given firework variant.
|
|
11
|
+
* Use this to fire a fully formed explosion burst in your own render loop without
|
|
12
|
+
* needing a {@link Fireworks} instance.
|
|
13
|
+
*
|
|
14
|
+
* @param variant - The firework variant to create.
|
|
15
|
+
* @param position - The center position of the explosion in canvas pixels.
|
|
16
|
+
* @param hue - Base hue in degrees (0–360).
|
|
17
|
+
* @param options - Optional overrides for `lineWidth` (default `5`) and `scale` (default `1`).
|
|
18
|
+
* @param rng - RNG function returning values in [0, 1). Defaults to `Math.random`.
|
|
19
|
+
*/
|
|
20
|
+
export function createExplosion(
|
|
21
|
+
variant: FireworkVariant,
|
|
22
|
+
position: Point,
|
|
23
|
+
hue: number,
|
|
24
|
+
options: { lineWidth?: number; scale?: number } = {},
|
|
25
|
+
rng: () => number = Math.random
|
|
26
|
+
): Explosion[] {
|
|
27
|
+
const lineWidth = options.lineWidth ?? 5;
|
|
28
|
+
const scale = options.scale ?? 1;
|
|
29
|
+
const explosions: Explosion[] = [];
|
|
30
|
+
|
|
31
|
+
switch (variant) {
|
|
32
|
+
case 'saturn':
|
|
33
|
+
createSaturn(explosions, position, hue, lineWidth, scale, rng);
|
|
34
|
+
break;
|
|
35
|
+
case 'dahlia':
|
|
36
|
+
createDahlia(explosions, position, hue, lineWidth, scale, rng);
|
|
37
|
+
break;
|
|
38
|
+
case 'heart':
|
|
39
|
+
createHeart(explosions, position, hue, lineWidth, scale, rng);
|
|
40
|
+
break;
|
|
41
|
+
case 'spiral':
|
|
42
|
+
createSpiral(explosions, position, hue, lineWidth, scale, rng);
|
|
43
|
+
break;
|
|
44
|
+
case 'flower':
|
|
45
|
+
createFlower(explosions, position, hue, lineWidth, scale, rng);
|
|
46
|
+
break;
|
|
47
|
+
case 'concentric':
|
|
48
|
+
createConcentric(explosions, position, hue, lineWidth, scale, rng);
|
|
49
|
+
break;
|
|
50
|
+
default: {
|
|
51
|
+
const type: ExplosionType = variant;
|
|
52
|
+
const config = EXPLOSION_CONFIGS[type];
|
|
53
|
+
const count = Math.floor(between(rng, config.particleCount[0], config.particleCount[1]));
|
|
54
|
+
const effectiveHue = type === 'brocade' ? between(rng, 35, 50) : hue;
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < count; i++) {
|
|
57
|
+
let angle: number | undefined;
|
|
58
|
+
let speed: number | undefined;
|
|
59
|
+
|
|
60
|
+
if (type === 'ring') {
|
|
61
|
+
angle = (i / count) * Math.PI * 2;
|
|
62
|
+
speed = between(rng, config.speed[0], config.speed[1]) * 0.5 + config.speed[0] * 0.5;
|
|
63
|
+
} else if (type === 'palm' || type === 'horsetail') {
|
|
64
|
+
const spread = type === 'horsetail' ? Math.PI / 8 : Math.PI / 5;
|
|
65
|
+
angle = -Math.PI / 2 + between(rng, -spread, spread);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
explosions.push(new Explosion(position, effectiveHue, lineWidth, type, scale, angle, speed));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return explosions;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createSaturn(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
|
|
77
|
+
const velocity = between(rng, 4, 6);
|
|
78
|
+
const shellCount = Math.floor(between(rng, 25, 35));
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < shellCount; i++) {
|
|
81
|
+
const rad = (i / shellCount) * Math.PI * 2;
|
|
82
|
+
|
|
83
|
+
explosions.push(new Explosion(
|
|
84
|
+
position, hue, lineWidth, 'peony', scale,
|
|
85
|
+
rad + between(rng, -0.05, 0.05),
|
|
86
|
+
velocity + between(rng, -0.25, 0.25)
|
|
87
|
+
));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fillCount = Math.floor(between(rng, 40, 60));
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < fillCount; i++) {
|
|
93
|
+
explosions.push(new Explosion(
|
|
94
|
+
position, hue, lineWidth, 'peony', scale,
|
|
95
|
+
between(rng, 0, Math.PI * 2),
|
|
96
|
+
velocity * between(rng, 0, 1)
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ringRotation = between(rng, 0, Math.PI * 2);
|
|
101
|
+
const ringCount = Math.floor(between(rng, 40, 55));
|
|
102
|
+
const ringVx = velocity * between(rng, 2, 3);
|
|
103
|
+
const ringVy = velocity * 0.6;
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < ringCount; i++) {
|
|
106
|
+
const rad = (i / ringCount) * Math.PI * 2;
|
|
107
|
+
const cx = Math.cos(rad) * ringVx + between(rng, -0.25, 0.25);
|
|
108
|
+
const cy = Math.sin(rad) * ringVy + between(rng, -0.25, 0.25);
|
|
109
|
+
const cosR = Math.cos(ringRotation);
|
|
110
|
+
const sinR = Math.sin(ringRotation);
|
|
111
|
+
const vx = cx * cosR - cy * sinR;
|
|
112
|
+
const vy = cx * sinR + cy * cosR;
|
|
113
|
+
const vz = Math.sin(rad) * velocity * 0.8;
|
|
114
|
+
|
|
115
|
+
explosions.push(new Explosion(
|
|
116
|
+
position, hue + 60, lineWidth, 'ring', scale,
|
|
117
|
+
Math.atan2(vy, vx),
|
|
118
|
+
Math.sqrt(vx * vx + vy * vy),
|
|
119
|
+
vz
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createDahlia(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
|
|
125
|
+
const petalCount = Math.floor(between(rng, 6, 9));
|
|
126
|
+
const particlesPerPetal = Math.floor(between(rng, 8, 12));
|
|
127
|
+
|
|
128
|
+
for (let petal = 0; petal < petalCount; petal++) {
|
|
129
|
+
const baseAngle = (petal / petalCount) * Math.PI * 2;
|
|
130
|
+
const petalHue = hue + (petal % 2 === 0 ? 25 : -25);
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < particlesPerPetal; i++) {
|
|
133
|
+
explosions.push(new Explosion(
|
|
134
|
+
position, petalHue, lineWidth, 'dahlia', scale,
|
|
135
|
+
baseAngle + between(rng, -0.3, 0.3)
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createHeart(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
|
|
142
|
+
const velocity = between(rng, 3, 5);
|
|
143
|
+
const count = Math.floor(between(rng, 60, 80));
|
|
144
|
+
const rotation = between(rng, -0.3, 0.3);
|
|
145
|
+
const cosR = Math.cos(rotation);
|
|
146
|
+
const sinR = Math.sin(rotation);
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < count; i++) {
|
|
149
|
+
const t = (i / count) * Math.PI * 2;
|
|
150
|
+
const hx = 16 * Math.pow(Math.sin(t), 3);
|
|
151
|
+
const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
|
|
152
|
+
const s = velocity / 16;
|
|
153
|
+
const vx = hx * s;
|
|
154
|
+
const vy = hy * s;
|
|
155
|
+
const rvx = vx * cosR - vy * sinR;
|
|
156
|
+
const rvy = vx * sinR + vy * cosR;
|
|
157
|
+
|
|
158
|
+
explosions.push(new Explosion(
|
|
159
|
+
position, hue, lineWidth, 'heart', scale,
|
|
160
|
+
Math.atan2(rvy, rvx),
|
|
161
|
+
Math.max(0.1, Math.sqrt(rvx * rvx + rvy * rvy) + between(rng, -0.15, 0.15))
|
|
162
|
+
));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createSpiral(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
|
|
167
|
+
const arms = Math.floor(between(rng, 3, 5));
|
|
168
|
+
const particlesPerArm = Math.floor(between(rng, 15, 20));
|
|
169
|
+
const twist = between(rng, 2, 3.5);
|
|
170
|
+
const baseRotation = between(rng, 0, Math.PI * 2);
|
|
171
|
+
|
|
172
|
+
for (let arm = 0; arm < arms; arm++) {
|
|
173
|
+
const baseAngle = baseRotation + (arm / arms) * Math.PI * 2;
|
|
174
|
+
const armHue = hue + arm * (360 / arms / 3);
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < particlesPerArm; i++) {
|
|
177
|
+
const progress = i / particlesPerArm;
|
|
178
|
+
|
|
179
|
+
explosions.push(new Explosion(
|
|
180
|
+
position, armHue, lineWidth, 'spiral', scale,
|
|
181
|
+
baseAngle + progress * twist,
|
|
182
|
+
2 + progress * 8 + between(rng, -0.3, 0.3)
|
|
183
|
+
));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createFlower(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
|
|
189
|
+
const velocity = between(rng, 4, 7);
|
|
190
|
+
const count = Math.floor(between(rng, 70, 90));
|
|
191
|
+
const petals = Math.floor(between(rng, 2, 4));
|
|
192
|
+
const rotation = between(rng, 0, Math.PI * 2);
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < count; i++) {
|
|
195
|
+
const t = (i / count) * Math.PI * 2;
|
|
196
|
+
const r = Math.abs(Math.cos(petals * t));
|
|
197
|
+
const speed = velocity * r;
|
|
198
|
+
|
|
199
|
+
if (speed < 0.3) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
explosions.push(new Explosion(
|
|
204
|
+
position, hue + between(rng, -15, 15), lineWidth, 'flower', scale,
|
|
205
|
+
t + rotation,
|
|
206
|
+
speed + between(rng, -0.2, 0.2)
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createConcentric(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
|
|
212
|
+
const outerCount = Math.floor(between(rng, 35, 50));
|
|
213
|
+
const outerSpeed = between(rng, 7, 10);
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < outerCount; i++) {
|
|
216
|
+
const angle = (i / outerCount) * Math.PI * 2;
|
|
217
|
+
|
|
218
|
+
explosions.push(new Explosion(
|
|
219
|
+
position, hue, lineWidth, 'ring', scale,
|
|
220
|
+
angle + between(rng, -0.05, 0.05),
|
|
221
|
+
outerSpeed + between(rng, -0.25, 0.25)
|
|
222
|
+
));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const innerCount = Math.floor(between(rng, 25, 35));
|
|
226
|
+
const innerSpeed = between(rng, 3, 5);
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < innerCount; i++) {
|
|
229
|
+
const angle = (i / innerCount) * Math.PI * 2;
|
|
230
|
+
|
|
231
|
+
explosions.push(new Explosion(
|
|
232
|
+
position, hue + 120, lineWidth, 'ring', scale,
|
|
233
|
+
angle + between(rng, -0.05, 0.05),
|
|
234
|
+
innerSpeed + between(rng, -0.25, 0.25)
|
|
235
|
+
));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -77,7 +77,7 @@ export class Explosion {
|
|
|
77
77
|
return false;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
if (this.#alpha <=
|
|
80
|
+
if (this.#alpha <= 0.4) {
|
|
81
81
|
this.#hasCrackled = true;
|
|
82
82
|
return true;
|
|
83
83
|
}
|
|
@@ -147,21 +147,21 @@ export class Explosion {
|
|
|
147
147
|
ctx.restore();
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
tick(): void {
|
|
150
|
+
tick(dt: number): void {
|
|
151
151
|
this.#trail.pop();
|
|
152
152
|
this.#trail.unshift({...this.#position});
|
|
153
153
|
|
|
154
|
-
this.#speed *= this.#config.friction;
|
|
155
|
-
this.#vz *= this.#config.friction;
|
|
154
|
+
this.#speed *= Math.pow(this.#config.friction, dt);
|
|
155
|
+
this.#vz *= Math.pow(this.#config.friction, dt);
|
|
156
156
|
|
|
157
|
-
this.#position.x += Math.cos(this.#angle) * this.#speed;
|
|
158
|
-
this.#position.y += Math.sin(this.#angle) * this.#speed + this.#config.gravity;
|
|
159
|
-
this.#z += this.#vz;
|
|
157
|
+
this.#position.x += Math.cos(this.#angle) * this.#speed * dt;
|
|
158
|
+
this.#position.y += (Math.sin(this.#angle) * this.#speed + this.#config.gravity) * dt;
|
|
159
|
+
this.#z += this.#vz * dt;
|
|
160
160
|
|
|
161
161
|
this.#depthScale = PERSPECTIVE / (PERSPECTIVE + this.#z);
|
|
162
162
|
|
|
163
|
-
this.#alpha -= this.#decay;
|
|
164
|
-
this.#sparkleTimer
|
|
163
|
+
this.#alpha -= this.#decay * dt;
|
|
164
|
+
this.#sparkleTimer += dt;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
#drawShape(ctx: CanvasRenderingContext2D, ds: number, alpha: number): void {
|
|
@@ -87,18 +87,18 @@ export class Firework extends EventTarget {
|
|
|
87
87
|
ctx.restore();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
tick(): void {
|
|
90
|
+
tick(dt: number): void {
|
|
91
91
|
this.#trail.pop();
|
|
92
92
|
this.#trail.unshift({...this.#position});
|
|
93
93
|
|
|
94
|
-
this.#speed *= this.#acceleration;
|
|
94
|
+
this.#speed *= Math.pow(this.#acceleration, dt);
|
|
95
95
|
|
|
96
96
|
const vx = Math.cos(this.#angle) * this.#speed;
|
|
97
97
|
const vy = Math.sin(this.#angle) * this.#speed;
|
|
98
98
|
|
|
99
99
|
this.#distanceTraveled = distance(this.#startPosition, {
|
|
100
|
-
x: this.#position.x + vx,
|
|
101
|
-
y: this.#position.y + vy
|
|
100
|
+
x: this.#position.x + vx * dt,
|
|
101
|
+
y: this.#position.y + vy * dt
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
if (this.#distanceTraveled >= this.#distance) {
|
|
@@ -106,12 +106,13 @@ export class Firework extends EventTarget {
|
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
this.#position.x += vx;
|
|
110
|
-
this.#position.y += vy;
|
|
109
|
+
this.#position.x += vx * dt;
|
|
110
|
+
this.#position.y += vy * dt;
|
|
111
111
|
|
|
112
|
-
this.#sparkTimer
|
|
112
|
+
this.#sparkTimer += dt;
|
|
113
113
|
|
|
114
|
-
if (this.#sparkTimer
|
|
114
|
+
if (this.#sparkTimer >= 3) {
|
|
115
|
+
this.#sparkTimer -= 3;
|
|
115
116
|
this.#pendingSparks.push(new Spark(
|
|
116
117
|
this.#position,
|
|
117
118
|
this.#hue,
|
package/src/fireworks/index.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Fireworks } from './layer';
|
|
2
|
+
import type { FireworksConfig, FireworkVariant } from './types';
|
|
3
|
+
import type { Point } from '../point';
|
|
4
|
+
import type { Effect } from '../effect';
|
|
5
|
+
|
|
6
|
+
export interface FireworksInstance extends Effect<FireworksConfig> {
|
|
7
|
+
launch(variant: FireworkVariant, position?: Point): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createFireworks(config?: FireworksConfig): FireworksInstance {
|
|
11
|
+
return new Fireworks(config) as FireworksInstance;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { createExplosion } from './create-explosion';
|
|
15
|
+
export { Explosion } from './explosion';
|
|
16
|
+
export { Firework } from './firework';
|
|
17
|
+
export { Spark } from './spark';
|
|
18
|
+
export { EXPLOSION_CONFIGS, FIREWORK_VARIANTS } from './types';
|
|
19
|
+
export type { ExplosionConfig, ExplosionType, FireworksConfig, FireworkVariant, ParticleShape } from './types';
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Effect } from '../effect';
|
|
2
|
+
import type { Point } from '../point';
|
|
3
|
+
import { MULBERRY } from './consts';
|
|
4
|
+
import { createExplosion } from './create-explosion';
|
|
5
|
+
import { Explosion } from './explosion';
|
|
6
|
+
import { Firework } from './firework';
|
|
7
|
+
import { Spark } from './spark';
|
|
8
|
+
import { FIREWORK_VARIANTS, type FireworksConfig, type FireworkVariant } from './types';
|
|
9
|
+
|
|
10
|
+
export class Fireworks extends Effect<FireworksConfig> {
|
|
11
|
+
#explosions: Explosion[] = [];
|
|
12
|
+
#fireworks: Firework[] = [];
|
|
13
|
+
#sparks: Spark[] = [];
|
|
14
|
+
#hue: number = 120;
|
|
15
|
+
#spawnTimer: number = 0;
|
|
16
|
+
#positionRandom = MULBERRY.fork();
|
|
17
|
+
#autoSpawn: boolean;
|
|
18
|
+
#variants: FireworkVariant[];
|
|
19
|
+
readonly #baseSize: number;
|
|
20
|
+
#scale: number;
|
|
21
|
+
readonly #tailWidth: number;
|
|
22
|
+
#width: number = 960;
|
|
23
|
+
#height: number = 540;
|
|
24
|
+
|
|
25
|
+
constructor(config: FireworksConfig = {}) {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
const scale = config.scale ?? 1;
|
|
29
|
+
this.#autoSpawn = config.autoSpawn ?? true;
|
|
30
|
+
this.#variants = config.variants?.length ? [...config.variants] : [...FIREWORK_VARIANTS];
|
|
31
|
+
this.#baseSize = 5 * scale;
|
|
32
|
+
this.#scale = scale;
|
|
33
|
+
this.#tailWidth = 2 * scale;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onResize(width: number, height: number): void {
|
|
37
|
+
this.#width = width;
|
|
38
|
+
this.#height = height;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
launch(variant: FireworkVariant, position?: Point): void {
|
|
42
|
+
const pos = position ?? {x: this.#width / 2, y: this.#height * 0.4};
|
|
43
|
+
this.#hue = MULBERRY.nextBetween(0, 360);
|
|
44
|
+
this.#spawnExplosion(pos, this.#hue, variant);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
configure(config: Partial<FireworksConfig>): void {
|
|
48
|
+
if (config.scale !== undefined) {
|
|
49
|
+
this.#scale = config.scale;
|
|
50
|
+
}
|
|
51
|
+
if (config.autoSpawn !== undefined) {
|
|
52
|
+
this.#autoSpawn = config.autoSpawn;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(config.variants) && config.variants.length > 0) {
|
|
55
|
+
this.#variants = [...config.variants];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
tick(dt: number, width: number, height: number): void {
|
|
60
|
+
this.#width = width;
|
|
61
|
+
this.#height = height;
|
|
62
|
+
this.#spawnTimer += dt;
|
|
63
|
+
|
|
64
|
+
const isSmall = innerWidth < 991;
|
|
65
|
+
const spawnInterval = isSmall ? 60 : 30;
|
|
66
|
+
|
|
67
|
+
if (this.#autoSpawn && this.#fireworks.length < 6 && this.#spawnTimer >= spawnInterval) {
|
|
68
|
+
this.#spawnTimer -= spawnInterval;
|
|
69
|
+
let count = MULBERRY.nextBetween(1, 100) < 10 ? 2 : 1;
|
|
70
|
+
|
|
71
|
+
while (count--) {
|
|
72
|
+
this.#hue = MULBERRY.nextBetween(0, 360);
|
|
73
|
+
this.#createFirework();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const firework of this.#fireworks) {
|
|
78
|
+
firework.tick(dt);
|
|
79
|
+
const collected = firework.collectSparks();
|
|
80
|
+
for (let i = 0; i < collected.length; i++) {
|
|
81
|
+
this.#sparks.push(collected[i]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const explosion of this.#explosions) {
|
|
86
|
+
explosion.tick(dt);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const spark of this.#sparks) {
|
|
90
|
+
spark.tick(dt);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const newExplosions: Explosion[] = [];
|
|
94
|
+
const newSparks: Spark[] = [];
|
|
95
|
+
|
|
96
|
+
for (const explosion of this.#explosions) {
|
|
97
|
+
if (explosion.checkSplit()) {
|
|
98
|
+
for (let i = 0; i < 4; i++) {
|
|
99
|
+
const angle = explosion.angle + (Math.PI / 2) * i + Math.PI / 4;
|
|
100
|
+
|
|
101
|
+
newExplosions.push(new Explosion(
|
|
102
|
+
explosion.position,
|
|
103
|
+
explosion.hue,
|
|
104
|
+
this.#baseSize * 0.6,
|
|
105
|
+
'peony',
|
|
106
|
+
this.#scale,
|
|
107
|
+
angle,
|
|
108
|
+
MULBERRY.nextBetween(3, 6)
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (explosion.checkCrackle()) {
|
|
114
|
+
for (let j = 0; j < 14; j++) {
|
|
115
|
+
const angle = MULBERRY.nextBetween(0, Math.PI * 2);
|
|
116
|
+
const speed = MULBERRY.nextBetween(3, 8);
|
|
117
|
+
|
|
118
|
+
newSparks.push(new Spark(
|
|
119
|
+
explosion.position,
|
|
120
|
+
explosion.hue + MULBERRY.nextBetween(-30, 30),
|
|
121
|
+
Math.cos(angle) * speed,
|
|
122
|
+
Math.sin(angle) * speed
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.#explosions.push(...newExplosions);
|
|
129
|
+
this.#sparks.push(...newSparks);
|
|
130
|
+
|
|
131
|
+
let aliveExplosions = 0;
|
|
132
|
+
for (let i = 0; i < this.#explosions.length; i++) {
|
|
133
|
+
if (!this.#explosions[i].isDead) {
|
|
134
|
+
this.#explosions[aliveExplosions++] = this.#explosions[i];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this.#explosions.length = aliveExplosions;
|
|
138
|
+
|
|
139
|
+
let aliveSparks = 0;
|
|
140
|
+
for (let i = 0; i < this.#sparks.length; i++) {
|
|
141
|
+
if (!this.#sparks[i].isDead) {
|
|
142
|
+
this.#sparks[aliveSparks++] = this.#sparks[i];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
this.#sparks.length = aliveSparks;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
149
|
+
if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
|
|
150
|
+
ctx.canvas.width = width;
|
|
151
|
+
ctx.canvas.height = height;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
155
|
+
|
|
156
|
+
for (const spark of this.#sparks) {
|
|
157
|
+
spark.draw(ctx);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const explosion of this.#explosions) {
|
|
161
|
+
explosion.draw(ctx);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const firework of this.#fireworks) {
|
|
165
|
+
firework.draw(ctx);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#spawnExplosion(position: Point, hue: number, variant?: FireworkVariant): void {
|
|
172
|
+
const selected = variant ?? this.#pickVariant();
|
|
173
|
+
const rng = () => MULBERRY.nextBetween(0, 1);
|
|
174
|
+
this.#explosions.push(...createExplosion(selected, position, hue, {lineWidth: this.#baseSize, scale: this.#scale}, rng));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#createFirework(position?: Point): void {
|
|
178
|
+
const hue = this.#hue;
|
|
179
|
+
const targetX = position?.x || this.#positionRandom.nextBetween(this.#width * .1, this.#width * .9);
|
|
180
|
+
const targetY = position?.y || this.#height * .1 + this.#positionRandom.nextBetween(0, this.#height * .5);
|
|
181
|
+
const startX = this.#width * 0.3 + this.#positionRandom.nextBetween(0, this.#width * 0.4);
|
|
182
|
+
|
|
183
|
+
const firework = new Firework(
|
|
184
|
+
{x: startX, y: this.#height},
|
|
185
|
+
{x: targetX, y: targetY},
|
|
186
|
+
hue,
|
|
187
|
+
this.#tailWidth,
|
|
188
|
+
this.#baseSize
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
firework.addEventListener('remove', () => {
|
|
192
|
+
this.#fireworks.splice(this.#fireworks.indexOf(firework), 1);
|
|
193
|
+
this.#spawnExplosion(firework.position, hue);
|
|
194
|
+
}, {once: true});
|
|
195
|
+
|
|
196
|
+
this.#fireworks.push(firework);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#pickVariant(): FireworkVariant {
|
|
200
|
+
const index = Math.floor(MULBERRY.nextBetween(0, this.#variants.length));
|
|
201
|
+
return this.#variants[index];
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/fireworks/spark.ts
CHANGED
|
@@ -8,7 +8,7 @@ export class Spark {
|
|
|
8
8
|
readonly #size: number;
|
|
9
9
|
readonly #decay: number;
|
|
10
10
|
readonly #friction: number = 0.94;
|
|
11
|
-
readonly #gravity: number = 0.
|
|
11
|
+
readonly #gravity: number = 0.3;
|
|
12
12
|
#alpha: number = 1;
|
|
13
13
|
|
|
14
14
|
get isDead(): boolean {
|
|
@@ -26,7 +26,7 @@ export class Spark {
|
|
|
26
26
|
this.#decay = MULBERRY.nextBetween(0.03, 0.08);
|
|
27
27
|
this.#velocity = {
|
|
28
28
|
x: velocityX + MULBERRY.nextBetween(-1.5, 1.5),
|
|
29
|
-
y: velocityY + MULBERRY.nextBetween(-2,
|
|
29
|
+
y: velocityY + MULBERRY.nextBetween(-2, 2)
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -37,14 +37,14 @@ export class Spark {
|
|
|
37
37
|
ctx.fill();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
tick(): void {
|
|
41
|
-
this.#velocity.x *= this.#friction;
|
|
42
|
-
this.#velocity.y *= this.#friction;
|
|
43
|
-
this.#velocity.y += this.#gravity;
|
|
40
|
+
tick(dt: number): void {
|
|
41
|
+
this.#velocity.x *= Math.pow(this.#friction, dt);
|
|
42
|
+
this.#velocity.y *= Math.pow(this.#friction, dt);
|
|
43
|
+
this.#velocity.y += this.#gravity * dt;
|
|
44
44
|
|
|
45
|
-
this.#position.x += this.#velocity.x;
|
|
46
|
-
this.#position.y += this.#velocity.y;
|
|
45
|
+
this.#position.x += this.#velocity.x * dt;
|
|
46
|
+
this.#position.y += this.#velocity.y * dt;
|
|
47
47
|
|
|
48
|
-
this.#alpha -= this.#decay;
|
|
48
|
+
this.#alpha -= this.#decay * dt;
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/fireworks/types.ts
CHANGED
|
@@ -21,10 +21,10 @@ export interface ExplosionConfig {
|
|
|
21
21
|
readonly glowSize: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export interface
|
|
24
|
+
export interface FireworksConfig {
|
|
25
25
|
readonly scale?: number;
|
|
26
26
|
readonly autoSpawn?: boolean;
|
|
27
|
-
readonly
|
|
27
|
+
readonly variants?: FireworkVariant[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export const FIREWORK_VARIANTS: FireworkVariant[] = [
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Mulberry32, mulberry32 } from '@basmilius/utils';
|
|
2
|
+
|
|
3
|
+
export const MULBERRY: Mulberry32 = mulberry32(13);
|
|
4
|
+
|
|
5
|
+
export const GLITTER_COLORS: string[] = [
|
|
6
|
+
'#ffd700', // gold
|
|
7
|
+
'#c0c0c0', // silver
|
|
8
|
+
'#ff69b4', // pink
|
|
9
|
+
'#00bfff', // sky blue
|
|
10
|
+
'#ff4500', // orange-red
|
|
11
|
+
'#7fff00', // chartreuse
|
|
12
|
+
'#9370db' // medium purple
|
|
13
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Glitter } from './layer';
|
|
2
|
+
import type { GlitterConfig } from './types';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createGlitter(config?: GlitterConfig): Effect<GlitterConfig> {
|
|
6
|
+
return new Glitter(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { GlitterConfig, FallingGlitter, SettledGlitter } from './types';
|