@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.
Files changed (129) hide show
  1. package/dist/index.d.mts +1053 -28
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +4840 -400
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +7 -2
  6. package/src/aurora/consts.ts +3 -0
  7. package/src/aurora/index.ts +10 -0
  8. package/src/aurora/layer.ts +180 -0
  9. package/src/aurora/types.ts +13 -0
  10. package/src/balloons/consts.ts +3 -0
  11. package/src/balloons/index.ts +12 -0
  12. package/src/balloons/layer.ts +169 -0
  13. package/src/balloons/particle.ts +110 -0
  14. package/src/balloons/types.ts +14 -0
  15. package/src/bubbles/consts.ts +3 -0
  16. package/src/bubbles/index.ts +10 -0
  17. package/src/bubbles/layer.ts +246 -0
  18. package/src/bubbles/types.ts +21 -0
  19. package/src/canvas.ts +32 -1
  20. package/src/color.ts +19 -0
  21. package/src/confetti/consts.ts +13 -13
  22. package/src/confetti/index.ts +20 -2
  23. package/src/confetti/layer.ts +155 -0
  24. package/src/confetti/particle.ts +106 -0
  25. package/src/confetti/shapes.ts +104 -0
  26. package/src/confetti/types.ts +4 -1
  27. package/src/distance.ts +1 -1
  28. package/src/donuts/consts.ts +19 -0
  29. package/src/donuts/donut.ts +12 -0
  30. package/src/donuts/index.ts +9 -0
  31. package/src/donuts/layer.ts +301 -0
  32. package/src/effect.ts +107 -0
  33. package/src/fade.ts +87 -0
  34. package/src/fireflies/consts.ts +3 -0
  35. package/src/fireflies/index.ts +12 -0
  36. package/src/fireflies/layer.ts +169 -0
  37. package/src/fireflies/particle.ts +124 -0
  38. package/src/fireflies/types.ts +17 -0
  39. package/src/firepit/consts.ts +3 -0
  40. package/src/firepit/index.ts +10 -0
  41. package/src/firepit/layer.ts +193 -0
  42. package/src/firepit/types.ts +20 -0
  43. package/src/fireworks/create-explosion.ts +237 -0
  44. package/src/fireworks/explosion.ts +9 -9
  45. package/src/fireworks/firework.ts +9 -8
  46. package/src/fireworks/index.ts +19 -3
  47. package/src/fireworks/layer.ts +203 -0
  48. package/src/fireworks/spark.ts +9 -9
  49. package/src/fireworks/types.ts +2 -2
  50. package/src/glitter/consts.ts +13 -0
  51. package/src/glitter/index.ts +9 -0
  52. package/src/glitter/layer.ts +181 -0
  53. package/src/glitter/types.ts +33 -0
  54. package/src/index.ts +27 -0
  55. package/src/lanterns/consts.ts +13 -0
  56. package/src/lanterns/index.ts +9 -0
  57. package/src/lanterns/layer.ts +178 -0
  58. package/src/lanterns/types.ts +22 -0
  59. package/src/layer.ts +26 -0
  60. package/src/leaves/consts.ts +16 -0
  61. package/src/leaves/index.ts +9 -0
  62. package/src/leaves/layer.ts +258 -0
  63. package/src/leaves/types.ts +25 -0
  64. package/src/lightning/consts.ts +3 -0
  65. package/src/lightning/index.ts +11 -0
  66. package/src/lightning/layer.ts +41 -0
  67. package/src/lightning/system.ts +196 -0
  68. package/src/lightning/types.ts +20 -0
  69. package/src/matrix/consts.ts +5 -0
  70. package/src/matrix/index.ts +9 -0
  71. package/src/matrix/layer.ts +154 -0
  72. package/src/matrix/types.ts +17 -0
  73. package/src/orbits/consts.ts +13 -0
  74. package/src/orbits/index.ts +9 -0
  75. package/src/orbits/layer.ts +213 -0
  76. package/src/orbits/types.ts +27 -0
  77. package/src/particles/consts.ts +3 -0
  78. package/src/particles/index.ts +10 -0
  79. package/src/particles/layer.ts +360 -0
  80. package/src/particles/types.ts +10 -0
  81. package/src/petals/consts.ts +13 -0
  82. package/src/petals/index.ts +10 -0
  83. package/src/petals/layer.ts +174 -0
  84. package/src/petals/types.ts +15 -0
  85. package/src/plasma/consts.ts +3 -0
  86. package/src/plasma/index.ts +10 -0
  87. package/src/plasma/layer.ts +107 -0
  88. package/src/plasma/types.ts +5 -0
  89. package/src/rain/consts.ts +3 -0
  90. package/src/rain/index.ts +12 -0
  91. package/src/rain/layer.ts +194 -0
  92. package/src/rain/particle.ts +132 -0
  93. package/src/rain/types.ts +22 -0
  94. package/src/sandstorm/consts.ts +3 -0
  95. package/src/sandstorm/index.ts +10 -0
  96. package/src/sandstorm/layer.ts +152 -0
  97. package/src/sandstorm/types.ts +10 -0
  98. package/src/scene.ts +201 -0
  99. package/src/shooting-stars/index.ts +3 -0
  100. package/src/shooting-stars/system.ts +151 -0
  101. package/src/shooting-stars/types.ts +11 -0
  102. package/src/simulation-canvas.ts +83 -0
  103. package/src/snow/consts.ts +2 -2
  104. package/src/snow/index.ts +9 -2
  105. package/src/snow/{simulation.ts → layer.ts} +64 -89
  106. package/src/sparklers/consts.ts +3 -0
  107. package/src/sparklers/index.ts +16 -0
  108. package/src/sparklers/layer.ts +220 -0
  109. package/src/sparklers/particle.ts +89 -0
  110. package/src/sparklers/types.ts +13 -0
  111. package/src/stars/consts.ts +3 -0
  112. package/src/stars/index.ts +10 -0
  113. package/src/stars/layer.ts +139 -0
  114. package/src/stars/types.ts +12 -0
  115. package/src/streamers/consts.ts +14 -0
  116. package/src/streamers/index.ts +10 -0
  117. package/src/streamers/layer.ts +223 -0
  118. package/src/streamers/types.ts +14 -0
  119. package/src/trail.ts +140 -0
  120. package/src/waves/consts.ts +3 -0
  121. package/src/waves/index.ts +10 -0
  122. package/src/waves/layer.ts +164 -0
  123. package/src/waves/types.ts +10 -0
  124. package/src/wormhole/consts.ts +3 -0
  125. package/src/wormhole/index.ts +10 -0
  126. package/src/wormhole/layer.ts +197 -0
  127. package/src/wormhole/types.ts +10 -0
  128. package/src/confetti/simulation.ts +0 -221
  129. package/src/fireworks/simulation.ts +0 -493
