@basmilius/sparkle 2.1.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 +317 -459
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1258 -949
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
- package/src/aurora/index.ts +9 -3
- package/src/aurora/layer.ts +57 -29
- package/src/balloons/index.ts +9 -3
- package/src/balloons/layer.ts +50 -19
- package/src/bubbles/index.ts +9 -3
- package/src/bubbles/layer.ts +30 -17
- package/src/canvas.ts +92 -2
- package/src/color.ts +11 -2
- package/src/confetti/index.ts +15 -3
- package/src/confetti/layer.ts +8 -5
- package/src/confetti/particle.ts +12 -11
- package/src/confetti/shapes.ts +84 -97
- package/src/donuts/consts.ts +2 -2
- package/src/donuts/index.ts +9 -3
- package/src/donuts/layer.ts +43 -12
- package/src/effect.ts +107 -0
- package/src/fade.ts +87 -0
- package/src/fireflies/index.ts +9 -3
- package/src/fireflies/layer.ts +26 -9
- package/src/fireflies/particle.ts +2 -2
- package/src/firepit/index.ts +9 -3
- package/src/firepit/layer.ts +26 -7
- package/src/fireworks/create-explosion.ts +237 -0
- package/src/fireworks/explosion.ts +1 -1
- package/src/fireworks/index.ts +15 -3
- package/src/fireworks/layer.ts +55 -304
- package/src/fireworks/spark.ts +2 -2
- package/src/fireworks/types.ts +2 -2
- package/src/glitter/index.ts +9 -4
- package/src/glitter/layer.ts +15 -7
- package/src/glitter/types.ts +10 -0
- package/src/index.ts +3 -4
- package/src/lanterns/index.ts +9 -4
- package/src/lanterns/layer.ts +22 -10
- package/src/lanterns/types.ts +8 -0
- package/src/layer.ts +13 -11
- package/src/leaves/index.ts +9 -4
- package/src/leaves/layer.ts +21 -14
- package/src/leaves/types.ts +9 -0
- package/src/lightning/index.ts +9 -4
- package/src/lightning/layer.ts +4 -4
- package/src/lightning/system.ts +3 -3
- package/src/lightning/types.ts +10 -2
- package/src/matrix/index.ts +9 -4
- package/src/matrix/layer.ts +15 -7
- package/src/matrix/types.ts +9 -0
- package/src/orbits/index.ts +9 -4
- package/src/orbits/layer.ts +51 -21
- package/src/orbits/types.ts +12 -1
- package/src/particles/index.ts +9 -3
- package/src/particles/layer.ts +55 -12
- package/src/petals/index.ts +9 -3
- package/src/petals/layer.ts +29 -13
- package/src/plasma/index.ts +9 -3
- package/src/plasma/layer.ts +21 -6
- package/src/rain/index.ts +9 -3
- package/src/rain/layer.ts +30 -8
- package/src/sandstorm/index.ts +9 -3
- package/src/sandstorm/layer.ts +26 -9
- package/src/scene.ts +204 -0
- package/src/shooting-stars/system.ts +26 -24
- package/src/shooting-stars/types.ts +2 -1
- package/src/simulation-canvas.ts +45 -6
- package/src/snow/index.ts +9 -3
- package/src/snow/layer.ts +24 -11
- package/src/sparklers/index.ts +13 -3
- package/src/sparklers/layer.ts +61 -15
- package/src/stars/index.ts +9 -3
- package/src/stars/layer.ts +28 -22
- package/src/streamers/index.ts +9 -3
- package/src/streamers/layer.ts +18 -6
- package/src/streamers/types.ts +1 -1
- package/src/waves/index.ts +9 -3
- package/src/waves/layer.ts +42 -45
- package/src/waves/types.ts +1 -0
- package/src/wormhole/index.ts +9 -3
- package/src/wormhole/layer.ts +22 -6
- package/src/aurora/simulation.ts +0 -19
- package/src/balloons/simulation.ts +0 -19
- package/src/bubbles/simulation.ts +0 -20
- package/src/confetti/simulation.ts +0 -27
- package/src/donuts/simulation.ts +0 -25
- package/src/fireflies/simulation.ts +0 -18
- package/src/firepit/simulation.ts +0 -17
- package/src/fireworks/simulation.ts +0 -18
- package/src/glitter/simulation.ts +0 -19
- package/src/lanterns/simulation.ts +0 -17
- package/src/layered.ts +0 -185
- package/src/leaves/simulation.ts +0 -18
- package/src/lightning/simulation.ts +0 -17
- package/src/matrix/simulation.ts +0 -18
- package/src/orbits/simulation.ts +0 -19
- package/src/particles/simulation.ts +0 -26
- package/src/petals/simulation.ts +0 -18
- package/src/plasma/simulation.ts +0 -17
- package/src/rain/simulation.ts +0 -21
- package/src/sandstorm/simulation.ts +0 -18
- package/src/snow/simulation.ts +0 -17
- package/src/sparklers/simulation.ts +0 -30
- package/src/stars/simulation.ts +0 -22
- package/src/streamers/simulation.ts +0 -16
- package/src/waves/simulation.ts +0 -18
- package/src/wormhole/simulation.ts +0 -17
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basmilius/sparkle",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "bas@mili.us",
|
|
7
7
|
"name": "Bas Milius",
|
|
8
8
|
"url": "https://bas.dev"
|
|
9
9
|
},
|
|
10
|
+
"homepage": "https://sparkle.graphics",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
13
|
"url": "https://github.com/basmilius/sparkle.git"
|
|
@@ -44,11 +45,14 @@
|
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/node": "^25.5.0",
|
|
48
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
47
49
|
"tsdown": "^0.21.4",
|
|
48
50
|
"typescript": "^6.0.2",
|
|
49
51
|
"vite": "^8.0.3",
|
|
50
52
|
"vitepress": "^1.6.3",
|
|
51
53
|
"vitepress-plugin-example": "^1.4.0",
|
|
52
|
-
"vitepress-plugin-render": "^1.4.0"
|
|
54
|
+
"vitepress-plugin-render": "^1.4.0",
|
|
55
|
+
"vue": "^3.5.13",
|
|
56
|
+
"vue-router": "^4.5.0"
|
|
53
57
|
}
|
|
54
58
|
}
|
package/src/aurora/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Aurora } from './layer';
|
|
2
|
+
import type { AuroraConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createAurora(config?: AuroraConfig): Effect<AuroraConfig> {
|
|
6
|
+
return new Aurora(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { AuroraConfig };
|
|
4
10
|
export type { AuroraBand } from './types';
|
package/src/aurora/layer.ts
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
3
|
import { MULBERRY } from './consts';
|
|
4
|
-
import type { AuroraSimulationConfig } from './simulation';
|
|
5
4
|
import type { AuroraBand } from './types';
|
|
6
5
|
|
|
7
6
|
const DEFAULT_COLORS = ['#9922ff', '#4455ff', '#0077ee', '#00aabb', '#22ddff'];
|
|
8
7
|
const TOP_HUE = 265;
|
|
9
8
|
|
|
10
|
-
export
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
|
|
9
|
+
export interface AuroraConfig {
|
|
10
|
+
readonly bands?: number;
|
|
11
|
+
readonly colors?: string[];
|
|
12
|
+
readonly speed?: number;
|
|
13
|
+
readonly intensity?: number;
|
|
14
|
+
readonly waveAmplitude?: number;
|
|
15
|
+
readonly verticalPosition?: number;
|
|
16
|
+
readonly scale?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Aurora extends Effect<AuroraConfig> {
|
|
20
|
+
#speed: number;
|
|
21
|
+
#intensity: number;
|
|
22
|
+
#waveAmplitude: number;
|
|
23
|
+
#verticalPosition: number;
|
|
16
24
|
#bands: AuroraBand[] = [];
|
|
17
25
|
|
|
18
|
-
constructor(config:
|
|
26
|
+
constructor(config: AuroraConfig = {}) {
|
|
19
27
|
super();
|
|
20
28
|
|
|
21
29
|
const bandCount = config.bands ?? 5;
|
|
@@ -36,26 +44,39 @@ export class AuroraLayer extends SimulationLayer {
|
|
|
36
44
|
|
|
37
45
|
this.#bands.push({
|
|
38
46
|
x: cluster + (MULBERRY.next() - 0.5) * 0.22,
|
|
39
|
-
baseY:
|
|
47
|
+
baseY: (MULBERRY.next() - 0.5) * 0.08,
|
|
40
48
|
height: 0.5 + MULBERRY.next() * 0.3,
|
|
41
49
|
sigma: 160 + MULBERRY.next() * 110,
|
|
42
50
|
phase1: MULBERRY.next() * Math.PI * 2,
|
|
43
51
|
phase2: MULBERRY.next() * Math.PI * 2,
|
|
44
52
|
amplitude1: 0.015 + MULBERRY.next() * 0.025,
|
|
45
53
|
frequency1: 0.003 + MULBERRY.next() * 0.004,
|
|
46
|
-
speed:
|
|
54
|
+
speed: 0.4 + MULBERRY.next() * 0.6,
|
|
47
55
|
hue,
|
|
48
|
-
opacity:
|
|
56
|
+
opacity: 0.5 + MULBERRY.next() * 0.3
|
|
49
57
|
});
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
configure(config: Partial<AuroraConfig>): void {
|
|
62
|
+
if (config.speed !== undefined) {
|
|
63
|
+
this.#speed = config.speed;
|
|
64
|
+
}
|
|
65
|
+
if (config.intensity !== undefined) {
|
|
66
|
+
this.#intensity = config.intensity;
|
|
67
|
+
}
|
|
68
|
+
if (config.waveAmplitude !== undefined) {
|
|
69
|
+
this.#waveAmplitude = config.waveAmplitude;
|
|
70
|
+
}
|
|
71
|
+
if (config.verticalPosition !== undefined) {
|
|
72
|
+
this.#verticalPosition = config.verticalPosition;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
55
75
|
|
|
76
|
+
tick(dt: number, _width: number, _height: number): void {
|
|
56
77
|
for (const band of this.#bands) {
|
|
57
|
-
band.phase1 += 0.005 * band.speed * dt;
|
|
58
|
-
band.phase2 += 0.008 * band.speed * dt;
|
|
78
|
+
band.phase1 += 0.005 * band.speed * this.#speed * dt;
|
|
79
|
+
band.phase2 += 0.008 * band.speed * this.#speed * dt;
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
|
|
@@ -77,7 +98,7 @@ export class AuroraLayer extends SimulationLayer {
|
|
|
77
98
|
for (const band of this.#bands) {
|
|
78
99
|
const swayX = band.amplitude1 * width * Math.sin(band.phase1);
|
|
79
100
|
const cx = band.x * width + swayX;
|
|
80
|
-
const baseY = band.baseY * height;
|
|
101
|
+
const baseY = (this.#verticalPosition + band.baseY) * height;
|
|
81
102
|
const rayHeight = band.height * height * (height / 800);
|
|
82
103
|
const sigma = band.sigma * scale;
|
|
83
104
|
const cutoff = sigma * 3.5;
|
|
@@ -88,6 +109,21 @@ export class AuroraLayer extends SimulationLayer {
|
|
|
88
109
|
const xStart = Math.max(0, Math.floor((cx - cutoff) / step) * step);
|
|
89
110
|
const xEnd = Math.min(width, Math.ceil((cx + cutoff) / step) * step);
|
|
90
111
|
|
|
112
|
+
const centreWave = Math.sin(band.frequency1 * cx + band.phase2) * waveRange;
|
|
113
|
+
const centreBase = baseY + centreWave;
|
|
114
|
+
const centreTop = centreBase - rayHeight;
|
|
115
|
+
const centreFadeBottom = centreBase + rayHeight * 0.1;
|
|
116
|
+
|
|
117
|
+
const gradient = ctx.createLinearGradient(0, centreFadeBottom, 0, centreTop);
|
|
118
|
+
gradient.addColorStop(0, `hsla(${band.hue}, 100%, 90%, 0)`);
|
|
119
|
+
gradient.addColorStop(0.04, `hsla(${band.hue}, 100%, 90%, 0.55)`);
|
|
120
|
+
gradient.addColorStop(0.1, `hsla(${band.hue}, 90%, 72%, 1)`);
|
|
121
|
+
gradient.addColorStop(0.32, `hsla(${band.hue}, 85%, 62%, 0.75)`);
|
|
122
|
+
gradient.addColorStop(0.62, `hsla(${midHue}, 80%, 56%, 0.35)`);
|
|
123
|
+
gradient.addColorStop(0.86, `hsla(${TOP_HUE}, 75%, 50%, 0.12)`);
|
|
124
|
+
gradient.addColorStop(1, `hsla(${TOP_HUE}, 70%, 45%, 0)`);
|
|
125
|
+
ctx.fillStyle = gradient;
|
|
126
|
+
|
|
91
127
|
for (let x = xStart; x < xEnd; x += step) {
|
|
92
128
|
const dx = x - cx;
|
|
93
129
|
const alpha = Math.exp(-dx * dx / sigmaSq2);
|
|
@@ -100,20 +136,12 @@ export class AuroraLayer extends SimulationLayer {
|
|
|
100
136
|
const colBase = baseY + waveOffset;
|
|
101
137
|
const colTop = colBase - rayHeight;
|
|
102
138
|
const fadeBottom = colBase + rayHeight * 0.1;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const gradient = ctx.createLinearGradient(0, fadeBottom, 0, colTop);
|
|
106
|
-
gradient.addColorStop(0, `hsla(${band.hue}, 100%, 90%, 0)`);
|
|
107
|
-
gradient.addColorStop(0.04, `hsla(${band.hue}, 100%, 90%, ${eff * 0.55})`);
|
|
108
|
-
gradient.addColorStop(0.1, `hsla(${band.hue}, 90%, 72%, ${eff})`);
|
|
109
|
-
gradient.addColorStop(0.32, `hsla(${band.hue}, 85%, 62%, ${eff * 0.75})`);
|
|
110
|
-
gradient.addColorStop(0.62, `hsla(${midHue}, 80%, 56%, ${eff * 0.35})`);
|
|
111
|
-
gradient.addColorStop(0.86, `hsla(${TOP_HUE}, 75%, 50%, ${eff * 0.12})`);
|
|
112
|
-
gradient.addColorStop(1, `hsla(${TOP_HUE}, 70%, 45%, 0)`);
|
|
113
|
-
|
|
114
|
-
ctx.fillStyle = gradient;
|
|
139
|
+
|
|
140
|
+
ctx.globalAlpha = alpha * band.opacity * this.#intensity;
|
|
115
141
|
ctx.fillRect(x, colTop, step, fadeBottom - colTop + 1);
|
|
116
142
|
}
|
|
143
|
+
|
|
144
|
+
ctx.globalAlpha = 1;
|
|
117
145
|
}
|
|
118
146
|
|
|
119
147
|
ctx.globalCompositeOperation = 'source-over';
|
package/src/balloons/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { Balloons } from './layer';
|
|
2
|
+
import type { BalloonsConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createBalloons(config?: BalloonsConfig): Effect<BalloonsConfig> {
|
|
6
|
+
return new Balloons(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
2
9
|
export { BalloonParticle } from './particle';
|
|
3
|
-
export {
|
|
10
|
+
export type { BalloonsConfig };
|
|
4
11
|
export type { BalloonParticleConfig } from './particle';
|
|
5
|
-
export type { BalloonSimulationConfig } from './simulation';
|
|
6
12
|
export type { Balloon } from './types';
|
package/src/balloons/layer.ts
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
3
|
import { MULBERRY } from './consts';
|
|
4
|
-
import type { BalloonSimulationConfig } from './simulation';
|
|
5
4
|
import type { Balloon } from './types';
|
|
6
5
|
|
|
7
6
|
const DEFAULT_COLORS = ['#ff4444', '#4488ff', '#44cc44', '#ffcc00', '#ff88cc', '#8844ff'];
|
|
8
7
|
|
|
9
|
-
export
|
|
8
|
+
export interface BalloonsConfig {
|
|
9
|
+
readonly count?: number;
|
|
10
|
+
readonly colors?: string[];
|
|
11
|
+
readonly sizeRange?: [number, number];
|
|
12
|
+
readonly speed?: number;
|
|
13
|
+
readonly driftAmount?: number;
|
|
14
|
+
readonly stringLength?: number;
|
|
15
|
+
readonly scale?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Balloons extends Effect<BalloonsConfig> {
|
|
10
19
|
readonly #scale: number;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
#speed: number;
|
|
21
|
+
#driftAmount: number;
|
|
22
|
+
#stringLengthMul: number;
|
|
14
23
|
readonly #sizeRange: [number, number];
|
|
15
24
|
readonly #colorRGBs: [number, number, number][];
|
|
16
25
|
#maxCount: number;
|
|
17
26
|
#time: number = 0;
|
|
18
27
|
#balloons: Balloon[] = [];
|
|
19
28
|
|
|
20
|
-
constructor(config:
|
|
29
|
+
constructor(config: BalloonsConfig = {}) {
|
|
21
30
|
super();
|
|
22
31
|
|
|
23
32
|
this.#scale = config.scale ?? 1;
|
|
@@ -39,6 +48,18 @@ export class BalloonLayer extends SimulationLayer {
|
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
|
|
51
|
+
configure(config: Partial<BalloonsConfig>): void {
|
|
52
|
+
if (config.speed !== undefined) {
|
|
53
|
+
this.#speed = config.speed;
|
|
54
|
+
}
|
|
55
|
+
if (config.driftAmount !== undefined) {
|
|
56
|
+
this.#driftAmount = config.driftAmount;
|
|
57
|
+
}
|
|
58
|
+
if (config.stringLength !== undefined) {
|
|
59
|
+
this.#stringLengthMul = config.stringLength;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
tick(dt: number, width: number, height: number): void {
|
|
43
64
|
this.#time += 0.015 * dt * this.#speed;
|
|
44
65
|
|
|
@@ -66,10 +87,10 @@ export class BalloonLayer extends SimulationLayer {
|
|
|
66
87
|
const rx = balloon.radiusX * this.#scale;
|
|
67
88
|
const ry = balloon.radiusY * this.#scale;
|
|
68
89
|
const [r, g, b] = balloon.color;
|
|
90
|
+
const cos = Math.cos(balloon.rotation);
|
|
91
|
+
const sin = Math.sin(balloon.rotation);
|
|
69
92
|
|
|
70
|
-
ctx.
|
|
71
|
-
ctx.translate(px, py);
|
|
72
|
-
ctx.rotate(balloon.rotation);
|
|
93
|
+
ctx.setTransform(cos, sin, -sin, cos, px, py);
|
|
73
94
|
|
|
74
95
|
const gradient = ctx.createRadialGradient(
|
|
75
96
|
-rx * 0.3, -ry * 0.3, rx * 0.1,
|
|
@@ -99,21 +120,31 @@ export class BalloonLayer extends SimulationLayer {
|
|
|
99
120
|
ctx.fill();
|
|
100
121
|
|
|
101
122
|
const stringLen = balloon.stringLength * this.#scale * this.#stringLengthMul;
|
|
102
|
-
const
|
|
123
|
+
const knotBaseY = knotY + 5 * this.#scale;
|
|
124
|
+
const ph = balloon.driftPhase;
|
|
125
|
+
const fr = balloon.driftFreq;
|
|
126
|
+
const swingAmt = 10 * this.#scale * this.#driftAmount;
|
|
127
|
+
|
|
128
|
+
// Each control point lags further behind the balloon's lateral oscillation,
|
|
129
|
+
// so the string trails the direction of movement like a real hanging string.
|
|
130
|
+
const midSwing = Math.sin(this.#time * fr + ph - 0.3) * swingAmt * 0.55;
|
|
131
|
+
const tipSwing = Math.sin(this.#time * fr + ph - 0.8) * swingAmt;
|
|
132
|
+
// Subtle high-frequency flutter at the tip for lightness.
|
|
133
|
+
const flutter = Math.sin(this.#time * fr * 2.5 + ph * 1.4 + 1.8) * 2.5 * this.#scale;
|
|
134
|
+
|
|
103
135
|
ctx.beginPath();
|
|
104
|
-
ctx.moveTo(0,
|
|
105
|
-
ctx.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
knotY + 5 * this.#scale + stringLen
|
|
136
|
+
ctx.moveTo(0, knotBaseY);
|
|
137
|
+
ctx.bezierCurveTo(
|
|
138
|
+
midSwing * 0.35, knotBaseY + stringLen * 0.3,
|
|
139
|
+
midSwing + flutter * 0.5, knotBaseY + stringLen * 0.65,
|
|
140
|
+
tipSwing + flutter, knotBaseY + stringLen
|
|
110
141
|
);
|
|
111
142
|
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.4)`;
|
|
112
143
|
ctx.lineWidth = 1;
|
|
113
144
|
ctx.stroke();
|
|
114
|
-
|
|
115
|
-
ctx.restore();
|
|
116
145
|
}
|
|
146
|
+
|
|
147
|
+
ctx.resetTransform();
|
|
117
148
|
}
|
|
118
149
|
|
|
119
150
|
#createBalloon(initialSpread: boolean): Balloon {
|
package/src/bubbles/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Bubbles } from './layer';
|
|
2
|
+
import type { BubblesConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createBubbles(config?: BubblesConfig): Effect<BubblesConfig> {
|
|
6
|
+
return new Bubbles(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { BubblesConfig };
|
|
4
10
|
export type { Bubble, PopParticle } from './types';
|
package/src/bubbles/layer.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseColor } from '../color';
|
|
2
|
+
import { Effect } from '../effect';
|
|
2
3
|
import { MULBERRY } from './consts';
|
|
3
|
-
import type { BubbleSimulationConfig } from './simulation';
|
|
4
4
|
import type { Bubble, PopParticle } from './types';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_COLORS = ['#88ccff', '#aaddff', '#ccbbff'];
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export interface BubblesConfig {
|
|
9
|
+
readonly count?: number;
|
|
10
|
+
readonly sizeRange?: [number, number];
|
|
11
|
+
readonly speed?: number;
|
|
12
|
+
readonly popOnClick?: boolean;
|
|
13
|
+
readonly popRadius?: number;
|
|
14
|
+
readonly colors?: string[];
|
|
15
|
+
readonly wobbleAmount?: number;
|
|
16
|
+
readonly scale?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Bubbles extends Effect<BubblesConfig> {
|
|
9
20
|
readonly #scale: number;
|
|
10
|
-
|
|
21
|
+
#speed: number;
|
|
11
22
|
readonly #sizeRange: [number, number];
|
|
12
|
-
|
|
23
|
+
#wobbleAmount: number;
|
|
13
24
|
readonly #popOnClick: boolean;
|
|
14
25
|
readonly #popRadius: number;
|
|
15
26
|
readonly #baseHues: number[];
|
|
@@ -20,7 +31,7 @@ export class BubbleLayer extends SimulationLayer {
|
|
|
20
31
|
#popParticles: PopParticle[] = [];
|
|
21
32
|
#canvas: HTMLCanvasElement | null = null;
|
|
22
33
|
|
|
23
|
-
constructor(config:
|
|
34
|
+
constructor(config: BubblesConfig = {}) {
|
|
24
35
|
super();
|
|
25
36
|
|
|
26
37
|
this.#scale = config.scale ?? 1;
|
|
@@ -58,6 +69,15 @@ export class BubbleLayer extends SimulationLayer {
|
|
|
58
69
|
this.#canvas = null;
|
|
59
70
|
}
|
|
60
71
|
|
|
72
|
+
configure(config: Partial<BubblesConfig>): void {
|
|
73
|
+
if (config.speed !== undefined) {
|
|
74
|
+
this.#speed = config.speed;
|
|
75
|
+
}
|
|
76
|
+
if (config.wobbleAmount !== undefined) {
|
|
77
|
+
this.#wobbleAmount = config.wobbleAmount;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
tick(dt: number, width: number, height: number): void {
|
|
62
82
|
this.#time += 0.01 * dt;
|
|
63
83
|
|
|
@@ -193,17 +213,10 @@ export class BubbleLayer extends SimulationLayer {
|
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
#colorToHue(color: string): number {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
ctx.fillStyle = color;
|
|
201
|
-
ctx.fillRect(0, 0, 1, 1);
|
|
202
|
-
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
203
|
-
|
|
204
|
-
let r = data[0] / 255;
|
|
205
|
-
let g = data[1] / 255;
|
|
206
|
-
let b = data[2] / 255;
|
|
216
|
+
const {r: r255, g: g255, b: b255} = parseColor(color);
|
|
217
|
+
let r = r255 / 255;
|
|
218
|
+
let g = g255 / 255;
|
|
219
|
+
let b = b255 / 255;
|
|
207
220
|
const max = Math.max(r, g, b);
|
|
208
221
|
const min = Math.min(r, g, b);
|
|
209
222
|
const delta = max - min;
|
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
|
|
|
@@ -120,6 +172,44 @@ export class LimitedFrameRateCanvas {
|
|
|
120
172
|
cancelAnimationFrame(this.#frame);
|
|
121
173
|
}
|
|
122
174
|
|
|
175
|
+
pause(): void {
|
|
176
|
+
this.#isStopped = true;
|
|
177
|
+
cancelAnimationFrame(this.#frame);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
resume(): void {
|
|
181
|
+
if (this.#isStopped) {
|
|
182
|
+
this.#isStopped = false;
|
|
183
|
+
this.#frame = requestAnimationFrame(this.loop.bind(this));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
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
|
+
|
|
123
213
|
draw(): void {
|
|
124
214
|
throw new Error('LimitedFrameRateCanvas::draw() should be overwritten.');
|
|
125
215
|
}
|
package/src/color.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
const cache = new Map<string, { r: number; g: number; b: number; a: number }>();
|
|
2
|
+
|
|
3
|
+
export function parseColor(fillStyle: string): { r: number; g: number; b: number; a: number } {
|
|
4
|
+
const cached = cache.get(fillStyle);
|
|
5
|
+
if (cached) {
|
|
6
|
+
return cached;
|
|
7
|
+
}
|
|
8
|
+
|
|
2
9
|
const canvas = document.createElement('canvas');
|
|
3
10
|
canvas.width = 1;
|
|
4
11
|
canvas.height = 1;
|
|
@@ -6,5 +13,7 @@ export function parseColor(fillStyle: string): {r: number; g: number; b: number;
|
|
|
6
13
|
ctx.fillStyle = fillStyle;
|
|
7
14
|
ctx.fillRect(0, 0, 1, 1);
|
|
8
15
|
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
9
|
-
|
|
16
|
+
const result = {r: data[0], g: data[1], b: data[2], a: data[3] / 255};
|
|
17
|
+
cache.set(fillStyle, result);
|
|
18
|
+
return result;
|
|
10
19
|
}
|
package/src/confetti/index.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import { Confetti } from './layer';
|
|
2
|
+
import type { ConfettiConfig } from './layer';
|
|
3
|
+
import type { Config as ConfettiBurstConfig } from './types';
|
|
4
|
+
import type { Effect } from '../effect';
|
|
5
|
+
|
|
6
|
+
export interface ConfettiInstance extends Effect<ConfettiConfig> {
|
|
7
|
+
burst(config: Partial<ConfettiBurstConfig>): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createConfetti(config?: ConfettiConfig): ConfettiInstance {
|
|
11
|
+
return new Confetti(config) as ConfettiInstance;
|
|
12
|
+
}
|
|
13
|
+
|
|
2
14
|
export { ConfettiParticle } from './particle';
|
|
3
|
-
export { ConfettiSimulation } from './simulation';
|
|
4
15
|
export { PALETTES } from './consts';
|
|
5
16
|
export { SHAPE_PATHS } from './shapes';
|
|
17
|
+
export type { ConfettiConfig };
|
|
18
|
+
export type { ConfettiBurstConfig };
|
|
6
19
|
export type { ConfettiParticleConfig } from './particle';
|
|
7
|
-
export type { ConfettiSimulationConfig } from './simulation';
|
|
8
20
|
export type { Palette, Shape as ConfettiShape } from './types';
|
package/src/confetti/layer.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
3
|
import { DEFAULT_CONFIG, MULBERRY, PALETTES } from './consts';
|
|
4
4
|
import { SHAPE_PATHS } from './shapes';
|
|
5
|
-
import type { ConfettiSimulationConfig } from './simulation';
|
|
6
5
|
import type { Config, Particle, ParticleConfig } from './types';
|
|
7
6
|
|
|
8
7
|
const TWO_PI = Math.PI * 2;
|
|
9
8
|
|
|
10
|
-
export
|
|
9
|
+
export interface ConfettiConfig {
|
|
10
|
+
readonly scale?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Confetti extends Effect<ConfettiConfig> {
|
|
11
14
|
readonly #scale: number;
|
|
12
15
|
#particles: Particle[] = [];
|
|
13
16
|
#width: number = 0;
|
|
14
17
|
#height: number = 0;
|
|
15
18
|
#isFiring: boolean = false;
|
|
16
19
|
|
|
17
|
-
constructor(config:
|
|
20
|
+
constructor(config: ConfettiConfig = {}) {
|
|
18
21
|
super();
|
|
19
22
|
this.#scale = config.scale ?? 1;
|
|
20
23
|
}
|
|
@@ -24,7 +27,7 @@ export class ConfettiLayer extends SimulationLayer {
|
|
|
24
27
|
this.#height = height;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
burst(config: Partial<Config>): void {
|
|
28
31
|
const width = this.#width;
|
|
29
32
|
const height = this.#height;
|
|
30
33
|
|