@basmilius/sparkle 2.3.0 → 2.5.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 (126) hide show
  1. package/dist/index.d.mts +637 -1
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +6964 -2577
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/src/balloons/layer.ts +4 -3
  7. package/src/black-hole/consts.ts +3 -0
  8. package/src/black-hole/index.ts +10 -0
  9. package/src/black-hole/layer.ts +193 -0
  10. package/src/black-hole/types.ts +8 -0
  11. package/src/boids/consts.ts +8 -0
  12. package/src/boids/index.ts +9 -0
  13. package/src/boids/layer.ts +245 -0
  14. package/src/boids/types.ts +7 -0
  15. package/src/butterflies/consts.ts +3 -0
  16. package/src/butterflies/index.ts +9 -0
  17. package/src/butterflies/layer.ts +246 -0
  18. package/src/butterflies/types.ts +23 -0
  19. package/src/caustics/consts.ts +3 -0
  20. package/src/caustics/index.ts +9 -0
  21. package/src/caustics/layer.ts +107 -0
  22. package/src/clouds/consts.ts +3 -0
  23. package/src/clouds/index.ts +9 -0
  24. package/src/clouds/layer.ts +167 -0
  25. package/src/clouds/types.ts +9 -0
  26. package/src/confetti/layer.ts +3 -2
  27. package/src/constellation/consts.ts +3 -0
  28. package/src/constellation/index.ts +10 -0
  29. package/src/constellation/layer.ts +256 -0
  30. package/src/constellation/types.ts +11 -0
  31. package/src/coral-reef/consts.ts +3 -0
  32. package/src/coral-reef/index.ts +10 -0
  33. package/src/coral-reef/layer.ts +276 -0
  34. package/src/coral-reef/types.ts +31 -0
  35. package/src/crystallization/consts.ts +3 -0
  36. package/src/crystallization/index.ts +10 -0
  37. package/src/crystallization/layer.ts +318 -0
  38. package/src/crystallization/types.ts +25 -0
  39. package/src/digital-rain/consts.ts +7 -0
  40. package/src/digital-rain/index.ts +10 -0
  41. package/src/digital-rain/layer.ts +195 -0
  42. package/src/digital-rain/types.ts +10 -0
  43. package/src/donuts/layer.ts +5 -3
  44. package/src/glitch/consts.ts +3 -0
  45. package/src/glitch/index.ts +9 -0
  46. package/src/glitch/layer.ts +231 -0
  47. package/src/glitch/types.ts +28 -0
  48. package/src/gradient-flow/consts.ts +3 -0
  49. package/src/gradient-flow/index.ts +9 -0
  50. package/src/gradient-flow/layer.ts +134 -0
  51. package/src/gradient-flow/types.ts +8 -0
  52. package/src/hologram/consts.ts +5 -0
  53. package/src/hologram/index.ts +9 -0
  54. package/src/hologram/layer.ts +205 -0
  55. package/src/hologram/types.ts +20 -0
  56. package/src/hyper-space/consts.ts +3 -0
  57. package/src/hyper-space/index.ts +10 -0
  58. package/src/hyper-space/layer.ts +167 -0
  59. package/src/hyper-space/types.ts +8 -0
  60. package/src/index.ts +29 -0
  61. package/src/interference/consts.ts +9 -0
  62. package/src/interference/index.ts +9 -0
  63. package/src/interference/layer.ts +129 -0
  64. package/src/kaleidoscope/consts.ts +12 -0
  65. package/src/kaleidoscope/index.ts +9 -0
  66. package/src/kaleidoscope/layer.ts +213 -0
  67. package/src/kaleidoscope/types.ts +19 -0
  68. package/src/lanterns/layer.ts +3 -2
  69. package/src/lava/consts.ts +3 -0
  70. package/src/lava/index.ts +9 -0
  71. package/src/lava/layer.ts +152 -0
  72. package/src/lava/types.ts +13 -0
  73. package/src/leaves/layer.ts +3 -2
  74. package/src/murmuration/consts.ts +3 -0
  75. package/src/murmuration/index.ts +10 -0
  76. package/src/murmuration/layer.ts +279 -0
  77. package/src/murmuration/types.ts +7 -0
  78. package/src/nebula/consts.ts +3 -0
  79. package/src/nebula/index.ts +10 -0
  80. package/src/nebula/layer.ts +150 -0
  81. package/src/nebula/types.ts +20 -0
  82. package/src/neon/consts.ts +5 -0
  83. package/src/neon/index.ts +9 -0
  84. package/src/neon/layer.ts +213 -0
  85. package/src/neon/types.ts +18 -0
  86. package/src/petals/layer.ts +3 -2
  87. package/src/pollen/consts.ts +3 -0
  88. package/src/pollen/index.ts +10 -0
  89. package/src/pollen/layer.ts +181 -0
  90. package/src/pollen/types.ts +10 -0
  91. package/src/popcorn/consts.ts +3 -0
  92. package/src/popcorn/index.ts +10 -0
  93. package/src/popcorn/layer.ts +218 -0
  94. package/src/popcorn/types.ts +13 -0
  95. package/src/portal/consts.ts +3 -0
  96. package/src/portal/index.ts +10 -0
  97. package/src/portal/layer.ts +251 -0
  98. package/src/portal/types.ts +10 -0
  99. package/src/pulse-grid/consts.ts +3 -0
  100. package/src/pulse-grid/index.ts +10 -0
  101. package/src/pulse-grid/layer.ts +185 -0
  102. package/src/pulse-grid/types.ts +8 -0
  103. package/src/roots/consts.ts +3 -0
  104. package/src/roots/index.ts +9 -0
  105. package/src/roots/layer.ts +218 -0
  106. package/src/roots/types.ts +23 -0
  107. package/src/smoke/consts.ts +3 -0
  108. package/src/smoke/index.ts +9 -0
  109. package/src/smoke/layer.ts +182 -0
  110. package/src/smoke/types.ts +14 -0
  111. package/src/snow/layer.ts +3 -2
  112. package/src/topography/consts.ts +3 -0
  113. package/src/topography/index.ts +9 -0
  114. package/src/topography/layer.ts +141 -0
  115. package/src/tornado/consts.ts +3 -0
  116. package/src/tornado/index.ts +10 -0
  117. package/src/tornado/layer.ts +271 -0
  118. package/src/tornado/types.ts +22 -0
  119. package/src/volcano/consts.ts +3 -0
  120. package/src/volcano/index.ts +10 -0
  121. package/src/volcano/layer.ts +261 -0
  122. package/src/volcano/types.ts +10 -0
  123. package/src/voronoi/consts.ts +3 -0
  124. package/src/voronoi/index.ts +10 -0
  125. package/src/voronoi/layer.ts +197 -0
  126. package/src/voronoi/types.ts +7 -0
