@basmilius/sparkle 2.0.0 → 2.1.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.
Files changed (148) hide show
  1. package/dist/index.d.mts +1192 -14
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +4552 -370
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +2 -1
  6. package/src/aurora/consts.ts +3 -0
  7. package/src/aurora/index.ts +4 -0
  8. package/src/aurora/layer.ts +152 -0
  9. package/src/aurora/simulation.ts +19 -0
  10. package/src/aurora/types.ts +13 -0
  11. package/src/balloons/consts.ts +3 -0
  12. package/src/balloons/index.ts +6 -0
  13. package/src/balloons/layer.ts +138 -0
  14. package/src/balloons/particle.ts +110 -0
  15. package/src/balloons/simulation.ts +19 -0
  16. package/src/balloons/types.ts +14 -0
  17. package/src/bubbles/consts.ts +3 -0
  18. package/src/bubbles/index.ts +4 -0
  19. package/src/bubbles/layer.ts +233 -0
  20. package/src/bubbles/simulation.ts +20 -0
  21. package/src/bubbles/types.ts +21 -0
  22. package/src/canvas.ts +20 -1
  23. package/src/color.ts +10 -0
  24. package/src/confetti/consts.ts +13 -13
  25. package/src/confetti/index.ts +6 -0
  26. package/src/confetti/layer.ts +152 -0
  27. package/src/confetti/particle.ts +105 -0
  28. package/src/confetti/shapes.ts +104 -0
  29. package/src/confetti/simulation.ts +9 -203
  30. package/src/confetti/types.ts +4 -1
  31. package/src/distance.ts +1 -1
  32. package/src/donuts/consts.ts +19 -0
  33. package/src/donuts/donut.ts +12 -0
  34. package/src/donuts/index.ts +3 -0
  35. package/src/donuts/layer.ts +270 -0
  36. package/src/donuts/simulation.ts +25 -0
  37. package/src/fireflies/consts.ts +3 -0
  38. package/src/fireflies/index.ts +6 -0
  39. package/src/fireflies/layer.ts +152 -0
  40. package/src/fireflies/particle.ts +124 -0
  41. package/src/fireflies/simulation.ts +18 -0
  42. package/src/fireflies/types.ts +17 -0
  43. package/src/firepit/consts.ts +3 -0
  44. package/src/firepit/index.ts +4 -0
  45. package/src/firepit/layer.ts +174 -0
  46. package/src/firepit/simulation.ts +17 -0
  47. package/src/firepit/types.ts +20 -0
  48. package/src/fireworks/explosion.ts +8 -8
  49. package/src/fireworks/firework.ts +9 -8
  50. package/src/fireworks/index.ts +6 -2
  51. package/src/fireworks/layer.ts +452 -0
  52. package/src/fireworks/simulation.ts +9 -484
  53. package/src/fireworks/spark.ts +7 -7
  54. package/src/glitter/consts.ts +13 -0
  55. package/src/glitter/index.ts +4 -0
  56. package/src/glitter/layer.ts +173 -0
  57. package/src/glitter/simulation.ts +19 -0
  58. package/src/glitter/types.ts +23 -0
  59. package/src/index.ts +28 -0
  60. package/src/lanterns/consts.ts +13 -0
  61. package/src/lanterns/index.ts +4 -0
  62. package/src/lanterns/layer.ts +166 -0
  63. package/src/lanterns/simulation.ts +17 -0
  64. package/src/lanterns/types.ts +14 -0
  65. package/src/layer.ts +24 -0
  66. package/src/layered.ts +185 -0
  67. package/src/leaves/consts.ts +16 -0
  68. package/src/leaves/index.ts +4 -0
  69. package/src/leaves/layer.ts +251 -0
  70. package/src/leaves/simulation.ts +18 -0
  71. package/src/leaves/types.ts +16 -0
  72. package/src/lightning/consts.ts +3 -0
  73. package/src/lightning/index.ts +6 -0
  74. package/src/lightning/layer.ts +41 -0
  75. package/src/lightning/simulation.ts +17 -0
  76. package/src/lightning/system.ts +196 -0
  77. package/src/lightning/types.ts +12 -0
  78. package/src/matrix/consts.ts +5 -0
  79. package/src/matrix/index.ts +4 -0
  80. package/src/matrix/layer.ts +146 -0
  81. package/src/matrix/simulation.ts +18 -0
  82. package/src/matrix/types.ts +8 -0
  83. package/src/orbits/consts.ts +13 -0
  84. package/src/orbits/index.ts +4 -0
  85. package/src/orbits/layer.ts +183 -0
  86. package/src/orbits/simulation.ts +19 -0
  87. package/src/orbits/types.ts +16 -0
  88. package/src/particles/consts.ts +3 -0
  89. package/src/particles/index.ts +4 -0
  90. package/src/particles/layer.ts +317 -0
  91. package/src/particles/simulation.ts +26 -0
  92. package/src/particles/types.ts +10 -0
  93. package/src/petals/consts.ts +13 -0
  94. package/src/petals/index.ts +4 -0
  95. package/src/petals/layer.ts +158 -0
  96. package/src/petals/simulation.ts +18 -0
  97. package/src/petals/types.ts +15 -0
  98. package/src/plasma/consts.ts +3 -0
  99. package/src/plasma/index.ts +4 -0
  100. package/src/plasma/layer.ts +92 -0
  101. package/src/plasma/simulation.ts +17 -0
  102. package/src/plasma/types.ts +5 -0
  103. package/src/rain/consts.ts +3 -0
  104. package/src/rain/index.ts +6 -0
  105. package/src/rain/layer.ts +172 -0
  106. package/src/rain/particle.ts +132 -0
  107. package/src/rain/simulation.ts +21 -0
  108. package/src/rain/types.ts +22 -0
  109. package/src/sandstorm/consts.ts +3 -0
  110. package/src/sandstorm/index.ts +4 -0
  111. package/src/sandstorm/layer.ts +135 -0
  112. package/src/sandstorm/simulation.ts +18 -0
  113. package/src/sandstorm/types.ts +10 -0
  114. package/src/shooting-stars/index.ts +3 -0
  115. package/src/shooting-stars/system.ts +149 -0
  116. package/src/shooting-stars/types.ts +10 -0
  117. package/src/simulation-canvas.ts +47 -0
  118. package/src/snow/consts.ts +2 -2
  119. package/src/snow/index.ts +1 -0
  120. package/src/snow/layer.ts +263 -0
  121. package/src/snow/simulation.ts +4 -288
  122. package/src/sparklers/consts.ts +3 -0
  123. package/src/sparklers/index.ts +6 -0
  124. package/src/sparklers/layer.ts +174 -0
  125. package/src/sparklers/particle.ts +89 -0
  126. package/src/sparklers/simulation.ts +30 -0
  127. package/src/sparklers/types.ts +13 -0
  128. package/src/stars/consts.ts +3 -0
  129. package/src/stars/index.ts +4 -0
  130. package/src/stars/layer.ts +133 -0
  131. package/src/stars/simulation.ts +22 -0
  132. package/src/stars/types.ts +12 -0
  133. package/src/streamers/consts.ts +14 -0
  134. package/src/streamers/index.ts +4 -0
  135. package/src/streamers/layer.ts +211 -0
  136. package/src/streamers/simulation.ts +16 -0
  137. package/src/streamers/types.ts +14 -0
  138. package/src/trail.ts +140 -0
  139. package/src/waves/consts.ts +3 -0
  140. package/src/waves/index.ts +4 -0
  141. package/src/waves/layer.ts +167 -0
  142. package/src/waves/simulation.ts +18 -0
  143. package/src/waves/types.ts +9 -0
  144. package/src/wormhole/consts.ts +3 -0
  145. package/src/wormhole/index.ts +4 -0
  146. package/src/wormhole/layer.ts +181 -0
  147. package/src/wormhole/simulation.ts +17 -0
  148. package/src/wormhole/types.ts +10 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@basmilius/sparkle",
