@basmilius/sparkle 1.0.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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1322 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
- package/src/canvas.ts +135 -0
- package/src/confetti/consts.ts +26 -0
- package/src/confetti/index.ts +2 -0
- package/src/confetti/simulation.ts +221 -0
- package/src/confetti/types.ts +53 -0
- package/src/distance.ts +8 -0
- package/src/fireworks/consts.ts +4 -0
- package/src/fireworks/explosion.ts +227 -0
- package/src/fireworks/firework.ts +123 -0
- package/src/fireworks/index.ts +3 -0
- package/src/fireworks/simulation.ts +493 -0
- package/src/fireworks/spark.ts +50 -0
- package/src/fireworks/types.ts +260 -0
- package/src/index.ts +4 -0
- package/src/point.ts +4 -0
- package/src/snow/consts.ts +3 -0
- package/src/snow/index.ts +2 -0
- package/src/snow/simulation.ts +301 -0
- package/src/snow/snowflake.ts +13 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type Config = {
|
|
2
|
+
readonly angle: number;
|
|
3
|
+
readonly colors: string[];
|
|
4
|
+
readonly decay: number;
|
|
5
|
+
readonly gravity: number;
|
|
6
|
+
readonly particles: number;
|
|
7
|
+
readonly shapes: Shape[];
|
|
8
|
+
readonly spread: number;
|
|
9
|
+
readonly ticks: number;
|
|
10
|
+
readonly startVelocity: number;
|
|
11
|
+
readonly x: number;
|
|
12
|
+
readonly y: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type Particle = {
|
|
16
|
+
colorStr: string;
|
|
17
|
+
decay: number;
|
|
18
|
+
flipAngle: number;
|
|
19
|
+
flipSpeed: number;
|
|
20
|
+
gravity: number;
|
|
21
|
+
rotAngle: number;
|
|
22
|
+
rotCos: number;
|
|
23
|
+
rotSin: number;
|
|
24
|
+
rotSpeed: number;
|
|
25
|
+
shape: Shape;
|
|
26
|
+
size: number;
|
|
27
|
+
swing: number;
|
|
28
|
+
swingAmp: number;
|
|
29
|
+
swingSpeed: number;
|
|
30
|
+
tick: number;
|
|
31
|
+
totalTicks: number;
|
|
32
|
+
vx: number;
|
|
33
|
+
vy: number;
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ParticleConfig = {
|
|
39
|
+
readonly angle: number;
|
|
40
|
+
readonly color: RGB;
|
|
41
|
+
readonly decay: number;
|
|
42
|
+
readonly gravity: number;
|
|
43
|
+
readonly shape: Shape;
|
|
44
|
+
readonly spread: number;
|
|
45
|
+
readonly startVelocity: number;
|
|
46
|
+
readonly ticks: number;
|
|
47
|
+
readonly x: number;
|
|
48
|
+
readonly y: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type RGB = [r: number, g: number, b: number];
|
|
52
|
+
|
|
53
|
+
export type Shape = 'circle' | 'diamond' | 'ribbon' | 'square' | 'star' | 'triangle';
|
package/src/distance.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { Point } from '../point';
|
|
2
|
+
import { MULBERRY } from './consts';
|
|
3
|
+
import { EXPLOSION_CONFIGS, type ExplosionConfig, type ExplosionType, type ParticleShape } from './types';
|
|
4
|
+
|
|
5
|
+
const PERSPECTIVE = 800;
|
|
6
|
+
|
|
7
|
+
export class Explosion {
|
|
8
|
+
readonly #position: Point;
|
|
9
|
+
readonly #angle: number;
|
|
10
|
+
readonly #brightness: number;
|
|
11
|
+
readonly #config: ExplosionConfig;
|
|
12
|
+
readonly #decay: number;
|
|
13
|
+
readonly #hue: number;
|
|
14
|
+
readonly #lineWidth: number;
|
|
15
|
+
readonly #shape: ParticleShape;
|
|
16
|
+
readonly #trail: Point[] = [];
|
|
17
|
+
readonly #type: ExplosionType;
|
|
18
|
+
#alpha: number = 1;
|
|
19
|
+
#depthScale: number = 1;
|
|
20
|
+
#hasCrackled: boolean = false;
|
|
21
|
+
#hasSplit: boolean = false;
|
|
22
|
+
#speed: number;
|
|
23
|
+
#sparkleTimer: number = 0;
|
|
24
|
+
#vz: number;
|
|
25
|
+
#z: number = 0;
|
|
26
|
+
|
|
27
|
+
get angle(): number {
|
|
28
|
+
return this.#angle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get hue(): number {
|
|
32
|
+
return this.#hue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get isDead(): boolean {
|
|
36
|
+
return this.#alpha <= 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get position(): Point {
|
|
40
|
+
return this.#position;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get type(): ExplosionType {
|
|
44
|
+
return this.#type;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
constructor(position: Point, hue: number, lineWidth: number, type: ExplosionType, scale: number = 1, angle?: number, speed?: number, vz?: number) {
|
|
48
|
+
const config = EXPLOSION_CONFIGS[type];
|
|
49
|
+
|
|
50
|
+
this.#config = config;
|
|
51
|
+
this.#type = type;
|
|
52
|
+
this.#shape = config.shape;
|
|
53
|
+
this.#position = {...position};
|
|
54
|
+
this.#alpha = 1;
|
|
55
|
+
this.#angle = angle ?? MULBERRY.nextBetween(0, Math.PI * 2);
|
|
56
|
+
this.#brightness = MULBERRY.nextBetween(config.brightness[0], config.brightness[1]);
|
|
57
|
+
this.#decay = MULBERRY.nextBetween(config.decay[0], config.decay[1]);
|
|
58
|
+
this.#hue = hue + MULBERRY.nextBetween(-config.hueVariation, config.hueVariation);
|
|
59
|
+
this.#lineWidth = lineWidth * config.lineWidthScale;
|
|
60
|
+
this.#speed = (speed ?? MULBERRY.nextBetween(config.speed[0], config.speed[1])) * scale;
|
|
61
|
+
|
|
62
|
+
if (vz !== undefined) {
|
|
63
|
+
this.#vz = vz * scale;
|
|
64
|
+
} else if (config.spread3d) {
|
|
65
|
+
this.#vz = MULBERRY.nextBetween(-this.#speed * 0.5, this.#speed * 0.5);
|
|
66
|
+
} else {
|
|
67
|
+
this.#vz = 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < config.trailMemory; i++) {
|
|
71
|
+
this.#trail.push({...position});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
checkCrackle(): boolean {
|
|
76
|
+
if (this.#type !== 'crackle' || this.#hasCrackled) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.#alpha <= this.#decay * 3) {
|
|
81
|
+
this.#hasCrackled = true;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
checkSplit(): boolean {
|
|
89
|
+
if (this.#type !== 'crossette' || this.#hasSplit) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.#alpha < 0.5) {
|
|
94
|
+
this.#hasSplit = true;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
draw(ctx: CanvasRenderingContext2D): void {
|
|
102
|
+
if (this.#config.strobe && this.#sparkleTimer % 6 < 3) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const ds = this.#depthScale;
|
|
107
|
+
const trailEnd = this.#trail[this.#trail.length - 1];
|
|
108
|
+
const effectiveWidth = this.#shape === 'line' ? this.#lineWidth * ds : this.#lineWidth * 0.4 * ds;
|
|
109
|
+
const effectiveAlpha = this.#alpha * Math.min(ds, 1.2);
|
|
110
|
+
|
|
111
|
+
ctx.save();
|
|
112
|
+
ctx.lineCap = 'round';
|
|
113
|
+
|
|
114
|
+
if (this.#trail.length > 2) {
|
|
115
|
+
for (let i = this.#trail.length - 1; i > 0; i--) {
|
|
116
|
+
const progress = i / this.#trail.length;
|
|
117
|
+
const alpha = (1 - progress) * effectiveAlpha * 0.5;
|
|
118
|
+
const width = effectiveWidth * (1 - progress * 0.4);
|
|
119
|
+
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.moveTo(this.#trail[i].x, this.#trail[i].y);
|
|
122
|
+
ctx.lineTo(this.#trail[i - 1].x, this.#trail[i - 1].y);
|
|
123
|
+
ctx.lineWidth = width;
|
|
124
|
+
ctx.strokeStyle = `hsla(${this.#hue}, 100%, ${this.#brightness * 0.7}%, ${alpha})`;
|
|
125
|
+
ctx.stroke();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ctx.beginPath();
|
|
130
|
+
ctx.moveTo(trailEnd.x, trailEnd.y);
|
|
131
|
+
ctx.lineTo(this.#position.x, this.#position.y);
|
|
132
|
+
ctx.lineWidth = effectiveWidth;
|
|
133
|
+
ctx.strokeStyle = `hsla(${this.#hue}, 100%, ${this.#brightness}%, ${effectiveAlpha})`;
|
|
134
|
+
ctx.stroke();
|
|
135
|
+
|
|
136
|
+
if (this.#shape !== 'line') {
|
|
137
|
+
this.#drawShape(ctx, ds, effectiveAlpha);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.#config.sparkle && this.#sparkleTimer % 4 < 2) {
|
|
141
|
+
ctx.beginPath();
|
|
142
|
+
ctx.arc(this.#position.x, this.#position.y, this.#lineWidth * 0.8 * ds, 0, Math.PI * 2);
|
|
143
|
+
ctx.fillStyle = `hsla(${this.#hue}, 30%, 95%, ${effectiveAlpha * 0.9})`;
|
|
144
|
+
ctx.fill();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
ctx.restore();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
tick(): void {
|
|
151
|
+
this.#trail.pop();
|
|
152
|
+
this.#trail.unshift({...this.#position});
|
|
153
|
+
|
|
154
|
+
this.#speed *= this.#config.friction;
|
|
155
|
+
this.#vz *= this.#config.friction;
|
|
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;
|
|
160
|
+
|
|
161
|
+
this.#depthScale = PERSPECTIVE / (PERSPECTIVE + this.#z);
|
|
162
|
+
|
|
163
|
+
this.#alpha -= this.#decay;
|
|
164
|
+
this.#sparkleTimer++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#drawShape(ctx: CanvasRenderingContext2D, ds: number, alpha: number): void {
|
|
168
|
+
const size = this.#lineWidth * 1.2 * ds;
|
|
169
|
+
const color = `hsla(${this.#hue}, 100%, ${this.#brightness}%, ${alpha})`;
|
|
170
|
+
|
|
171
|
+
switch (this.#shape) {
|
|
172
|
+
case 'circle':
|
|
173
|
+
ctx.beginPath();
|
|
174
|
+
ctx.arc(this.#position.x, this.#position.y, size * 0.5, 0, Math.PI * 2);
|
|
175
|
+
ctx.fillStyle = color;
|
|
176
|
+
ctx.fill();
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case 'star':
|
|
180
|
+
this.#drawStarPath(ctx, this.#position.x, this.#position.y, size * 0.7, 4, this.#sparkleTimer * 0.15);
|
|
181
|
+
ctx.fillStyle = `hsla(${this.#hue}, 60%, ${Math.min(this.#brightness + 10, 100)}%, ${alpha})`;
|
|
182
|
+
ctx.fill();
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case 'diamond':
|
|
186
|
+
this.#drawDiamondPath(ctx, this.#position.x, this.#position.y, size * 0.6, this.#angle);
|
|
187
|
+
ctx.fillStyle = color;
|
|
188
|
+
ctx.fill();
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#drawStarPath(ctx: CanvasRenderingContext2D, cx: number, cy: number, radius: number, points: number, rotation: number): void {
|
|
194
|
+
const innerRadius = radius * 0.4;
|
|
195
|
+
const totalPoints = points * 2;
|
|
196
|
+
|
|
197
|
+
ctx.beginPath();
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < totalPoints; i++) {
|
|
200
|
+
const r = i % 2 === 0 ? radius : innerRadius;
|
|
201
|
+
const angle = (i * Math.PI / points) + rotation;
|
|
202
|
+
const px = cx + Math.cos(angle) * r;
|
|
203
|
+
const py = cy + Math.sin(angle) * r;
|
|
204
|
+
|
|
205
|
+
if (i === 0) {
|
|
206
|
+
ctx.moveTo(px, py);
|
|
207
|
+
} else {
|
|
208
|
+
ctx.lineTo(px, py);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ctx.closePath();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#drawDiamondPath(ctx: CanvasRenderingContext2D, cx: number, cy: number, size: number, rotation: number): void {
|
|
216
|
+
const cos = Math.cos(rotation);
|
|
217
|
+
const sin = Math.sin(rotation);
|
|
218
|
+
const hw = size * 0.5;
|
|
219
|
+
|
|
220
|
+
ctx.beginPath();
|
|
221
|
+
ctx.moveTo(cx + cos * size - sin * 0, cy + sin * size + cos * 0);
|
|
222
|
+
ctx.lineTo(cx + cos * 0 - sin * hw, cy + sin * 0 + cos * hw);
|
|
223
|
+
ctx.lineTo(cx + cos * -size - sin * 0, cy + sin * -size + cos * 0);
|
|
224
|
+
ctx.lineTo(cx + cos * 0 - sin * -hw, cy + sin * 0 + cos * -hw);
|
|
225
|
+
ctx.closePath();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { distance } from '../distance';
|
|
2
|
+
import type { Point } from '../point';
|
|
3
|
+
import { FIREWORK_TRAIL_MEMORY, MULBERRY } from './consts';
|
|
4
|
+
import { Spark } from './spark';
|
|
5
|
+
|
|
6
|
+
export class Firework extends EventTarget {
|
|
7
|
+
readonly #position: Point;
|
|
8
|
+
readonly #startPosition: Point;
|
|
9
|
+
readonly #acceleration: number = 1.05;
|
|
10
|
+
readonly #angle: number;
|
|
11
|
+
readonly #baseSize: number;
|
|
12
|
+
readonly #brightness: number = MULBERRY.nextBetween(55, 75);
|
|
13
|
+
readonly #distance: number;
|
|
14
|
+
readonly #hue: number;
|
|
15
|
+
readonly #tailWidth: number;
|
|
16
|
+
readonly #trail: Point[] = [];
|
|
17
|
+
#distanceTraveled: number = 0;
|
|
18
|
+
#speed: number = 1;
|
|
19
|
+
#sparkTimer: number = 0;
|
|
20
|
+
#pendingSparks: Spark[] = [];
|
|
21
|
+
|
|
22
|
+
get position(): Point {
|
|
23
|
+
return this.#position;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get hue(): number {
|
|
27
|
+
return this.#hue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
constructor(start: Point, target: Point, hue: number, tailWidth: number, baseSize: number) {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
this.#hue = hue;
|
|
34
|
+
this.#tailWidth = tailWidth;
|
|
35
|
+
this.#baseSize = baseSize;
|
|
36
|
+
this.#position = {...start};
|
|
37
|
+
this.#startPosition = {...start};
|
|
38
|
+
this.#angle = Math.atan2(target.y - start.y, target.x - start.x);
|
|
39
|
+
this.#distance = distance(start, target);
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < FIREWORK_TRAIL_MEMORY; i++) {
|
|
42
|
+
this.#trail.push({...start});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
collectSparks(): Spark[] {
|
|
47
|
+
const sparks = this.#pendingSparks;
|
|
48
|
+
this.#pendingSparks = [];
|
|
49
|
+
return sparks;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
draw(ctx: CanvasRenderingContext2D): void {
|
|
53
|
+
ctx.save();
|
|
54
|
+
ctx.lineCap = 'round';
|
|
55
|
+
|
|
56
|
+
for (let i = this.#trail.length - 1; i > 0; i--) {
|
|
57
|
+
const progress = i / this.#trail.length;
|
|
58
|
+
const alpha = (1 - progress) * 0.8;
|
|
59
|
+
const width = this.#tailWidth * (1 - progress * 0.5);
|
|
60
|
+
const hue = this.#hue + MULBERRY.nextBetween(-15, 15);
|
|
61
|
+
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
ctx.moveTo(this.#trail[i].x, this.#trail[i].y);
|
|
64
|
+
ctx.lineTo(this.#trail[i - 1].x, this.#trail[i - 1].y);
|
|
65
|
+
ctx.lineWidth = width;
|
|
66
|
+
ctx.strokeStyle = `hsla(${hue}, 100%, ${this.#brightness}%, ${alpha})`;
|
|
67
|
+
ctx.stroke();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ctx.shadowBlur = this.#baseSize * 4;
|
|
71
|
+
ctx.shadowColor = `hsl(${this.#hue}, 100%, 60%)`;
|
|
72
|
+
|
|
73
|
+
ctx.beginPath();
|
|
74
|
+
ctx.moveTo(this.#trail[0].x, this.#trail[0].y);
|
|
75
|
+
ctx.lineTo(this.#position.x, this.#position.y);
|
|
76
|
+
ctx.lineWidth = this.#tailWidth;
|
|
77
|
+
ctx.strokeStyle = `hsl(${this.#hue}, 100%, ${this.#brightness}%)`;
|
|
78
|
+
ctx.stroke();
|
|
79
|
+
|
|
80
|
+
ctx.shadowBlur = this.#baseSize * 6;
|
|
81
|
+
ctx.shadowColor = `hsl(${this.#hue}, 80%, 80%)`;
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.arc(this.#position.x, this.#position.y, this.#tailWidth * 0.6, 0, Math.PI * 2);
|
|
84
|
+
ctx.fillStyle = `hsl(${this.#hue}, 20%, 92%)`;
|
|
85
|
+
ctx.fill();
|
|
86
|
+
|
|
87
|
+
ctx.restore();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
tick(): void {
|
|
91
|
+
this.#trail.pop();
|
|
92
|
+
this.#trail.unshift({...this.#position});
|
|
93
|
+
|
|
94
|
+
this.#speed *= this.#acceleration;
|
|
95
|
+
|
|
96
|
+
const vx = Math.cos(this.#angle) * this.#speed;
|
|
97
|
+
const vy = Math.sin(this.#angle) * this.#speed;
|
|
98
|
+
|
|
99
|
+
this.#distanceTraveled = distance(this.#startPosition, {
|
|
100
|
+
x: this.#position.x + vx,
|
|
101
|
+
y: this.#position.y + vy
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (this.#distanceTraveled >= this.#distance) {
|
|
105
|
+
this.dispatchEvent(new CustomEvent('remove'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.#position.x += vx;
|
|
110
|
+
this.#position.y += vy;
|
|
111
|
+
|
|
112
|
+
this.#sparkTimer++;
|
|
113
|
+
|
|
114
|
+
if (this.#sparkTimer % 3 === 0) {
|
|
115
|
+
this.#pendingSparks.push(new Spark(
|
|
116
|
+
this.#position,
|
|
117
|
+
this.#hue,
|
|
118
|
+
-vx * 0.1,
|
|
119
|
+
-vy * 0.1
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|