@basmilius/sparkle 2.2.0 → 2.3.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 +12 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +158 -107
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/canvas.ts +80 -2
- package/src/confetti/shapes.ts +84 -97
- package/src/effect.ts +2 -2
- package/src/scene.ts +9 -6
- package/src/simulation-canvas.ts +9 -6
package/package.json
CHANGED
package/src/canvas.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export class LimitedFrameRateCanvas {
|
|
2
2
|
static #globalSpeed: number = 1;
|
|
3
|
+
static #globalFrameRate: number | null = null;
|
|
4
|
+
static #showFps: boolean = false;
|
|
3
5
|
|
|
4
6
|
static get globalSpeed(): number {
|
|
5
7
|
return LimitedFrameRateCanvas.#globalSpeed;
|
|
@@ -9,6 +11,28 @@ export class LimitedFrameRateCanvas {
|
|
|
9
11
|
LimitedFrameRateCanvas.#globalSpeed = value;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Global frame rate override for all canvas instances.
|
|
16
|
+
* null = use each instance's own frame rate.
|
|
17
|
+
* 0 = unlimited (render as fast as the browser allows).
|
|
18
|
+
* Any positive number = cap at that many frames per second.
|
|
19
|
+
*/
|
|
20
|
+
static get globalFrameRate(): number | null {
|
|
21
|
+
return LimitedFrameRateCanvas.#globalFrameRate;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static set globalFrameRate(value: number | null) {
|
|
25
|
+
LimitedFrameRateCanvas.#globalFrameRate = value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static get showFps(): boolean {
|
|
29
|
+
return LimitedFrameRateCanvas.#showFps;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static set showFps(value: boolean) {
|
|
33
|
+
LimitedFrameRateCanvas.#showFps = value;
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
readonly #canvas: HTMLCanvasElement;
|
|
13
37
|
readonly #context: CanvasRenderingContext2D;
|
|
14
38
|
readonly #frameRate: number;
|
|
@@ -23,6 +47,9 @@ export class LimitedFrameRateCanvas {
|
|
|
23
47
|
#isStopped: boolean = true;
|
|
24
48
|
#height: number = 540;
|
|
25
49
|
#width: number = 960;
|
|
50
|
+
#fps: string = '0.0';
|
|
51
|
+
#fpsFrames: number = 0;
|
|
52
|
+
#fpsTime: number = 0;
|
|
26
53
|
|
|
27
54
|
get canvas(): HTMLCanvasElement {
|
|
28
55
|
return this.#canvas;
|
|
@@ -48,6 +75,10 @@ export class LimitedFrameRateCanvas {
|
|
|
48
75
|
this.#speed = value;
|
|
49
76
|
}
|
|
50
77
|
|
|
78
|
+
get dpr(): number {
|
|
79
|
+
return devicePixelRatio;
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
get frameRate(): number {
|
|
52
83
|
return this.#frameRate;
|
|
53
84
|
}
|
|
@@ -76,7 +107,7 @@ export class LimitedFrameRateCanvas {
|
|
|
76
107
|
this.#canvas = canvas;
|
|
77
108
|
this.#context = canvas.getContext('2d', options);
|
|
78
109
|
this.#frameRate = frameRate;
|
|
79
|
-
this.#target = 1000 / frameRate;
|
|
110
|
+
this.#target = frameRate > 0 ? 1000 / frameRate : 0;
|
|
80
111
|
|
|
81
112
|
this.onVisibilityChange = this.onVisibilityChange.bind(this);
|
|
82
113
|
this.onResize = this.onResize.bind(this);
|
|
@@ -93,7 +124,10 @@ export class LimitedFrameRateCanvas {
|
|
|
93
124
|
this.#current = Date.now();
|
|
94
125
|
this.#frame = requestAnimationFrame(this.loop.bind(this));
|
|
95
126
|
|
|
96
|
-
|
|
127
|
+
const globalRate = LimitedFrameRateCanvas.#globalFrameRate;
|
|
128
|
+
const effectiveTarget = globalRate !== null ? (globalRate > 0 ? 1000 / globalRate : 0) : this.#target;
|
|
129
|
+
|
|
130
|
+
if (effectiveTarget > 0 && this.#then > 0 && this.#current - this.#then + 1 < effectiveTarget) {
|
|
97
131
|
return;
|
|
98
132
|
}
|
|
99
133
|
|
|
@@ -105,6 +139,24 @@ export class LimitedFrameRateCanvas {
|
|
|
105
139
|
this.tick();
|
|
106
140
|
this.draw();
|
|
107
141
|
|
|
142
|
+
if (LimitedFrameRateCanvas.#showFps) {
|
|
143
|
+
++this.#fpsFrames;
|
|
144
|
+
|
|
145
|
+
if (this.#fpsTime === 0) {
|
|
146
|
+
this.#fpsTime = this.#current;
|
|
147
|
+
} else {
|
|
148
|
+
const elapsed = this.#current - this.#fpsTime;
|
|
149
|
+
|
|
150
|
+
if (elapsed >= 1000) {
|
|
151
|
+
this.#fps = (Math.round(this.#fpsFrames * 10000 / elapsed) / 10).toFixed(1);
|
|
152
|
+
this.#fpsFrames = 0;
|
|
153
|
+
this.#fpsTime = this.#current;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.#drawFps();
|
|
158
|
+
}
|
|
159
|
+
|
|
108
160
|
this.#then = this.#now;
|
|
109
161
|
}
|
|
110
162
|
|
|
@@ -132,6 +184,32 @@ export class LimitedFrameRateCanvas {
|
|
|
132
184
|
}
|
|
133
185
|
}
|
|
134
186
|
|
|
187
|
+
#drawFps(): void {
|
|
188
|
+
const ctx = this.#context;
|
|
189
|
+
const text = `${this.#fps} FPS`;
|
|
190
|
+
const x = 9;
|
|
191
|
+
const y = 9;
|
|
192
|
+
const paddingX = 6;
|
|
193
|
+
const paddingY = 4;
|
|
194
|
+
|
|
195
|
+
ctx.save();
|
|
196
|
+
ctx.font = '700 10px ui-monospace, monospace';
|
|
197
|
+
|
|
198
|
+
const textWidth = ctx.measureText(text).width;
|
|
199
|
+
const boxWidth = textWidth + paddingX * 2;
|
|
200
|
+
const boxHeight = 11 + paddingY * 2;
|
|
201
|
+
|
|
202
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
|
|
203
|
+
ctx.beginPath();
|
|
204
|
+
ctx.roundRect(x, y, boxWidth, boxHeight, 3);
|
|
205
|
+
ctx.fill();
|
|
206
|
+
|
|
207
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
208
|
+
ctx.textBaseline = 'middle';
|
|
209
|
+
ctx.fillText(text, x + paddingX, y + boxHeight / 1.9);
|
|
210
|
+
ctx.restore();
|
|
211
|
+
}
|
|
212
|
+
|
|
135
213
|
draw(): void {
|
|
136
214
|
throw new Error('LimitedFrameRateCanvas::draw() should be overwritten.');
|
|
137
215
|
}
|
package/src/confetti/shapes.ts
CHANGED
|
@@ -2,103 +2,90 @@ import type { Shape } from './types';
|
|
|
2
2
|
|
|
3
3
|
const TWO_PI = Math.PI * 2;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
bowtie
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
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
|
-
}
|
|
5
|
+
function buildShapePaths(): Record<Shape, Path2D> {
|
|
6
|
+
const bowtie = new Path2D();
|
|
7
|
+
bowtie.moveTo(-1, -0.7);
|
|
8
|
+
bowtie.lineTo(0, 0);
|
|
9
|
+
bowtie.lineTo(-1, 0.7);
|
|
10
|
+
bowtie.closePath();
|
|
11
|
+
bowtie.moveTo(1, -0.7);
|
|
12
|
+
bowtie.lineTo(0, 0);
|
|
13
|
+
bowtie.lineTo(1, 0.7);
|
|
14
|
+
bowtie.closePath();
|
|
15
|
+
|
|
16
|
+
const circle = new Path2D();
|
|
17
|
+
circle.ellipse(0, 0, 0.6, 1, 0, 0, TWO_PI);
|
|
18
|
+
|
|
19
|
+
const crescent = new Path2D();
|
|
20
|
+
crescent.arc(0, 0, 1, 0, TWO_PI, false);
|
|
21
|
+
crescent.arc(0.45, 0, 0.9, 0, TWO_PI, true);
|
|
22
|
+
|
|
23
|
+
const diamond = new Path2D();
|
|
24
|
+
diamond.moveTo(0, -1);
|
|
25
|
+
diamond.lineTo(0.6, 0);
|
|
26
|
+
diamond.lineTo(0, 1);
|
|
27
|
+
diamond.lineTo(-0.6, 0);
|
|
28
|
+
diamond.closePath();
|
|
29
|
+
|
|
30
|
+
const heart = new Path2D();
|
|
31
|
+
heart.moveTo(0, 1);
|
|
32
|
+
heart.bezierCurveTo(-0.4, 0.55, -1, 0.1, -1, -0.35);
|
|
33
|
+
heart.bezierCurveTo(-1, -0.8, -0.5, -1, 0, -0.6);
|
|
34
|
+
heart.bezierCurveTo(0.5, -1, 1, -0.8, 1, -0.35);
|
|
35
|
+
heart.bezierCurveTo(1, 0.1, 0.4, 0.55, 0, 1);
|
|
36
|
+
heart.closePath();
|
|
37
|
+
|
|
38
|
+
const hexagon = new Path2D();
|
|
39
|
+
for (let i = 0; i < 6; i++) {
|
|
40
|
+
const angle = (i * Math.PI / 3) - Math.PI / 2;
|
|
41
|
+
if (i === 0) {
|
|
42
|
+
hexagon.moveTo(Math.cos(angle), Math.sin(angle));
|
|
43
|
+
} else {
|
|
44
|
+
hexagon.lineTo(Math.cos(angle), Math.sin(angle));
|
|
57
45
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
ribbon
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ring
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
}
|
|
46
|
+
}
|
|
47
|
+
hexagon.closePath();
|
|
48
|
+
|
|
49
|
+
const ribbon = new Path2D();
|
|
50
|
+
ribbon.rect(-0.2, -1, 0.4, 2);
|
|
51
|
+
|
|
52
|
+
const ring = new Path2D();
|
|
53
|
+
ring.arc(0, 0, 1, 0, TWO_PI, false);
|
|
54
|
+
ring.arc(0, 0, 0.55, 0, TWO_PI, true);
|
|
55
|
+
|
|
56
|
+
const square = new Path2D();
|
|
57
|
+
square.rect(-0.7, -0.7, 1.4, 1.4);
|
|
58
|
+
|
|
59
|
+
const star = new Path2D();
|
|
60
|
+
for (let i = 0; i < 10; i++) {
|
|
61
|
+
const r = i % 2 === 0 ? 1 : 0.42;
|
|
62
|
+
const angle = (i * Math.PI / 5) - Math.PI / 2;
|
|
63
|
+
if (i === 0) {
|
|
64
|
+
star.moveTo(r * Math.cos(angle), r * Math.sin(angle));
|
|
65
|
+
} else {
|
|
66
|
+
star.lineTo(r * Math.cos(angle), r * Math.sin(angle));
|
|
87
67
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
triangle
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
path.lineTo(Math.cos(angle), Math.sin(angle));
|
|
99
|
-
}
|
|
68
|
+
}
|
|
69
|
+
star.closePath();
|
|
70
|
+
|
|
71
|
+
const triangle = new Path2D();
|
|
72
|
+
for (let i = 0; i < 3; i++) {
|
|
73
|
+
const angle = (i * 2 * Math.PI / 3) - Math.PI / 2;
|
|
74
|
+
if (i === 0) {
|
|
75
|
+
triangle.moveTo(Math.cos(angle), Math.sin(angle));
|
|
76
|
+
} else {
|
|
77
|
+
triangle.lineTo(Math.cos(angle), Math.sin(angle));
|
|
100
78
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
};
|
|
79
|
+
}
|
|
80
|
+
triangle.closePath();
|
|
81
|
+
|
|
82
|
+
return {bowtie, circle, crescent, diamond, heart, hexagon, ribbon, ring, square, star, triangle};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let _shapePaths: Record<Shape, Path2D> | null = null;
|
|
86
|
+
|
|
87
|
+
export const SHAPE_PATHS: Record<Shape, Path2D> = new Proxy({} as Record<Shape, Path2D>, {
|
|
88
|
+
get(_, key: string) {
|
|
89
|
+
return (_shapePaths ??= buildShapePaths())[key as Shape];
|
|
90
|
+
}
|
|
91
|
+
});
|
package/src/effect.ts
CHANGED
|
@@ -50,7 +50,7 @@ export abstract class Effect<TConfig = Record<string, unknown>> implements Simul
|
|
|
50
50
|
* Mount this effect to a canvas element or CSS selector, creating the render loop.
|
|
51
51
|
* Must be called before start().
|
|
52
52
|
*/
|
|
53
|
-
mount(canvas: HTMLCanvasElement | string, options: CanvasRenderingContext2DSettings = {colorSpace: 'display-p3'}): this {
|
|
53
|
+
mount(canvas: HTMLCanvasElement | string, options: CanvasRenderingContext2DSettings = {colorSpace: 'display-p3'}, frameRate: number = 60): this {
|
|
54
54
|
if (typeof canvas === 'string') {
|
|
55
55
|
const el = document.querySelector<HTMLCanvasElement>(canvas);
|
|
56
56
|
|
|
@@ -61,7 +61,7 @@ export abstract class Effect<TConfig = Record<string, unknown>> implements Simul
|
|
|
61
61
|
canvas = el;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
this.#canvas = new SimulationCanvas(canvas, this as unknown as SimulationLayer,
|
|
64
|
+
this.#canvas = new SimulationCanvas(canvas, this as unknown as SimulationLayer, frameRate, options);
|
|
65
65
|
return this;
|
|
66
66
|
}
|
|
67
67
|
|
package/src/scene.ts
CHANGED
|
@@ -38,19 +38,22 @@ class SceneCanvas extends LimitedFrameRateCanvas {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
draw(): void {
|
|
41
|
-
|
|
42
|
-
this.canvas.
|
|
41
|
+
const dpr = this.dpr;
|
|
42
|
+
this.canvas.height = this.height * dpr;
|
|
43
|
+
this.canvas.width = this.width * dpr;
|
|
43
44
|
|
|
44
45
|
const ctx = this.context;
|
|
46
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
45
47
|
ctx.clearRect(0, 0, this.width, this.height);
|
|
46
48
|
|
|
47
49
|
for (const layer of this.#layers) {
|
|
48
50
|
if (layer.fade) {
|
|
49
|
-
const offCtx = this.#getOffscreenCtx(this.width, this.height);
|
|
51
|
+
const offCtx = this.#getOffscreenCtx(this.width * dpr, this.height * dpr);
|
|
52
|
+
offCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
50
53
|
offCtx.clearRect(0, 0, this.width, this.height);
|
|
51
54
|
layer.draw(offCtx, this.width, this.height);
|
|
52
55
|
applyEdgeFade(offCtx, this.width, this.height, layer.fade);
|
|
53
|
-
ctx.drawImage(this.#offscreen!, 0, 0);
|
|
56
|
+
ctx.drawImage(this.#offscreen!, 0, 0, this.width, this.height);
|
|
54
57
|
} else {
|
|
55
58
|
ctx.save();
|
|
56
59
|
layer.draw(ctx, this.width, this.height);
|
|
@@ -71,8 +74,8 @@ class SceneCanvas extends LimitedFrameRateCanvas {
|
|
|
71
74
|
super.onResize();
|
|
72
75
|
|
|
73
76
|
if (this.#offscreen) {
|
|
74
|
-
this.#offscreen.width = this.width;
|
|
75
|
-
this.#offscreen.height = this.height;
|
|
77
|
+
this.#offscreen.width = this.width * this.dpr;
|
|
78
|
+
this.#offscreen.height = this.height * this.dpr;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
for (const layer of this.#layers) {
|
package/src/simulation-canvas.ts
CHANGED
|
@@ -36,17 +36,20 @@ export class SimulationCanvas extends LimitedFrameRateCanvas {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
draw(): void {
|
|
39
|
-
|
|
40
|
-
this.canvas.
|
|
39
|
+
const dpr = this.dpr;
|
|
40
|
+
this.canvas.height = this.height * dpr;
|
|
41
|
+
this.canvas.width = this.width * dpr;
|
|
41
42
|
|
|
42
43
|
const ctx = this.context;
|
|
44
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
43
45
|
|
|
44
46
|
if (this.#simulation.fade) {
|
|
45
|
-
const offCtx = this.#getOffscreenCtx(this.width, this.height);
|
|
47
|
+
const offCtx = this.#getOffscreenCtx(this.width * dpr, this.height * dpr);
|
|
48
|
+
offCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
46
49
|
offCtx.clearRect(0, 0, this.width, this.height);
|
|
47
50
|
this.#simulation.draw(offCtx, this.width, this.height);
|
|
48
51
|
applyEdgeFade(offCtx, this.width, this.height, this.#simulation.fade);
|
|
49
|
-
ctx.drawImage(this.#offscreen!, 0, 0);
|
|
52
|
+
ctx.drawImage(this.#offscreen!, 0, 0, this.width, this.height);
|
|
50
53
|
} else {
|
|
51
54
|
ctx.save();
|
|
52
55
|
this.#simulation.draw(ctx, this.width, this.height);
|
|
@@ -63,8 +66,8 @@ export class SimulationCanvas extends LimitedFrameRateCanvas {
|
|
|
63
66
|
super.onResize();
|
|
64
67
|
|
|
65
68
|
if (this.#offscreen) {
|
|
66
|
-
this.#offscreen.width = this.width;
|
|
67
|
-
this.#offscreen.height = this.height;
|
|
69
|
+
this.#offscreen.width = this.width * this.dpr;
|
|
70
|
+
this.#offscreen.height = this.height * this.dpr;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
this.#simulation.onResize(this.width, this.height);
|