@@ -0,0 +1,181 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { GLITTER_COLORS, MULBERRY } from './consts';
4
+ import type { FallingGlitter, GlitterConfig, SettledGlitter } from './types';
5
+
6
+ export class Glitter extends Effect<GlitterConfig> {
7
+ readonly #scale: number;
8
+ readonly #size: number;
9
+ #speed: number;
10
+ #groundLevel: number;
11
+ readonly #maxSettled: number;
12
+ readonly #colorRGBs: [number, number, number][];
13
+ #maxCount: number;
14
+ #time: number = 0;
15
+ #falling: FallingGlitter[] = [];
16
+ #settled: SettledGlitter[] = [];
17
+
18
+ constructor(config: GlitterConfig = {}) {
19
+ super();
20
+
21
+ this.#scale = config.scale ?? 1;
22
+ this.#maxCount = config.count ?? 80;
23
+ this.#size = (config.size ?? 4) * this.#scale;
24
+ this.#speed = config.speed ?? 1;
25
+ this.#groundLevel = config.groundLevel ?? 0.85;
26
+ this.#maxSettled = config.maxSettled ?? 200;
27
+
28
+ const colors = config.colors ?? GLITTER_COLORS;
29
+ this.#colorRGBs = colors.map(c => hexToRGB(c));
30
+
31
+ if (innerWidth < 991) {
32
+ this.#maxCount = Math.floor(this.#maxCount / 2);
33
+ }
34
+
35
+ for (let i = 0; i < this.#maxCount; ++i) {
36
+ this.#falling.push(this.#createFallingPiece(true));
37
+ }
38
+ }
39
+
40
+ configure(config: Partial<GlitterConfig>): void {
41
+ if (config.speed !== undefined) {
42
+ this.#speed = config.speed;
43
+ }
44
+ if (config.groundLevel !== undefined) {
45
+ this.#groundLevel = config.groundLevel;
46
+ }
47
+ }
48
+
49
+ tick(dt: number, _width: number, _height: number): void {
50
+ this.#time += 0.03 * dt;
51
+
52
+ let alive = 0;
53
+
54
+ for (let i = 0; i < this.#falling.length; i++) {
55
+ const piece = this.#falling[i];
56
+
57
+ piece.y += piece.vy * this.#speed * dt;
58
+ piece.rotation += piece.rotationSpeed * dt;
59
+ piece.flipAngle += piece.flipSpeed * dt;
60
+
61
+ piece.sparkle = 0.3 + 0.7 * Math.max(0, Math.sin(this.#time * 3 + piece.flipAngle * 2));
62
+
63
+ if (piece.y >= this.#groundLevel) {
64
+ this.#settleGlitter(piece);
65
+ this.#falling[alive++] = this.#createFallingPiece(false);
66
+ } else if (piece.y > 1.1) {
67
+ this.#falling[alive++] = this.#createFallingPiece(false);
68
+ } else {
69
+ this.#falling[alive++] = piece;
70
+ }
71
+ }
72
+
73
+ this.#falling.length = alive;
74
+
75
+ while (this.#settled.length > this.#maxSettled) {
76
+ this.#settled.shift();
77
+ }
78
+ }
79
+
80
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
81
+
82
+ for (const piece of this.#settled) {
83
+ const px = piece.x * width;
84
+ const py = piece.y * height;
85
+ const [r, g, b] = this.#colorRGBs[piece.colorIndex % this.#colorRGBs.length];
86
+
87
+ const sparkle = 0.3 + 0.7 * Math.max(0, Math.sin(this.#time * piece.sparkleSpeed + piece.sparklePhase));
88
+ const alpha = 0.4 + 0.6 * sparkle;
89
+
90
+ this.#drawDiamond(ctx, px, py, piece.size, piece.rotation, r, g, b, alpha, sparkle);
91
+ }
92
+
93
+ for (const piece of this.#falling) {
94
+ const px = piece.x * width;
95
+ const py = piece.y * height;
96
+ const [r, g, b] = this.#colorRGBs[piece.colorIndex % this.#colorRGBs.length];
97
+
98
+ const flipFactor = Math.abs(Math.cos(piece.flipAngle));
99
+ const alpha = 0.5 + 0.5 * piece.sparkle;
100
+
101
+ this.#drawDiamond(ctx, px, py, piece.size, piece.rotation, r, g, b, alpha, piece.sparkle, flipFactor);
102
+ }
103
+ }
104
+
105
+ #drawDiamond(
106
+ ctx: CanvasRenderingContext2D,
107
+ cx: number,
108
+ cy: number,
109
+ size: number,
110
+ rotation: number,
111
+ r: number,
112
+ g: number,
113
+ b: number,
114
+ alpha: number,
115
+ sparkle: number,
116
+ flipFactor: number = 1
117
+ ): void {
118
+ const halfW = size * flipFactor;
119
+ const halfH = size;
120
+
121
+ if (halfW < 0.3) {
122
+ return;
123
+ }
124
+
125
+ const cos = Math.cos(rotation);
126
+ const sin = Math.sin(rotation);
127
+
128
+ const points = [
129
+ {x: 0, y: -halfH},
130
+ {x: halfW, y: 0},
131
+ {x: 0, y: halfH},
132
+ {x: -halfW, y: 0}
133
+ ];
134
+
135
+ ctx.beginPath();
136
+ ctx.moveTo(cx + points[0].x * cos - points[0].y * sin, cy + points[0].x * sin + points[0].y * cos);
137
+
138
+ for (let p = 1; p < points.length; p++) {
139
+ ctx.lineTo(cx + points[p].x * cos - points[p].y * sin, cy + points[p].x * sin + points[p].y * cos);
140
+ }
141
+
142
+ ctx.closePath();
143
+
144
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.8})`;
145
+ ctx.fill();
146
+
147
+ if (sparkle > 0.5) {
148
+ const highlightAlpha = (sparkle - 0.5) * 2 * alpha;
149
+ ctx.fillStyle = `rgba(255, 255, 255, ${highlightAlpha * 0.6})`;
150
+ ctx.fill();
151
+ }
152
+ }
153
+
154
+ #settleGlitter(piece: FallingGlitter): void {
155
+ this.#settled.push({
156
+ x: piece.x,
157
+ y: this.#groundLevel + MULBERRY.next() * 0.05,
158
+ size: piece.size * 0.8,
159
+ rotation: piece.rotation,
160
+ sparklePhase: MULBERRY.next() * Math.PI * 2,
161
+ sparkleSpeed: 0.5 + MULBERRY.next() * 2,
162
+ colorIndex: piece.colorIndex
163
+ });
164
+ }
165
+
166
+ #createFallingPiece(initialSpread: boolean): FallingGlitter {
167
+ return {
168
+ x: MULBERRY.next(),
169
+ y: initialSpread ? MULBERRY.next() * this.#groundLevel : -0.05 - MULBERRY.next() * 0.1,
170
+ vy: (0.0008 + MULBERRY.next() * 0.0015) * this.#scale,
171
+ size: (0.5 + MULBERRY.next() * 1) * this.#size,
172
+ rotation: MULBERRY.next() * Math.PI * 2,
173
+ rotationSpeed: (MULBERRY.next() - 0.5) * 0.08,
174
+ flipAngle: MULBERRY.next() * Math.PI * 2,
175
+ flipSpeed: 0.03 + MULBERRY.next() * 0.07,
176
+ sparkle: MULBERRY.next(),
177
+ colorIndex: Math.floor(MULBERRY.next() * this.#colorRGBs.length),
178
+ settled: false
179
+ };
180
+ }
181
+ }
@@ -0,0 +1,33 @@
1
+ export interface GlitterConfig {
2
+ readonly count?: number;
3
+ readonly colors?: string[];
4
+ readonly size?: number;
5
+ readonly speed?: number;
6
+ readonly groundLevel?: number;
7
+ readonly maxSettled?: number;
8
+ readonly scale?: number;
9
+ }
10
+
11
+ export type FallingGlitter = {
12
+ x: number;
13
+ y: number;
14
+ vy: number;
15
+ size: number;
16
+ rotation: number;
17
+ rotationSpeed: number;
18
+ flipAngle: number;
19
+ flipSpeed: number;
20
+ sparkle: number;
21
+ colorIndex: number;
22
+ settled: boolean;
23
+ };
24
+
25
+ export type SettledGlitter = {
26
+ x: number;
27
+ y: number;
28
+ size: number;
29
+ rotation: number;
30
+ sparklePhase: number;
31
+ sparkleSpeed: number;
32
+ colorIndex: number;
33
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,31 @@
1
+ export * from './aurora';
2
+ export * from './balloons';
3
+ export * from './bubbles';
1
4
  export * from './canvas';
5
+ export * from './color';
2
6
  export * from './confetti';
7
+ export * from './donuts';
8
+ export * from './effect';
9
+ export * from './fireflies';
10
+ export * from './firepit';
3
11
  export * from './fireworks';
12
+ export * from './glitter';
13
+ export * from './lanterns';
14
+ export * from './leaves';
15
+ export * from './lightning';
16
+ export * from './matrix';
17
+ export * from './orbits';
18
+ export * from './particles';
19
+ export * from './petals';
20
+ export * from './plasma';
21
+ export * from './rain';
22
+ export * from './sandstorm';
23
+ export * from './scene';
24
+ export * from './shooting-stars';
4
25
  export * from './snow';
26
+ export * from './sparklers';
27
+ export * from './stars';
28
+ export * from './streamers';
29
+ export * from './trail';
30
+ export * from './waves';
31
+ export * from './wormhole';
@@ -0,0 +1,13 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const LANTERN_COLORS: string[] = [
6
+ '#ff6b35', // warm orange
7
+ '#ff8c42', // light orange
8
+ '#ffd166', // golden
9
+ '#ffb347', // amber
10
+ '#e85d04', // deep orange
11
+ '#f4845f', // coral
12
+ '#c1121f' // red
13
+ ];
@@ -0,0 +1,9 @@
1
+ import { Lanterns } from './layer';
2
+ import type { LanternsConfig } from './types';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createLanterns(config?: LanternsConfig): Effect<LanternsConfig> {
6
+ return new Lanterns(config);
7
+ }
8
+
9
+ export type { LanternsConfig, Lantern } from './types';
@@ -0,0 +1,178 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { LANTERN_COLORS, MULBERRY } from './consts';
4
+ import type { Lantern, LanternsConfig } from './types';
5
+
6
+ export class Lanterns extends Effect<LanternsConfig> {
7
+ readonly #scale: number;
8
+ #speed: number;
9
+ readonly #size: number;
10
+ readonly #colorRGBs: [number, number, number][];
11
+ #maxCount: number;
12
+ #time: number = 0;
13
+ #lanterns: Lantern[] = [];
14
+ #sortedLanterns: Lantern[] = [];
15
+ #sortDirty: boolean = true;
16
+
17
+ constructor(config: LanternsConfig = {}) {
18
+ super();
19
+
20
+ this.#scale = config.scale ?? 1;
21
+ this.#maxCount = config.count ?? 25;
22
+ this.#size = (config.size ?? 20) * this.#scale;
23
+ this.#speed = config.speed ?? 0.5;
24
+
25
+ const colors = config.colors ?? LANTERN_COLORS;
26
+ this.#colorRGBs = colors.map(c => hexToRGB(c));
27
+
28
+ if (innerWidth < 991) {
29
+ this.#maxCount = Math.floor(this.#maxCount / 2);
30
+ }
31
+
32
+ for (let i = 0; i < this.#maxCount; ++i) {
33
+ this.#lanterns.push(this.#createLantern(true));
34
+ }
35
+ }
36
+
37
+ configure(config: Partial<LanternsConfig>): void {
38
+ if (config.speed !== undefined) {
39
+ this.#speed = config.speed;
40
+ }
41
+ }
42
+
43
+ tick(dt: number, width: number, height: number): void {
44
+ this.#time += 0.02 * dt * this.#speed;
45
+
46
+ for (let i = 0; i < this.#lanterns.length; i++) {
47
+ const lantern = this.#lanterns[i];
48
+
49
+ lantern.y -= (lantern.vy * this.#speed * dt) / (height * 1.5);
50
+
51
+ const sway = Math.sin(this.#time * lantern.swaySpeed + lantern.swayPhase) * lantern.swayAmplitude;
52
+ lantern.x += sway * dt / (width * 8);
53
+
54
+ if (lantern.y < -0.15) {
55
+ this.#lanterns[i] = this.#createLantern(false);
56
+ this.#sortDirty = true;
57
+ }
58
+ }
59
+ }
60
+
61
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
62
+
63
+ if (this.#sortDirty) {
64
+ this.#sortedLanterns = [...this.#lanterns].sort((a, b) => a.size - b.size);
65
+ this.#sortDirty = false;
66
+ }
67
+
68
+ const sorted = this.#sortedLanterns;
69
+
70
+ for (const lantern of sorted) {
71
+ const px = lantern.x * width;
72
+ const py = lantern.y * height;
73
+ const size = lantern.size;
74
+ const [r, g, b] = this.#colorRGBs[lantern.colorIndex];
75
+
76
+ const glowPulse = 0.6 + 0.4 * Math.sin(this.#time * lantern.glowSpeed + lantern.glowPhase);
77
+ const alpha = lantern.opacity * glowPulse;
78
+
79
+ const glowRadius = size * 3;
80
+ const glowGradient = ctx.createRadialGradient(px, py, 0, px, py, glowRadius);
81
+ glowGradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha * 0.35})`);
82
+ glowGradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, ${alpha * 0.15})`);
83
+ glowGradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, ${alpha * 0.05})`);
84
+ glowGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
85
+
86
+ ctx.fillStyle = glowGradient;
87
+ ctx.beginPath();
88
+ ctx.arc(px, py, glowRadius, 0, Math.PI * 2);
89
+ ctx.fill();
90
+
91
+ ctx.setTransform(1, 0, 0, 1, px, py);
92
+
93
+ const bodyW = size * 0.8;
94
+ const bodyH = size;
95
+ const topW = bodyW * 0.6;
96
+
97
+ ctx.beginPath();
98
+ ctx.moveTo(-topW, -bodyH * 0.5);
99
+ ctx.quadraticCurveTo(-bodyW, 0, -bodyW * 0.7, bodyH * 0.5);
100
+ ctx.lineTo(bodyW * 0.7, bodyH * 0.5);
101
+ ctx.quadraticCurveTo(bodyW, 0, topW, -bodyH * 0.5);
102
+ ctx.closePath();
103
+
104
+ const bodyGradient = ctx.createLinearGradient(0, -bodyH * 0.5, 0, bodyH * 0.5);
105
+ bodyGradient.addColorStop(0, `rgba(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 30)}, ${alpha * 0.9})`);
106
+ bodyGradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, ${alpha * 0.85})`);
107
+ bodyGradient.addColorStop(1, `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 20)}, ${alpha * 0.8})`);
108
+
109
+ ctx.fillStyle = bodyGradient;
110
+ ctx.fill();
111
+
112
+ ctx.beginPath();
113
+ ctx.moveTo(-topW * 0.7, -bodyH * 0.55);
114
+ ctx.lineTo(topW * 0.7, -bodyH * 0.55);
115
+ ctx.lineWidth = size * 0.06;
116
+ ctx.strokeStyle = `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, ${alpha * 0.7})`;
117
+ ctx.stroke();
118
+
119
+ const flameH = bodyH * 0.3;
120
+ const flameW = bodyW * 0.15;
121
+ const flameFlicker = Math.sin(this.#time * 8 + lantern.glowPhase) * flameW * 0.3;
122
+
123
+ const flameGradient = ctx.createRadialGradient(
124
+ flameFlicker, -flameH * 0.1, 0,
125
+ flameFlicker, -flameH * 0.1, flameH
126
+ );
127
+ flameGradient.addColorStop(0, `rgba(255, 255, 200, ${alpha * 0.95})`);
128
+ flameGradient.addColorStop(0.3, `rgba(255, 200, 80, ${alpha * 0.7})`);
129
+ flameGradient.addColorStop(0.7, `rgba(255, 140, 40, ${alpha * 0.3})`);
130
+ flameGradient.addColorStop(1, `rgba(255, 100, 20, 0)`);
131
+
132
+ ctx.beginPath();
133
+ ctx.moveTo(-flameW + flameFlicker, flameH * 0.2);
134
+ ctx.quadraticCurveTo(-flameW * 0.5 + flameFlicker, -flameH * 0.3, flameFlicker, -flameH);
135
+ ctx.quadraticCurveTo(flameW * 0.5 + flameFlicker, -flameH * 0.3, flameW + flameFlicker, flameH * 0.2);
136
+ ctx.closePath();
137
+ ctx.fillStyle = flameGradient;
138
+ ctx.fill();
139
+
140
+ const stringLen = size * 0.6;
141
+ const stringDrift = Math.sin(this.#time * 1.5 + lantern.swayPhase) * size * 0.1;
142
+
143
+ ctx.beginPath();
144
+ ctx.moveTo(0, bodyH * 0.5);
145
+ ctx.quadraticCurveTo(
146
+ stringDrift,
147
+ bodyH * 0.5 + stringLen * 0.5,
148
+ -stringDrift * 0.5,
149
+ bodyH * 0.5 + stringLen
150
+ );
151
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.4})`;
152
+ ctx.lineWidth = size * 0.04;
153
+ ctx.stroke();
154
+
155
+ ctx.resetTransform();
156
+ }
157
+ }
158
+
159
+ #createLantern(initialSpread: boolean): Lantern {
160
+ const colorIndex = Math.floor(MULBERRY.next() * this.#colorRGBs.length);
161
+ const sizeVariation = 0.6 + MULBERRY.next() * 0.8;
162
+
163
+ return {
164
+ x: 0.05 + MULBERRY.next() * 0.9,
165
+ y: initialSpread ? MULBERRY.next() * 1.3 : 1.15 + MULBERRY.next() * 0.2,
166
+ vx: 0,
167
+ vy: 0.2 + MULBERRY.next() * 0.6,
168
+ size: this.#size * sizeVariation,
169
+ glowPhase: MULBERRY.next() * Math.PI * 2,
170
+ glowSpeed: 0.8 + MULBERRY.next() * 1.2,
171
+ swayPhase: MULBERRY.next() * Math.PI * 2,
172
+ swaySpeed: 0.4 + MULBERRY.next() * 0.8,
173
+ swayAmplitude: 0.3 + MULBERRY.next() * 0.7,
174
+ colorIndex,
175
+ opacity: 0.7 + MULBERRY.next() * 0.3
176
+ };
177
+ }
178
+ }
@@ -0,0 +1,22 @@
1
+ export interface LanternsConfig {
2
+ readonly count?: number;
3
+ readonly colors?: string[];
4
+ readonly size?: number;
5
+ readonly speed?: number;
6
+ readonly scale?: number;
7
+ }
8
+
9
+ export type Lantern = {
10
+ x: number;
11
+ y: number;
12
+ vx: number;
13
+ vy: number;
14
+ size: number;
15
+ glowPhase: number;
16
+ glowSpeed: number;
17
+ swayPhase: number;
18
+ swaySpeed: number;
19
+ swayAmplitude: number;
20
+ colorIndex: number;
21
+ opacity: number;
22
+ };
package/src/layer.ts ADDED
@@ -0,0 +1,26 @@
1
+ export type EdgeFadeSide = number | [number, number];
2
+
3
+ export type EdgeFade = {
4
+ readonly top?: EdgeFadeSide;
5
+ readonly bottom?: EdgeFadeSide;
6
+ readonly left?: EdgeFadeSide;
7
+ readonly right?: EdgeFadeSide;
8
+ };
9
+
10
+ /**
11
+ * Internal interface implemented by all Effect subclasses. Used by SimulationCanvas
12
+ * and Scene to drive rendering without depending on the generic Effect<TConfig> type.
13
+ */
14
+ export interface SimulationLayer {
15
+ fade: EdgeFade | null;
16
+
17
+ tick(dt: number, width: number, height: number): void;
18
+
19
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void;
20
+
21
+ onResize(width: number, height: number): void;
22
+
23
+ onMount(canvas: HTMLCanvasElement): void;
24
+
25
+ onUnmount(canvas: HTMLCanvasElement): void;
26
+ }
@@ -0,0 +1,16 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const LEAF_COLORS: string[] = [
6
+ '#c0392b', // deep red
7
+ '#e74c3c', // bright red
8
+ '#d35400', // burnt orange
9
+ '#e67e22', // orange
10
+ '#f39c12', // amber
11
+ '#f1c40f', // golden yellow
12
+ '#d4a017', // dark gold
13
+ '#8b4513', // saddle brown
14
+ '#a0522d', // sienna
15
+ '#6b8e23' // olive (some green still)
16
+ ];
@@ -0,0 +1,9 @@
1
+ import { Leaves } from './layer';
2
+ import type { LeavesConfig } from './types';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createLeaves(config?: LeavesConfig): Effect<LeavesConfig> {
6
+ return new Leaves(config);
7
+ }
8
+
9
+ export type { LeavesConfig, Leaf } from './types';