3
3
  "license": "MIT",
4
- "version": "2.0.0",
4
+ "version": "2.1.0",
5
5
  "author": {
6
6
  "email": "bas@mili.us",
7
7
  "name": "Bas Milius",
@@ -43,6 +43,7 @@
43
43
  "@basmilius/utils": "^3.15.0"
44
44
  },
45
45
  "devDependencies": {
46
+ "@types/node": "^25.5.0",
46
47
  "tsdown": "^0.21.4",
47
48
  "typescript": "^6.0.2",
48
49
  "vite": "^8.0.3",
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,4 @@
1
+ export { AuroraLayer } from './layer';
2
+ export { AuroraSimulation } from './simulation';
3
+ export type { AuroraSimulationConfig } from './simulation';
4
+ export type { AuroraBand } from './types';
@@ -0,0 +1,152 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { SimulationLayer } from '../layer';
3
+ import { MULBERRY } from './consts';
4
+ import type { AuroraSimulationConfig } from './simulation';
5
+ import type { AuroraBand } from './types';
6
+
7
+ const DEFAULT_COLORS = ['#9922ff', '#4455ff', '#0077ee', '#00aabb', '#22ddff'];
8
+ const TOP_HUE = 265;
9
+
10
+ export class AuroraLayer extends SimulationLayer {
11
+ readonly #speed: number;
12
+ readonly #intensity: number;
13
+ readonly #waveAmplitude: number;
14
+ readonly #verticalPosition: number;
15
+ #time: number = 0;
16
+ #bands: AuroraBand[] = [];
17
+
18
+ constructor(config: AuroraSimulationConfig = {}) {
19
+ super();
20
+
21
+ const bandCount = config.bands ?? 5;
22
+ const colors = config.colors ?? DEFAULT_COLORS;
23
+ this.#speed = config.speed ?? 1;
24
+ this.#intensity = config.intensity ?? 0.8;
25
+ this.#waveAmplitude = config.waveAmplitude ?? 1;
26
+ this.#verticalPosition = config.verticalPosition ?? 0.68;
27
+
28
+ // Two loose clusters (left + right), rays within each cluster overlap into a whole
29
+ const clusterCenters = [0.35, 0.65];
30
+
31
+ for (let i = 0; i < bandCount; i++) {
32
+ const color = colors[i % colors.length];
33
+ const [r, g, b] = hexToRGB(color);
34
+ const hue = this.#rgbToHue(r, g, b);
35
+ const cluster = clusterCenters[i % clusterCenters.length];
36
+
37
+ this.#bands.push({
38
+ x: cluster + (MULBERRY.next() - 0.5) * 0.22,
39
+ baseY: this.#verticalPosition + (MULBERRY.next() - 0.5) * 0.08,
40
+ height: 0.5 + MULBERRY.next() * 0.3,
41
+ sigma: 160 + MULBERRY.next() * 110,
42
+ phase1: MULBERRY.next() * Math.PI * 2,
43
+ phase2: MULBERRY.next() * Math.PI * 2,
44
+ amplitude1: 0.015 + MULBERRY.next() * 0.025,
45
+ frequency1: 0.003 + MULBERRY.next() * 0.004,
46
+ speed: (0.4 + MULBERRY.next() * 0.6) * this.#speed,
47
+ hue,
48
+ opacity: (0.5 + MULBERRY.next() * 0.3) * this.#intensity
49
+ });
50
+ }
51
+ }
52
+
53
+ tick(dt: number, width: number, height: number): void {
54
+ this.#time += 0.016 * dt * this.#speed;
55
+
56
+ for (const band of this.#bands) {
57
+ band.phase1 += 0.005 * band.speed * dt;
58
+ band.phase2 += 0.008 * band.speed * dt;
59
+ }
60
+ }
61
+
62
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
63
+ // Dark sky background gradient
64
+ const bg = ctx.createLinearGradient(0, 0, 0, height);
65
+ bg.addColorStop(0, '#000000');
66
+ bg.addColorStop(0.5, '#050012');
67
+ bg.addColorStop(1, '#0a0025');
68
+ ctx.fillStyle = bg;
69
+ ctx.fillRect(0, 0, width, height);
70
+
71
+ // Aurora curtain rays — vertical bands with Gaussian horizontal falloff
72
+ ctx.globalCompositeOperation = 'screen';
73
+
74
+ const step = 4;
75
+ const scale = width / 1920;
76
+
77
+ for (const band of this.#bands) {
78
+ const swayX = band.amplitude1 * width * Math.sin(band.phase1);
79
+ const cx = band.x * width + swayX;
80
+ const baseY = band.baseY * height;
81
+ const rayHeight = band.height * height * (height / 800);
82
+ const sigma = band.sigma * scale;
83
+ const cutoff = sigma * 3.5;
84
+ const sigmaSq2 = 2 * sigma * sigma;
85
+ const midHue = (band.hue + TOP_HUE) / 2;
86
+ const waveRange = height * 0.035 * this.#waveAmplitude;
87
+
88
+ const xStart = Math.max(0, Math.floor((cx - cutoff) / step) * step);
89
+ const xEnd = Math.min(width, Math.ceil((cx + cutoff) / step) * step);
90
+
91
+ for (let x = xStart; x < xEnd; x += step) {
92
+ const dx = x - cx;
93
+ const alpha = Math.exp(-dx * dx / sigmaSq2);
94
+
95
+ if (alpha < 0.015) {
96
+ continue;
97
+ }
98
+
99
+ const waveOffset = Math.sin(band.frequency1 * x + band.phase2) * waveRange;
100
+ const colBase = baseY + waveOffset;
101
+ const colTop = colBase - rayHeight;
102
+ const fadeBottom = colBase + rayHeight * 0.1;
103
+ const eff = alpha * band.opacity;
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;
115
+ ctx.fillRect(x, colTop, step, fadeBottom - colTop + 1);
116
+ }
117
+ }
118
+
119
+ ctx.globalCompositeOperation = 'source-over';
120
+ }
121
+
122
+ #rgbToHue(r: number, g: number, b: number): number {
123
+ r /= 255;
124
+ g /= 255;
125
+ b /= 255;
126
+ const max = Math.max(r, g, b);
127
+ const min = Math.min(r, g, b);
128
+ const delta = max - min;
129
+
130
+ if (delta === 0) {
131
+ return 0;
132
+ }
133
+
134
+ let hue: number;
135
+
136
+ if (max === r) {
137
+ hue = ((g - b) / delta) % 6;
138
+ } else if (max === g) {
139
+ hue = (b - r) / delta + 2;
140
+ } else {
141
+ hue = (r - g) / delta + 4;
142
+ }
143
+
144
+ hue = Math.round(hue * 60);
145
+
146
+ if (hue < 0) {
147
+ hue += 360;
148
+ }
149
+
150
+ return hue;
151
+ }
152
+ }
@@ -0,0 +1,19 @@
1
+ import { SimulationCanvas } from '../simulation-canvas';
2
+ import { AuroraLayer } from './layer';
3
+
4
+ export interface AuroraSimulationConfig {
5
+ readonly bands?: number;
6
+ readonly colors?: string[];
7
+ readonly speed?: number;
8
+ readonly intensity?: number;
9
+ readonly waveAmplitude?: number;
10
+ readonly verticalPosition?: number;
11
+ readonly scale?: number;
12
+ readonly canvasOptions?: CanvasRenderingContext2DSettings;
13
+ }
14
+
15
+ export class AuroraSimulation extends SimulationCanvas {
16
+ constructor(canvas: HTMLCanvasElement, config: AuroraSimulationConfig = {}) {
17
+ super(canvas, new AuroraLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ export type AuroraBand = {
2
+ x: number;
3
+ baseY: number;
4
+ height: number;
5
+ sigma: number;
6
+ phase1: number;
7
+ phase2: number;
8
+ amplitude1: number;
9
+ frequency1: number;
10
+ speed: number;
11
+ hue: number;
12
+ opacity: number;
13
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,6 @@
1
+ export { BalloonLayer } from './layer';
2
+ export { BalloonParticle } from './particle';
3
+ export { BalloonSimulation } from './simulation';
4
+ export type { BalloonParticleConfig } from './particle';
5
+ export type { BalloonSimulationConfig } from './simulation';
6
+ export type { Balloon } from './types';
@@ -0,0 +1,138 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { SimulationLayer } from '../layer';
3
+ import { MULBERRY } from './consts';
4
+ import type { BalloonSimulationConfig } from './simulation';
5
+ import type { Balloon } from './types';
6
+
7
+ const DEFAULT_COLORS = ['#ff4444', '#4488ff', '#44cc44', '#ffcc00', '#ff88cc', '#8844ff'];
8
+
9
+ export class BalloonLayer extends SimulationLayer {
10
+ readonly #scale: number;
11
+ readonly #speed: number;
12
+ readonly #driftAmount: number;
13
+ readonly #stringLengthMul: number;
14
+ readonly #sizeRange: [number, number];
15
+ readonly #colorRGBs: [number, number, number][];
16
+ #maxCount: number;
17
+ #time: number = 0;
18
+ #balloons: Balloon[] = [];
19
+
20
+ constructor(config: BalloonSimulationConfig = {}) {
21
+ super();
22
+
23
+ this.#scale = config.scale ?? 1;
24
+ this.#maxCount = config.count ?? 15;
25
+ this.#sizeRange = config.sizeRange ?? [25, 45];
26
+ this.#speed = config.speed ?? 1;
27
+ this.#driftAmount = config.driftAmount ?? 1;
28
+ this.#stringLengthMul = config.stringLength ?? 1;
29
+
30
+ const colors = config.colors ?? DEFAULT_COLORS;
31
+ this.#colorRGBs = colors.map(c => hexToRGB(c));
32
+
33
+ if (innerWidth < 991) {
34
+ this.#maxCount = Math.floor(this.#maxCount / 2);
35
+ }
36
+
37
+ for (let i = 0; i < this.#maxCount; ++i) {
38
+ this.#balloons.push(this.#createBalloon(true));
39
+ }
40
+ }
41
+
42
+ tick(dt: number, width: number, height: number): void {
43
+ this.#time += 0.015 * dt * this.#speed;
44
+
45
+ for (let i = 0; i < this.#balloons.length; i++) {
46
+ const balloon = this.#balloons[i];
47
+
48
+ balloon.y -= (balloon.riseSpeed * this.#speed * dt) / (height * 1.2);
49
+
50
+ const drift = Math.sin(this.#time * balloon.driftFreq + balloon.driftPhase) * balloon.driftAmp * this.#driftAmount;
51
+ balloon.x += drift * dt / (width * 5);
52
+
53
+ balloon.rotation = Math.sin(this.#time * balloon.rotationSpeed + balloon.driftPhase) * 0.08;
54
+
55
+ if (balloon.y < -0.2) {
56
+ this.#balloons[i] = this.#createBalloon(false);
57
+ }
58
+ }
59
+ }
60
+
61
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
62
+
63
+ for (const balloon of this.#balloons) {
64
+ const px = balloon.x * width;
65
+ const py = balloon.y * height;
66
+ const rx = balloon.radiusX * this.#scale;
67
+ const ry = balloon.radiusY * this.#scale;
68
+ const [r, g, b] = balloon.color;
69
+
70
+ ctx.save();
71
+ ctx.translate(px, py);
72
+ ctx.rotate(balloon.rotation);
73
+
74
+ const gradient = ctx.createRadialGradient(
75
+ -rx * 0.3, -ry * 0.3, rx * 0.1,
76
+ 0, 0, Math.max(rx, ry)
77
+ );
78
+ gradient.addColorStop(0, `rgba(${Math.min(255, r + 80)}, ${Math.min(255, g + 80)}, ${Math.min(255, b + 80)}, 0.95)`);
79
+ gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.9)`);
80
+ gradient.addColorStop(1, `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, 0.85)`);
81
+
82
+ ctx.beginPath();
83
+ ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
84
+ ctx.fillStyle = gradient;
85
+ ctx.fill();
86
+
87
+ ctx.beginPath();
88
+ ctx.ellipse(-rx * 0.25, -ry * 0.3, rx * 0.2, ry * 0.15, -0.3, 0, Math.PI * 2);
89
+ ctx.fillStyle = `rgba(255, 255, 255, 0.35)`;
90
+ ctx.fill();
91
+
92
+ const knotY = ry + 2 * this.#scale;
93
+ ctx.beginPath();
94
+ ctx.moveTo(-3 * this.#scale, knotY);
95
+ ctx.lineTo(0, knotY + 5 * this.#scale);
96
+ ctx.lineTo(3 * this.#scale, knotY);
97
+ ctx.closePath();
98
+ ctx.fillStyle = `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 30)}, 0.9)`;
99
+ ctx.fill();
100
+
101
+ const stringLen = balloon.stringLength * this.#scale * this.#stringLengthMul;
102
+ const drift = Math.sin(this.#time * 2 + balloon.driftPhase) * 8 * this.#scale * this.#driftAmount;
103
+ ctx.beginPath();
104
+ ctx.moveTo(0, knotY + 5 * this.#scale);
105
+ ctx.quadraticCurveTo(
106
+ drift,
107
+ knotY + 5 * this.#scale + stringLen * 0.5,
108
+ -drift * 0.5,
109
+ knotY + 5 * this.#scale + stringLen
110
+ );
111
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.4)`;
112
+ ctx.lineWidth = 1;
113
+ ctx.stroke();
114
+
115
+ ctx.restore();
116
+ }
117
+ }
118
+
119
+ #createBalloon(initialSpread: boolean): Balloon {
120
+ const colorIndex = Math.floor(MULBERRY.next() * this.#colorRGBs.length);
121
+ const baseRadius = this.#sizeRange[0] + MULBERRY.next() * (this.#sizeRange[1] - this.#sizeRange[0]);
122
+
123
+ return {
124
+ x: 0.1 + MULBERRY.next() * 0.8,
125
+ y: initialSpread ? MULBERRY.next() * 1.2 : 1.2 + MULBERRY.next() * 0.2,
126
+ radiusX: baseRadius * 0.85,
127
+ radiusY: baseRadius,
128
+ color: this.#colorRGBs[colorIndex],
129
+ driftPhase: MULBERRY.next() * Math.PI * 2,
130
+ driftFreq: 0.5 + MULBERRY.next() * 1,
131
+ driftAmp: 0.3 + MULBERRY.next() * 0.7,
132
+ riseSpeed: 0.3 + MULBERRY.next() * 0.7,
133
+ rotation: 0,
134
+ rotationSpeed: 0.5 + MULBERRY.next() * 1.5,
135
+ stringLength: 30 + MULBERRY.next() * 40
136
+ };
137
+ }
138
+ }
@@ -0,0 +1,110 @@
1
+ import type { Point } from '../point';
2
+
3
+ export interface BalloonParticleConfig {
4
+ readonly driftAmp?: number;
5
+ readonly driftFreq?: number;
6
+ readonly driftPhase?: number;
7
+ readonly radiusX?: number;
8
+ readonly radiusY?: number;
9
+ readonly riseSpeed?: number;
10
+ readonly rotationSpeed?: number;
11
+ readonly scale?: number;
12
+ readonly stringLength?: number;
13
+ }
14
+
15
+ export class BalloonParticle {
16
+ readonly #color: [number, number, number];
17
+ readonly #driftAmp: number;
18
+ readonly #driftFreq: number;
19
+ readonly #driftPhase: number;
20
+ readonly #radiusX: number;
21
+ readonly #radiusY: number;
22
+ readonly #riseSpeed: number;
23
+ readonly #rotationSpeed: number;
24
+ readonly #scale: number;
25
+ readonly #stringLength: number;
26
+ #x: number;
27
+ #y: number;
28
+ #rotation: number = 0;
29
+ #time: number = 0;
30
+
31
+ get isDone(): boolean {
32
+ return this.#y < -(this.#radiusY * 2 + this.#stringLength);
33
+ }
34
+
35
+ get position(): Point {
36
+ return {x: this.#x, y: this.#y};
37
+ }
38
+
39
+ constructor(position: Point, color: [number, number, number], config: BalloonParticleConfig = {}) {
40
+ this.#x = position.x;
41
+ this.#y = position.y;
42
+ this.#color = color;
43
+ this.#driftAmp = config.driftAmp ?? (0.3 + Math.random() * 0.7);
44
+ this.#driftFreq = config.driftFreq ?? (0.5 + Math.random() * 1);
45
+ this.#driftPhase = config.driftPhase ?? (Math.random() * Math.PI * 2);
46
+ this.#radiusX = (config.radiusX ?? (25 + Math.random() * 20)) * (config.scale ?? 1);
47
+ this.#radiusY = (config.radiusY ?? (config.radiusX ? config.radiusX * (1 / 0.85) : 30 + Math.random() * 23)) * (config.scale ?? 1);
48
+ this.#riseSpeed = config.riseSpeed ?? (0.5 + Math.random() * 0.8);
49
+ this.#rotationSpeed = config.rotationSpeed ?? (0.5 + Math.random() * 1.5);
50
+ this.#scale = config.scale ?? 1;
51
+ this.#stringLength = (config.stringLength ?? (30 + Math.random() * 40)) * this.#scale;
52
+ }
53
+
54
+ draw(ctx: CanvasRenderingContext2D): void {
55
+ const [r, g, b] = this.#color;
56
+ const rx = this.#radiusX;
57
+ const ry = this.#radiusY;
58
+
59
+ ctx.save();
60
+ ctx.translate(this.#x, this.#y);
61
+ ctx.rotate(this.#rotation);
62
+
63
+ const gradient = ctx.createRadialGradient(-rx * 0.3, -ry * 0.3, rx * 0.1, 0, 0, Math.max(rx, ry));
64
+ gradient.addColorStop(0, `rgba(${Math.min(255, r + 80)}, ${Math.min(255, g + 80)}, ${Math.min(255, b + 80)}, 0.95)`);
65
+ gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.9)`);
66
+ gradient.addColorStop(1, `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, 0.85)`);
67
+
68
+ ctx.beginPath();
69
+ ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
70
+ ctx.fillStyle = gradient;
71
+ ctx.fill();
72
+
73
+ ctx.beginPath();
74
+ ctx.ellipse(-rx * 0.25, -ry * 0.3, rx * 0.2, ry * 0.15, -0.3, 0, Math.PI * 2);
75
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
76
+ ctx.fill();
77
+
78
+ const knotY = ry + 2 * this.#scale;
79
+ ctx.beginPath();
80
+ ctx.moveTo(-3 * this.#scale, knotY);
81
+ ctx.lineTo(0, knotY + 5 * this.#scale);
82
+ ctx.lineTo(3 * this.#scale, knotY);
83
+ ctx.closePath();
84
+ ctx.fillStyle = `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 30)}, 0.9)`;
85
+ ctx.fill();
86
+
87
+ const stringDrift = Math.sin(this.#time * 2 + this.#driftPhase) * 8 * this.#scale;
88
+ ctx.beginPath();
89
+ ctx.moveTo(0, knotY + 5 * this.#scale);
90
+ ctx.quadraticCurveTo(
91
+ stringDrift,
92
+ knotY + 5 * this.#scale + this.#stringLength * 0.5,
93
+ -stringDrift * 0.5,
94
+ knotY + 5 * this.#scale + this.#stringLength
95
+ );
96
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.4)`;
97
+ ctx.lineWidth = 1;
98
+ ctx.stroke();
99
+
100
+ ctx.restore();
101
+ }
102
+
103
+ tick(dt: number = 1): void {
104
+ this.#time += 0.015 * dt;
105
+
106
+ this.#y -= this.#riseSpeed * dt;
107
+ this.#x += Math.sin(this.#time * this.#driftFreq + this.#driftPhase) * this.#driftAmp * dt;
108
+ this.#rotation = Math.sin(this.#time * this.#rotationSpeed + this.#driftPhase) * 0.08;
109
+ }
110
+ }
@@ -0,0 +1,19 @@
1
+ import { SimulationCanvas } from '../simulation-canvas';
2
+ import { BalloonLayer } from './layer';
3
+
4
+ export interface BalloonSimulationConfig {
5
+ readonly count?: number;
6
+ readonly colors?: string[];
7
+ readonly sizeRange?: [number, number];
8
+ readonly speed?: number;
9
+ readonly driftAmount?: number;
10
+ readonly stringLength?: number;
11
+ readonly scale?: number;
12
+ readonly canvasOptions?: CanvasRenderingContext2DSettings;
13
+ }
14
+
15
+ export class BalloonSimulation extends SimulationCanvas {
16
+ constructor(canvas: HTMLCanvasElement, config: BalloonSimulationConfig = {}) {
17
+ super(canvas, new BalloonLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
18
+ }
19
+ }
@@ -0,0 +1,14 @@
1
+ export type Balloon = {
2
+ x: number;
3
+ y: number;
4
+ radiusX: number;
5
+ radiusY: number;
6
+ color: [number, number, number];
7
+ driftPhase: number;
8
+ driftFreq: number;
9
+ driftAmp: number;
10
+ riseSpeed: number;
11
+ rotation: number;
12
+ rotationSpeed: number;
13
+ stringLength: number;
14
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,4 @@
1
+ export { BubbleLayer } from './layer';
2
+ export { BubbleSimulation } from './simulation';
3
+ export type { BubbleSimulationConfig } from './simulation';
4
+ export type { Bubble, PopParticle } from './types';