@@ -0,0 +1,256 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { ConstellationStar } from './types';
5
+
6
+ export interface ConstellationConfig {
7
+ readonly stars?: number;
8
+ readonly speed?: number;
9
+ readonly connectionDistance?: number;
10
+ readonly color?: string;
11
+ readonly lineWidth?: number;
12
+ readonly twinkleSpeed?: number;
13
+ readonly scale?: number;
14
+ }
15
+
16
+ const DEFAULT_STARS = 50;
17
+ const DEFAULT_CONNECTION_DISTANCE = 120;
18
+ const DEFAULT_LINE_WIDTH = 0.5;
19
+ const TWO_PI = Math.PI * 2;
20
+ const SPRITE_SIZE = 64;
21
+ const SPRITE_CENTER = SPRITE_SIZE / 2;
22
+ const SPRITE_RADIUS = SPRITE_SIZE / 2;
23
+
24
+ export class Constellation extends Effect<ConstellationConfig> {
25
+ readonly #scale: number;
26
+ readonly #r: number;
27
+ readonly #g: number;
28
+ readonly #b: number;
29
+ readonly #lineWidth: number;
30
+ readonly #maxStars: number;
31
+ #speed: number;
32
+ #twinkleSpeed: number;
33
+ #connectionDistance: number;
34
+ #time: number = 0;
35
+ #stars: ConstellationStar[] = [];
36
+ #sprite: HTMLCanvasElement;
37
+ #width: number = 0;
38
+ #height: number = 0;
39
+
40
+ constructor(config: ConstellationConfig = {}) {
41
+ super();
42
+
43
+ this.#scale = config.scale ?? 1;
44
+ this.#maxStars = config.stars ?? DEFAULT_STARS;
45
+ this.#speed = config.speed ?? 1;
46
+ this.#connectionDistance = (config.connectionDistance ?? DEFAULT_CONNECTION_DISTANCE) * this.#scale;
47
+ this.#lineWidth = (config.lineWidth ?? DEFAULT_LINE_WIDTH) * this.#scale;
48
+ this.#twinkleSpeed = config.twinkleSpeed ?? 1;
49
+
50
+ const [r, g, b] = hexToRGB(config.color ?? '#ffffff');
51
+ this.#r = r;
52
+ this.#g = g;
53
+ this.#b = b;
54
+
55
+ this.#sprite = this.#createSprite(r, g, b);
56
+ }
57
+
58
+ configure(config: Partial<ConstellationConfig>): void {
59
+ if (config.speed !== undefined) {
60
+ this.#speed = config.speed;
61
+ }
62
+ if (config.twinkleSpeed !== undefined) {
63
+ this.#twinkleSpeed = config.twinkleSpeed;
64
+ }
65
+ if (config.connectionDistance !== undefined) {
66
+ this.#connectionDistance = config.connectionDistance * this.#scale;
67
+ }
68
+ }
69
+
70
+ onResize(width: number, height: number): void {
71
+ this.#width = width;
72
+ this.#height = height;
73
+
74
+ if (this.#stars.length === 0) {
75
+ for (let index = 0; index < this.#maxStars; index++) {
76
+ this.#stars.push(this.#createStar(width, height, true));
77
+ }
78
+ }
79
+ }
80
+
81
+ tick(dt: number, width: number, height: number): void {
82
+ this.#width = width;
83
+ this.#height = height;
84
+
85
+ const dtSeconds = dt / 1000 * this.#speed;
86
+ this.#time += dtSeconds;
87
+
88
+ for (const star of this.#stars) {
89
+ // Twinkle: oscillate brightness.
90
+ const twinkle = 0.5 + 0.5 * Math.sin(this.#time * star.twinkleSpeed * this.#twinkleSpeed + star.phase);
91
+ star.targetBrightness = twinkle;
92
+
93
+ // Smooth brightness transition (fade-in effect).
94
+ star.brightness += (star.targetBrightness - star.brightness) * Math.min(1, dtSeconds * 3);
95
+
96
+ // Drift slowly.
97
+ star.x += star.vx * dtSeconds;
98
+ star.y += star.vy * dtSeconds;
99
+
100
+ // Recycle stars that drift off-screen.
101
+ if (star.x < -10 || star.x > width + 10 || star.y < -10 || star.y > height + 10) {
102
+ this.#recycleStar(star, width, height);
103
+ }
104
+ }
105
+ }
106
+
107
+ draw(ctx: CanvasRenderingContext2D, _width: number, _height: number): void {
108
+ const connectionDist = this.#connectionDistance;
109
+ const connectionDistSq = connectionDist * connectionDist;
110
+ const stars = this.#stars;
111
+ const starCount = stars.length;
112
+ const r = this.#r;
113
+ const g = this.#g;
114
+ const b = this.#b;
115
+
116
+ // Draw connection lines.
117
+ ctx.lineWidth = this.#lineWidth;
118
+
119
+ for (let index = 0; index < starCount; index++) {
120
+ const starA = stars[index];
121
+
122
+ if (starA.brightness < 0.05) {
123
+ continue;
124
+ }
125
+
126
+ for (let jndex = index + 1; jndex < starCount; jndex++) {
127
+ const starB = stars[jndex];
128
+
129
+ if (starB.brightness < 0.05) {
130
+ continue;
131
+ }
132
+
133
+ const dx = starA.x - starB.x;
134
+ const dy = starA.y - starB.y;
135
+ const distSq = dx * dx + dy * dy;
136
+
137
+ if (distSq > connectionDistSq) {
138
+ continue;
139
+ }
140
+
141
+ const dist = Math.sqrt(distSq);
142
+ const opacity = Math.min(starA.brightness, starB.brightness) * (1 - dist / connectionDist);
143
+
144
+ if (opacity < 0.01) {
145
+ continue;
146
+ }
147
+
148
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${opacity * 0.6})`;
149
+ ctx.beginPath();
150
+ ctx.moveTo(starA.x, starA.y);
151
+ ctx.lineTo(starB.x, starB.y);
152
+ ctx.stroke();
153
+ }
154
+ }
155
+
156
+ // Draw star sprites.
157
+ for (const star of stars) {
158
+ if (star.brightness < 0.02) {
159
+ continue;
160
+ }
161
+
162
+ const displaySize = star.size * 2;
163
+
164
+ ctx.globalAlpha = star.brightness;
165
+ ctx.drawImage(
166
+ this.#sprite,
167
+ star.x - star.size,
168
+ star.y - star.size,
169
+ displaySize,
170
+ displaySize
171
+ );
172
+ }
173
+
174
+ ctx.globalAlpha = 1;
175
+ }
176
+
177
+ #createSprite(r: number, g: number, b: number): HTMLCanvasElement {
178
+ const canvas = document.createElement('canvas');
179
+ canvas.width = SPRITE_SIZE;
180
+ canvas.height = SPRITE_SIZE;
181
+ const ctx = canvas.getContext('2d')!;
182
+
183
+ const gradient = ctx.createRadialGradient(
184
+ SPRITE_CENTER, SPRITE_CENTER, 0,
185
+ SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS
186
+ );
187
+
188
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1)`);
189
+ gradient.addColorStop(0.1, `rgba(${r}, ${g}, ${b}, 0.7)`);
190
+ gradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, 0.2)`);
191
+ gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.05)`);
192
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
193
+
194
+ ctx.fillStyle = gradient;
195
+ ctx.beginPath();
196
+ ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, TWO_PI);
197
+ ctx.fill();
198
+
199
+ return canvas;
200
+ }
201
+
202
+ #createStar(width: number, height: number, initialSpread: boolean): ConstellationStar {
203
+ const driftSpeed = 2 + MULBERRY.next() * 4;
204
+ const driftAngle = MULBERRY.next() * TWO_PI;
205
+
206
+ return {
207
+ x: initialSpread ? MULBERRY.next() * width : -10,
208
+ y: initialSpread ? MULBERRY.next() * height : -10,
209
+ size: (1.5 + MULBERRY.next() * 3) * this.#scale,
210
+ brightness: initialSpread ? MULBERRY.next() : 0,
211
+ targetBrightness: 0,
212
+ phase: MULBERRY.next() * TWO_PI,
213
+ twinkleSpeed: 0.5 + MULBERRY.next() * 2,
214
+ vx: Math.cos(driftAngle) * driftSpeed,
215
+ vy: Math.sin(driftAngle) * driftSpeed
216
+ };
217
+ }
218
+
219
+ #recycleStar(star: ConstellationStar, width: number, height: number): void {
220
+ const edge = Math.floor(MULBERRY.next() * 4);
221
+ const driftSpeed = 2 + MULBERRY.next() * 4;
222
+
223
+ switch (edge) {
224
+ case 0: // Top edge.
225
+ star.x = MULBERRY.next() * width;
226
+ star.y = -5;
227
+ star.vx = (MULBERRY.next() - 0.5) * driftSpeed;
228
+ star.vy = Math.abs(MULBERRY.next() * driftSpeed) + 0.5;
229
+ break;
230
+ case 1: // Right edge.
231
+ star.x = width + 5;
232
+ star.y = MULBERRY.next() * height;
233
+ star.vx = -(Math.abs(MULBERRY.next() * driftSpeed) + 0.5);
234
+ star.vy = (MULBERRY.next() - 0.5) * driftSpeed;
235
+ break;
236
+ case 2: // Bottom edge.
237
+ star.x = MULBERRY.next() * width;
238
+ star.y = height + 5;
239
+ star.vx = (MULBERRY.next() - 0.5) * driftSpeed;
240
+ star.vy = -(Math.abs(MULBERRY.next() * driftSpeed) + 0.5);
241
+ break;
242
+ default: // Left edge.
243
+ star.x = -5;
244
+ star.y = MULBERRY.next() * height;
245
+ star.vx = Math.abs(MULBERRY.next() * driftSpeed) + 0.5;
246
+ star.vy = (MULBERRY.next() - 0.5) * driftSpeed;
247
+ break;
248
+ }
249
+
250
+ star.size = (1.5 + MULBERRY.next() * 3) * this.#scale;
251
+ star.brightness = 0;
252
+ star.targetBrightness = 0;
253
+ star.phase = MULBERRY.next() * TWO_PI;
254
+ star.twinkleSpeed = 0.5 + MULBERRY.next() * 2;
255
+ }
256
+ }
@@ -0,0 +1,11 @@
1
+ export type ConstellationStar = {
2
+ x: number;
3
+ y: number;
4
+ size: number;
5
+ brightness: number;
6
+ targetBrightness: number;
7
+ phase: number;
8
+ twinkleSpeed: number;
9
+ vx: number;
10
+ vy: number;
11
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,10 @@
1
+ import { CoralReef } from './layer';
2
+ import type { CoralReefConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createCoralReef(config?: CoralReefConfig): Effect<CoralReefConfig> {
6
+ return new CoralReef(config);
7
+ }
8
+
9
+ export type { CoralReefConfig };
10
+ export type { CoralAnemone, CoralBubble, CoralJellyfish } from './types';
@@ -0,0 +1,276 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { CoralAnemone, CoralBubble, CoralJellyfish } from './types';
4
+
5
+ const DEFAULT_COLORS = ['#ff6b9d', '#c44dff', '#00d4aa', '#ff8c42', '#4dc9f6'];
6
+
7
+ export interface CoralReefConfig {
8
+ readonly anemones?: number;
9
+ readonly jellyfish?: number;
10
+ readonly bubbles?: number;
11
+ readonly speed?: number;
12
+ readonly colors?: string[];
13
+ readonly scale?: number;
14
+ }
15
+
16
+ export class CoralReef extends Effect<CoralReefConfig> {
17
+ readonly #scale: number;
18
+ readonly #colors: string[];
19
+ #speed: number;
20
+ #maxAnemones: number;
21
+ #maxJellyfish: number;
22
+ #maxBubbles: number;
23
+ #time: number = 0;
24
+ #anemones: CoralAnemone[] = [];
25
+ #jellyfish: CoralJellyfish[] = [];
26
+ #bubbles: CoralBubble[] = [];
27
+ #width: number = 960;
28
+ #height: number = 540;
29
+
30
+ constructor(config: CoralReefConfig = {}) {
31
+ super();
32
+
33
+ this.#scale = config.scale ?? 1;
34
+ this.#speed = config.speed ?? 1;
35
+ this.#colors = config.colors ?? DEFAULT_COLORS;
36
+ this.#maxAnemones = config.anemones ?? 8;
37
+ this.#maxJellyfish = config.jellyfish ?? 5;
38
+ this.#maxBubbles = config.bubbles ?? 20;
39
+
40
+ if (innerWidth < 991) {
41
+ this.#maxAnemones = Math.floor(this.#maxAnemones / 2);
42
+ this.#maxJellyfish = Math.floor(this.#maxJellyfish / 2);
43
+ this.#maxBubbles = Math.floor(this.#maxBubbles / 2);
44
+ }
45
+ }
46
+
47
+ configure(config: Partial<CoralReefConfig>): void {
48
+ if (config.speed !== undefined) {
49
+ this.#speed = config.speed;
50
+ }
51
+ }
52
+
53
+ onResize(width: number, height: number): void {
54
+ this.#width = width;
55
+ this.#height = height;
56
+
57
+ this.#anemones = [];
58
+ this.#jellyfish = [];
59
+ this.#bubbles = [];
60
+
61
+ for (let i = 0; i < this.#maxAnemones; ++i) {
62
+ this.#anemones.push(this.#createAnemone());
63
+ }
64
+
65
+ for (let i = 0; i < this.#maxJellyfish; ++i) {
66
+ this.#jellyfish.push(this.#createJellyfish(true));
67
+ }
68
+
69
+ for (let i = 0; i < this.#maxBubbles; ++i) {
70
+ this.#bubbles.push(this.#createBubble(true));
71
+ }
72
+ }
73
+
74
+ tick(dt: number, width: number, height: number): void {
75
+ this.#width = width;
76
+ this.#height = height;
77
+ this.#time += 0.001 * this.#speed * dt;
78
+
79
+ for (const anemone of this.#anemones) {
80
+ anemone.phase += 0.0008 * anemone.speed * this.#speed * dt;
81
+ }
82
+
83
+ for (let i = 0; i < this.#jellyfish.length; i++) {
84
+ const jelly = this.#jellyfish[i];
85
+
86
+ jelly.pulsePhase += 0.002 * jelly.speed * this.#speed * dt;
87
+ jelly.y -= (jelly.speed * this.#speed * 0.015 * dt) / height;
88
+ jelly.x += (Math.sin(this.#time * 2 + jelly.phase) * jelly.drift * dt) / width;
89
+
90
+ if (jelly.y < -0.15) {
91
+ this.#jellyfish[i] = this.#createJellyfish(false);
92
+ }
93
+ }
94
+
95
+ for (let i = 0; i < this.#bubbles.length; i++) {
96
+ const bubble = this.#bubbles[i];
97
+
98
+ bubble.wobblePhase += 0.003 * this.#speed * dt;
99
+ bubble.y -= (bubble.speed * this.#speed * 0.02 * dt) / height;
100
+ bubble.x += (Math.sin(bubble.wobblePhase) * 0.15 * dt) / width;
101
+
102
+ if (bubble.y < -0.05) {
103
+ this.#bubbles[i] = this.#createBubble(false);
104
+ }
105
+ }
106
+ }
107
+
108
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
109
+ this.#drawAnemones(ctx, width, height);
110
+ this.#drawJellyfish(ctx, width, height);
111
+ this.#drawBubbles(ctx, width, height);
112
+ }
113
+
114
+ #drawAnemones(ctx: CanvasRenderingContext2D, width: number, height: number): void {
115
+ ctx.lineCap = 'round';
116
+
117
+ for (const anemone of this.#anemones) {
118
+ const baseX = anemone.x * width;
119
+ const baseY = anemone.baseY * height;
120
+
121
+ for (let seg = 0; seg < anemone.segments; seg++) {
122
+ const ratio = seg / anemone.segments;
123
+ const segWidth = anemone.width * this.#scale * (1 - ratio * 0.7);
124
+ const sway = Math.sin(anemone.phase + seg * 0.6) * (12 + seg * 4) * this.#scale;
125
+
126
+ let startX = baseX;
127
+ let startY = baseY;
128
+
129
+ for (let prev = 0; prev < seg; prev++) {
130
+ const prevSway = Math.sin(anemone.phase + prev * 0.6) * (12 + prev * 4) * this.#scale;
131
+ startX += prevSway * 0.15;
132
+ startY -= anemone.segmentLength * this.#scale;
133
+ }
134
+
135
+ const endX = startX + sway * 0.15;
136
+ const endY = startY - anemone.segmentLength * this.#scale;
137
+
138
+ ctx.beginPath();
139
+ ctx.moveTo(startX, startY);
140
+ ctx.quadraticCurveTo(
141
+ startX + sway * 0.5,
142
+ (startY + endY) / 2,
143
+ endX,
144
+ endY
145
+ );
146
+ ctx.lineWidth = segWidth;
147
+ ctx.strokeStyle = anemone.color;
148
+ ctx.globalAlpha = 0.7 + ratio * 0.3;
149
+ ctx.stroke();
150
+ }
151
+ }
152
+
153
+ ctx.globalAlpha = 1;
154
+ }
155
+
156
+ #drawJellyfish(ctx: CanvasRenderingContext2D, width: number, height: number): void {
157
+ for (const jelly of this.#jellyfish) {
158
+ const px = jelly.x * width;
159
+ const py = jelly.y * height;
160
+ const size = jelly.size * this.#scale;
161
+ const pulse = 1 + Math.sin(jelly.pulsePhase) * 0.15;
162
+ const bellWidth = size * pulse;
163
+ const bellHeight = size * 0.6 * (1 / pulse);
164
+
165
+ ctx.globalAlpha = 0.6;
166
+ ctx.fillStyle = jelly.color;
167
+ ctx.beginPath();
168
+ ctx.ellipse(px, py, bellWidth, bellHeight, 0, Math.PI, 0);
169
+ ctx.fill();
170
+
171
+ ctx.globalAlpha = 0.3;
172
+ ctx.fillStyle = jelly.color;
173
+ ctx.beginPath();
174
+ ctx.ellipse(px, py + bellHeight * 0.2, bellWidth * 0.7, bellHeight * 0.5, 0, Math.PI, 0);
175
+ ctx.fill();
176
+
177
+ ctx.globalAlpha = 0.4;
178
+ ctx.strokeStyle = jelly.color;
179
+ ctx.lineWidth = 1.5 * this.#scale;
180
+ ctx.lineCap = 'round';
181
+
182
+ const tentacleSpacing = (bellWidth * 2) / (jelly.tentacles + 1);
183
+
184
+ for (let t = 0; t < jelly.tentacles; t++) {
185
+ const tx = px - bellWidth + tentacleSpacing * (t + 1);
186
+ const tentacleLength = size * (0.8 + MULBERRY.next() * 0.01);
187
+
188
+ ctx.beginPath();
189
+ ctx.moveTo(tx, py);
190
+
191
+ const cp1x = tx + Math.sin(jelly.pulsePhase + t * 1.2) * size * 0.3;
192
+ const cp1y = py + tentacleLength * 0.4;
193
+ const cp2x = tx - Math.sin(jelly.pulsePhase * 0.7 + t * 0.9) * size * 0.2;
194
+ const cp2y = py + tentacleLength * 0.7;
195
+ const endX = tx + Math.sin(jelly.pulsePhase * 1.3 + t * 0.6) * size * 0.15;
196
+ const endY = py + tentacleLength;
197
+
198
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY);
199
+ ctx.stroke();
200
+ }
201
+
202
+ ctx.globalAlpha = 0.3;
203
+ ctx.fillStyle = '#ffffff';
204
+ ctx.beginPath();
205
+ ctx.ellipse(px - bellWidth * 0.25, py - bellHeight * 0.3, bellWidth * 0.15, bellHeight * 0.1, -0.3, 0, Math.PI * 2);
206
+ ctx.fill();
207
+ }
208
+
209
+ ctx.globalAlpha = 1;
210
+ }
211
+
212
+ #drawBubbles(ctx: CanvasRenderingContext2D, width: number, height: number): void {
213
+ for (const bubble of this.#bubbles) {
214
+ const px = bubble.x * width;
215
+ const py = bubble.y * height;
216
+ const radius = bubble.size * this.#scale;
217
+
218
+ ctx.globalAlpha = bubble.opacity;
219
+ ctx.beginPath();
220
+ ctx.arc(px, py, radius, 0, Math.PI * 2);
221
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
222
+ ctx.lineWidth = 1;
223
+ ctx.stroke();
224
+
225
+ ctx.globalAlpha = bubble.opacity * 0.15;
226
+ ctx.fillStyle = '#ffffff';
227
+ ctx.fill();
228
+
229
+ ctx.globalAlpha = bubble.opacity * 0.6;
230
+ ctx.beginPath();
231
+ ctx.ellipse(px - radius * 0.25, py - radius * 0.25, radius * 0.15, radius * 0.08, -0.5, 0, Math.PI * 2);
232
+ ctx.fillStyle = '#ffffff';
233
+ ctx.fill();
234
+ }
235
+
236
+ ctx.globalAlpha = 1;
237
+ }
238
+
239
+ #createAnemone(): CoralAnemone {
240
+ return {
241
+ x: 0.05 + MULBERRY.next() * 0.9,
242
+ baseY: 0.92 + MULBERRY.next() * 0.08,
243
+ segments: 4 + Math.floor(MULBERRY.next() * 4),
244
+ segmentLength: 12 + MULBERRY.next() * 10,
245
+ phase: MULBERRY.next() * Math.PI * 2,
246
+ speed: 0.6 + MULBERRY.next() * 0.8,
247
+ color: this.#colors[Math.floor(MULBERRY.next() * this.#colors.length)],
248
+ width: 6 + MULBERRY.next() * 6
249
+ };
250
+ }
251
+
252
+ #createJellyfish(initialSpread: boolean): CoralJellyfish {
253
+ return {
254
+ x: 0.1 + MULBERRY.next() * 0.8,
255
+ y: initialSpread ? 0.1 + MULBERRY.next() * 0.7 : 1.15 + MULBERRY.next() * 0.1,
256
+ size: 20 + MULBERRY.next() * 30,
257
+ phase: MULBERRY.next() * Math.PI * 2,
258
+ speed: 0.3 + MULBERRY.next() * 0.7,
259
+ tentacles: 4 + Math.floor(MULBERRY.next() * 4),
260
+ color: this.#colors[Math.floor(MULBERRY.next() * this.#colors.length)],
261
+ pulsePhase: MULBERRY.next() * Math.PI * 2,
262
+ drift: 0.3 + MULBERRY.next() * 0.5
263
+ };
264
+ }
265
+
266
+ #createBubble(initialSpread: boolean): CoralBubble {
267
+ return {
268
+ x: MULBERRY.next(),
269
+ y: initialSpread ? MULBERRY.next() : 1.05 + MULBERRY.next() * 0.1,
270
+ size: 2 + MULBERRY.next() * 5,
271
+ speed: 0.3 + MULBERRY.next() * 0.7,
272
+ wobblePhase: MULBERRY.next() * Math.PI * 2,
273
+ opacity: 0.2 + MULBERRY.next() * 0.4
274
+ };
275
+ }
276
+ }
@@ -0,0 +1,31 @@
1
+ export type CoralAnemone = {
2
+ x: number;
3
+ baseY: number;
4
+ segments: number;
5
+ segmentLength: number;
6
+ phase: number;
7
+ speed: number;
8
+ color: string;
9
+ width: number;
10
+ };
11
+
12
+ export type CoralJellyfish = {
13
+ x: number;
14
+ y: number;
15
+ size: number;
16
+ phase: number;
17
+ speed: number;
18
+ tentacles: number;
19
+ color: string;
20
+ pulsePhase: number;
21
+ drift: number;
22
+ };
23
+
24
+ export type CoralBubble = {
25
+ x: number;
26
+ y: number;
27
+ size: number;
28
+ speed: number;
29
+ wobblePhase: number;
30
+ opacity: number;
31
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,10 @@
1
+ import { Crystallization } from './layer';
2
+ import type { CrystallizationConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createCrystallization(config?: CrystallizationConfig): Effect<CrystallizationConfig> {
6
+ return new Crystallization(config);
7
+ }
8
+
9
+ export type { CrystallizationConfig };
10
+ export type { CrystalBranch, CrystalSeed } from './types';