@basmilius/sparkle 2.4.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,167 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { HyperSpaceStar } from './types';
4
+
5
+ export interface HyperSpaceConfig {
6
+ readonly count?: number;
7
+ readonly speed?: number;
8
+ readonly color?: string;
9
+ readonly scale?: number;
10
+ }
11
+
12
+ export class HyperSpace extends Effect<HyperSpaceConfig> {
13
+ readonly #scale: number;
14
+ #speed: number;
15
+ #colorR: number;
16
+ #colorG: number;
17
+ #colorB: number;
18
+ #stars: HyperSpaceStar[] = [];
19
+ #maxCount: number;
20
+ #width: number = 960;
21
+ #height: number = 540;
22
+ #initialized: boolean = false;
23
+
24
+ constructor(config: HyperSpaceConfig = {}) {
25
+ super();
26
+
27
+ this.#scale = config.scale ?? 1;
28
+ this.#speed = config.speed ?? 1;
29
+ this.#maxCount = config.count ?? 250;
30
+
31
+ const parsed = this.#parseHex(config.color ?? '#ffffff');
32
+ this.#colorR = parsed[0];
33
+ this.#colorG = parsed[1];
34
+ this.#colorB = parsed[2];
35
+
36
+ if (typeof globalThis.innerWidth !== 'undefined' && globalThis.innerWidth < 991) {
37
+ this.#maxCount = Math.floor(this.#maxCount / 2);
38
+ }
39
+ }
40
+
41
+ configure(config: Partial<HyperSpaceConfig>): void {
42
+ if (config.speed !== undefined) {
43
+ this.#speed = config.speed;
44
+ }
45
+
46
+ if (config.color !== undefined) {
47
+ const parsed = this.#parseHex(config.color);
48
+ this.#colorR = parsed[0];
49
+ this.#colorG = parsed[1];
50
+ this.#colorB = parsed[2];
51
+ }
52
+ }
53
+
54
+ onResize(width: number, height: number): void {
55
+ this.#width = width;
56
+ this.#height = height;
57
+
58
+ if (!this.#initialized && width > 0 && height > 0) {
59
+ this.#initialized = true;
60
+ this.#stars = [];
61
+
62
+ for (let i = 0; i < this.#maxCount; ++i) {
63
+ this.#stars.push(this.#createStar(true));
64
+ }
65
+ }
66
+ }
67
+
68
+ tick(dt: number, width: number, height: number): void {
69
+ this.#width = width;
70
+ this.#height = height;
71
+
72
+ const maxRadius = Math.sqrt((width / 2) ** 2 + (height / 2) ** 2);
73
+
74
+ for (let i = 0; i < this.#stars.length; ++i) {
75
+ const star = this.#stars[i];
76
+
77
+ star.prevRadius = star.radius;
78
+
79
+ const acceleration = 1 + (star.radius / maxRadius) * 4;
80
+ star.radius += star.speed * this.#speed * acceleration * dt * 0.3 * this.#scale;
81
+
82
+ if (star.radius >= maxRadius + 10) {
83
+ this.#stars[i] = this.#createStar(false);
84
+ }
85
+ }
86
+ }
87
+
88
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
89
+ const cx = width / 2;
90
+ const cy = height / 2;
91
+ const maxRadius = Math.sqrt(cx * cx + cy * cy);
92
+ const cr = this.#colorR;
93
+ const cg = this.#colorG;
94
+ const cb = this.#colorB;
95
+
96
+ ctx.globalCompositeOperation = 'source-over';
97
+ ctx.globalAlpha = 1;
98
+ ctx.fillStyle = 'rgb(0, 0, 8)';
99
+ ctx.fillRect(0, 0, width, height);
100
+
101
+ ctx.globalCompositeOperation = 'lighter';
102
+
103
+ for (const star of this.#stars) {
104
+ const normalizedRadius = star.radius / maxRadius;
105
+ const alpha = Math.min(1, 0.1 + normalizedRadius * 0.9) * star.brightness;
106
+ const lineWidth = Math.max(0.5, this.#scale * (0.3 + normalizedRadius * 1.5));
107
+
108
+ const px = cx + Math.cos(star.angle) * star.radius;
109
+ const py = cy + Math.sin(star.angle) * star.radius;
110
+
111
+ const trailLength = Math.max(star.radius - star.prevRadius, 1);
112
+ const prevRadius = Math.max(0, star.prevRadius);
113
+ const tx = cx + Math.cos(star.angle) * prevRadius;
114
+ const ty = cy + Math.sin(star.angle) * prevRadius;
115
+
116
+ const gradient = ctx.createLinearGradient(tx, ty, px, py);
117
+ gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0)`);
118
+ gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, ${alpha})`);
119
+
120
+ ctx.globalAlpha = 1;
121
+ ctx.beginPath();
122
+ ctx.moveTo(tx, ty);
123
+ ctx.lineTo(px, py);
124
+ ctx.strokeStyle = gradient;
125
+ ctx.lineWidth = lineWidth;
126
+ ctx.lineCap = 'round';
127
+ ctx.stroke();
128
+
129
+ if (normalizedRadius > 0.5) {
130
+ ctx.globalAlpha = alpha * 0.8;
131
+ ctx.fillStyle = `rgb(${cr}, ${cg}, ${cb})`;
132
+ ctx.beginPath();
133
+ ctx.arc(px, py, lineWidth * 0.7, 0, Math.PI * 2);
134
+ ctx.fill();
135
+ }
136
+ }
137
+
138
+ ctx.globalAlpha = 1;
139
+ ctx.resetTransform();
140
+ ctx.globalCompositeOperation = 'source-over';
141
+ }
142
+
143
+ #createStar(spread: boolean): HyperSpaceStar {
144
+ const maxRadius = Math.sqrt((this.#width / 2) ** 2 + (this.#height / 2) ** 2);
145
+ const angle = MULBERRY.next() * Math.PI * 2;
146
+ const radius = spread ? MULBERRY.next() * maxRadius * 0.8 : MULBERRY.next() * maxRadius * 0.05;
147
+
148
+ return {
149
+ angle,
150
+ radius,
151
+ prevRadius: radius,
152
+ speed: 0.5 + MULBERRY.next() * 1.5,
153
+ size: 0.5 + MULBERRY.next() * 1.5,
154
+ brightness: 0.5 + MULBERRY.next() * 0.5
155
+ };
156
+ }
157
+
158
+ #parseHex(hex: string): [number, number, number] {
159
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
160
+
161
+ if (!result) {
162
+ return [255, 255, 255];
163
+ }
164
+
165
+ return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
166
+ }
167
+ }
@@ -0,0 +1,8 @@
1
+ export interface HyperSpaceStar {
2
+ angle: number;
3
+ radius: number;
4
+ speed: number;
5
+ prevRadius: number;
6
+ size: number;
7
+ brightness: number;
8
+ }
package/src/index.ts CHANGED
@@ -1,31 +1,60 @@
1
1
  export * from './aurora';
2
2
  export * from './balloons';
3
+ export * from './black-hole';
4
+ export * from './boids';
3
5
  export * from './bubbles';
6
+ export * from './butterflies';
4
7
  export * from './canvas';
8
+ export * from './caustics';
9
+ export * from './clouds';
5
10
  export * from './color';
6
11
  export * from './confetti';
12
+ export * from './constellation';
13
+ export * from './coral-reef';
14
+ export * from './crystallization';
15
+ export * from './digital-rain';
7
16
  export * from './donuts';
8
17
  export * from './effect';
9
18
  export * from './fireflies';
10
19
  export * from './firepit';
11
20
  export * from './fireworks';
21
+ export * from './glitch';
12
22
  export * from './glitter';
23
+ export * from './gradient-flow';
24
+ export * from './hologram';
25
+ export * from './hyper-space';
26
+ export * from './interference';
27
+ export * from './kaleidoscope';
13
28
  export * from './lanterns';
29
+ export * from './lava';
14
30
  export * from './leaves';
15
31
  export * from './lightning';
16
32
  export * from './matrix';
33
+ export * from './murmuration';
34
+ export * from './nebula';
35
+ export * from './neon';
17
36
  export * from './orbits';
18
37
  export * from './particles';
19
38
  export * from './petals';
20
39
  export * from './plasma';
40
+ export * from './pollen';
41
+ export * from './popcorn';
42
+ export * from './portal';
43
+ export * from './pulse-grid';
21
44
  export * from './rain';
45
+ export * from './roots';
22
46
  export * from './sandstorm';
23
47
  export * from './scene';
24
48
  export * from './shooting-stars';
49
+ export * from './smoke';
25
50
  export * from './snow';
26
51
  export * from './sparklers';
27
52
  export * from './stars';
28
53
  export * from './streamers';
54
+ export * from './topography';
55
+ export * from './tornado';
29
56
  export * from './trail';
57
+ export * from './voronoi';
58
+ export * from './volcano';
30
59
  export * from './waves';
31
60
  export * from './wormhole';
@@ -0,0 +1,9 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const DEFAULT_COLORS: string[] = [
6
+ '#ff0066',
7
+ '#0066ff',
8
+ '#00ff66'
9
+ ];
@@ -0,0 +1,9 @@
1
+ import { Interference } from './layer';
2
+ import type { InterferenceConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createInterference(config?: InterferenceConfig): Effect<InterferenceConfig> {
6
+ return new Interference(config);
7
+ }
8
+
9
+ export type { InterferenceConfig };
@@ -0,0 +1,129 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { DEFAULT_COLORS } from './consts';
4
+
5
+ export interface InterferenceConfig {
6
+ readonly speed?: number;
7
+ readonly scale?: number;
8
+ readonly resolution?: number;
9
+ readonly layers?: number;
10
+ readonly colors?: string[];
11
+ }
12
+
13
+ export class Interference extends Effect<InterferenceConfig> {
14
+ #speed: number;
15
+ #scale: number;
16
+ readonly #resolution: number;
17
+ readonly #layerCount: number;
18
+ readonly #colorRGBs: [number, number, number][];
19
+ #time: number = 0;
20
+ #offscreen: HTMLCanvasElement | null = null;
21
+ #offscreenCtx: CanvasRenderingContext2D | null = null;
22
+ #imageData: ImageData | null = null;
23
+ #centers: { x: number; y: number; freqX: number; freqY: number; phaseX: number; phaseY: number; speedMul: number }[];
24
+
25
+ constructor(config: InterferenceConfig = {}) {
26
+ super();
27
+
28
+ this.#speed = config.speed ?? 1;
29
+ this.#scale = config.scale ?? 1;
30
+ this.#resolution = config.resolution ?? 3;
31
+ this.#layerCount = config.layers ?? 3;
32
+
33
+ const colors = config.colors ?? DEFAULT_COLORS;
34
+ this.#colorRGBs = colors.map(c => hexToRGB(c));
35
+
36
+ this.#centers = [];
37
+
38
+ for (let idx = 0; idx < this.#layerCount; idx++) {
39
+ this.#centers.push({
40
+ x: 0,
41
+ y: 0,
42
+ freqX: 0.3 + idx * 0.17,
43
+ freqY: 0.2 + idx * 0.13,
44
+ phaseX: idx * Math.PI * 2 / this.#layerCount,
45
+ phaseY: idx * Math.PI * 1.3 / this.#layerCount,
46
+ speedMul: 0.7 + idx * 0.3
47
+ });
48
+ }
49
+ }
50
+
51
+ configure(config: Partial<InterferenceConfig>): void {
52
+ if (config.speed !== undefined) {
53
+ this.#speed = config.speed;
54
+ }
55
+ if (config.scale !== undefined) {
56
+ this.#scale = config.scale;
57
+ }
58
+ }
59
+
60
+ tick(dt: number, width: number, height: number): void {
61
+ this.#time += 0.015 * dt * this.#speed;
62
+
63
+ for (let idx = 0; idx < this.#centers.length; idx++) {
64
+ const center = this.#centers[idx];
65
+ center.x = (0.5 + 0.35 * Math.sin(this.#time * center.freqX + center.phaseX)) * width;
66
+ center.y = (0.5 + 0.35 * Math.cos(this.#time * center.freqY + center.phaseY)) * height;
67
+ }
68
+ }
69
+
70
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
71
+ const resolution = this.#resolution;
72
+ const offWidth = Math.ceil(width / resolution);
73
+ const offHeight = Math.ceil(height / resolution);
74
+
75
+ if (!this.#offscreen || this.#offscreen.width !== offWidth || this.#offscreen.height !== offHeight) {
76
+ this.#offscreen = document.createElement('canvas');
77
+ this.#offscreen.width = offWidth;
78
+ this.#offscreen.height = offHeight;
79
+ this.#offscreenCtx = this.#offscreen.getContext('2d');
80
+ this.#imageData = this.#offscreenCtx!.createImageData(offWidth, offHeight);
81
+ }
82
+
83
+ const data = this.#imageData!.data;
84
+ const scale = this.#scale;
85
+ const freq = 30 * scale;
86
+ const centers = this.#centers;
87
+ const centerCount = centers.length;
88
+ const colorRGBs = this.#colorRGBs;
89
+ const colorCount = colorRGBs.length;
90
+
91
+ for (let py = 0; py < offHeight; py++) {
92
+ const worldY = py * resolution;
93
+
94
+ for (let px = 0; px < offWidth; px++) {
95
+ const worldX = px * resolution;
96
+
97
+ let totalR = 0;
98
+ let totalG = 0;
99
+ let totalB = 0;
100
+
101
+ for (let ci = 0; ci < centerCount; ci++) {
102
+ const center = centers[ci];
103
+ const dx = worldX - center.x;
104
+ const dy = worldY - center.y;
105
+ const dist = Math.sqrt(dx * dx + dy * dy);
106
+
107
+ const wave = (Math.sin(dist / freq) + 1) * 0.5;
108
+ const color = colorRGBs[ci % colorCount];
109
+
110
+ totalR += color[0] * wave;
111
+ totalG += color[1] * wave;
112
+ totalB += color[2] * wave;
113
+ }
114
+
115
+ const maxVal = centerCount * 255;
116
+ const offset = (py * offWidth + px) * 4;
117
+ data[offset] = Math.min(255, (totalR / maxVal) * 255) | 0;
118
+ data[offset + 1] = Math.min(255, (totalG / maxVal) * 255) | 0;
119
+ data[offset + 2] = Math.min(255, (totalB / maxVal) * 255) | 0;
120
+ data[offset + 3] = 255;
121
+ }
122
+ }
123
+
124
+ this.#offscreenCtx!.putImageData(this.#imageData!, 0, 0);
125
+
126
+ ctx.imageSmoothingEnabled = true;
127
+ ctx.drawImage(this.#offscreen!, 0, 0, width, height);
128
+ }
129
+ }
@@ -0,0 +1,12 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const DEFAULT_COLORS: string[] = [
6
+ '#ff3366',
7
+ '#33ccff',
8
+ '#ffcc00',
9
+ '#66ff66',
10
+ '#cc66ff',
11
+ '#ff9933'
12
+ ];
@@ -0,0 +1,9 @@
1
+ import { Kaleidoscope } from './layer';
2
+ import type { KaleidoscopeConfig } from './types';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createKaleidoscope(config?: KaleidoscopeConfig): Effect<KaleidoscopeConfig> {
6
+ return new Kaleidoscope(config);
7
+ }
8
+
9
+ export type { KaleidoscopeConfig, KaleidoscopeShape } from './types';
@@ -0,0 +1,213 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { DEFAULT_COLORS, MULBERRY } from './consts';
4
+ import type { KaleidoscopeConfig, KaleidoscopeShape } from './types';
5
+
6
+ export class Kaleidoscope extends Effect<KaleidoscopeConfig> {
7
+ #segments: number;
8
+ #speed: number;
9
+ #shapeCount: number;
10
+ #colors: string[];
11
+ #scale: number;
12
+ #shapes: KaleidoscopeShape[] = [];
13
+ #sourceCanvas: HTMLCanvasElement | null = null;
14
+ #sourceCtx: CanvasRenderingContext2D | null = null;
15
+ #sourceRadius: number = 0;
16
+ #time: number = 0;
17
+ #initialized: boolean = false;
18
+
19
+ constructor(config: KaleidoscopeConfig = {}) {
20
+ super();
21
+
22
+ this.#segments = config.segments ?? 8;
23
+ this.#speed = config.speed ?? 1;
24
+ this.#shapeCount = config.shapes ?? 15;
25
+ this.#colors = config.colors ?? DEFAULT_COLORS;
26
+ this.#scale = config.scale ?? 1;
27
+ }
28
+
29
+ configure(config: Partial<KaleidoscopeConfig>): void {
30
+ if (config.speed !== undefined) {
31
+ this.#speed = config.speed;
32
+ }
33
+ }
34
+
35
+ onResize(width: number, height: number): void {
36
+ this.#sourceRadius = Math.min(width, height) / 2;
37
+
38
+ const size = Math.ceil(this.#sourceRadius * 2);
39
+ this.#sourceCanvas = document.createElement('canvas');
40
+ this.#sourceCanvas.width = size;
41
+ this.#sourceCanvas.height = size;
42
+ this.#sourceCtx = this.#sourceCanvas.getContext('2d')!;
43
+
44
+ if (!this.#initialized) {
45
+ this.#initialized = true;
46
+ this.#shapes = [];
47
+
48
+ for (let index = 0; index < this.#shapeCount; index++) {
49
+ this.#shapes.push(this.#createShape());
50
+ }
51
+ }
52
+ }
53
+
54
+ tick(dt: number, _width: number, _height: number): void {
55
+ const speedMul = this.#speed * dt;
56
+ this.#time += 0.02 * speedMul;
57
+
58
+ const radius = this.#sourceRadius * 0.8;
59
+
60
+ for (const shape of this.#shapes) {
61
+ shape.x += shape.vx * speedMul;
62
+ shape.y += shape.vy * speedMul;
63
+ shape.rotation += shape.rotationSpeed * speedMul;
64
+
65
+ const dist = Math.sqrt(shape.x * shape.x + shape.y * shape.y);
66
+
67
+ if (dist + shape.size > radius) {
68
+ const nx = shape.x / dist;
69
+ const ny = shape.y / dist;
70
+ const dot = shape.vx * nx + shape.vy * ny;
71
+ shape.vx -= 2 * dot * nx;
72
+ shape.vy -= 2 * dot * ny;
73
+
74
+ const overlap = dist + shape.size - radius;
75
+ shape.x -= nx * overlap;
76
+ shape.y -= ny * overlap;
77
+ }
78
+ }
79
+ }
80
+
81
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
82
+ if (!this.#sourceCanvas || !this.#sourceCtx) {
83
+ return;
84
+ }
85
+
86
+ const sourceCtx = this.#sourceCtx;
87
+ const radius = this.#sourceRadius;
88
+ const segmentAngle = (Math.PI * 2) / this.#segments;
89
+ const centerX = width / 2;
90
+ const centerY = height / 2;
91
+
92
+ sourceCtx.clearRect(0, 0, this.#sourceCanvas.width, this.#sourceCanvas.height);
93
+ sourceCtx.save();
94
+
95
+ sourceCtx.beginPath();
96
+ sourceCtx.moveTo(radius, radius);
97
+ sourceCtx.arc(radius, radius, radius, -segmentAngle / 2, segmentAngle / 2);
98
+ sourceCtx.closePath();
99
+ sourceCtx.clip();
100
+
101
+ for (const shape of this.#shapes) {
102
+ const px = radius + shape.x * this.#scale;
103
+ const py = radius + shape.y * this.#scale;
104
+ const size = shape.size * this.#scale;
105
+ const [cr, cg, cb] = hexToRGB(shape.color);
106
+
107
+ sourceCtx.save();
108
+ sourceCtx.translate(px, py);
109
+ sourceCtx.rotate(shape.rotation);
110
+
111
+ if (shape.type === 0) {
112
+ const gradient = sourceCtx.createRadialGradient(0, 0, 0, 0, 0, size);
113
+ gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0.9)`);
114
+ gradient.addColorStop(0.5, `rgba(${cr}, ${cg}, ${cb}, 0.5)`);
115
+ gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
116
+ sourceCtx.fillStyle = gradient;
117
+ sourceCtx.beginPath();
118
+ sourceCtx.arc(0, 0, size, 0, Math.PI * 2);
119
+ sourceCtx.fill();
120
+ } else if (shape.type === 1) {
121
+ sourceCtx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, 0.7)`;
122
+ sourceCtx.beginPath();
123
+ for (let point = 0; point < 6; point++) {
124
+ const angle = (point / 6) * Math.PI * 2;
125
+ const hx = Math.cos(angle) * size;
126
+ const hy = Math.sin(angle) * size;
127
+
128
+ if (point === 0) {
129
+ sourceCtx.moveTo(hx, hy);
130
+ } else {
131
+ sourceCtx.lineTo(hx, hy);
132
+ }
133
+ }
134
+ sourceCtx.closePath();
135
+ sourceCtx.fill();
136
+
137
+ sourceCtx.strokeStyle = `rgba(${Math.min(255, cr + 60)}, ${Math.min(255, cg + 60)}, ${Math.min(255, cb + 60)}, 0.5)`;
138
+ sourceCtx.lineWidth = 1.5;
139
+ sourceCtx.stroke();
140
+ } else if (shape.type === 2) {
141
+ const petalCount = 5;
142
+
143
+ sourceCtx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, 0.6)`;
144
+
145
+ for (let petal = 0; petal < petalCount; petal++) {
146
+ const angle = (petal / petalCount) * Math.PI * 2;
147
+ sourceCtx.save();
148
+ sourceCtx.rotate(angle);
149
+ sourceCtx.beginPath();
150
+ sourceCtx.ellipse(0, -size * 0.5, size * 0.3, size * 0.6, 0, 0, Math.PI * 2);
151
+ sourceCtx.fill();
152
+ sourceCtx.restore();
153
+ }
154
+
155
+ const coreGradient = sourceCtx.createRadialGradient(0, 0, 0, 0, 0, size * 0.25);
156
+ coreGradient.addColorStop(0, `rgba(255, 255, 255, 0.8)`);
157
+ coreGradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0.3)`);
158
+ sourceCtx.fillStyle = coreGradient;
159
+ sourceCtx.beginPath();
160
+ sourceCtx.arc(0, 0, size * 0.25, 0, Math.PI * 2);
161
+ sourceCtx.fill();
162
+ } else {
163
+ const gradient = sourceCtx.createRadialGradient(0, 0, size * 0.3, 0, 0, size);
164
+ gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0)`);
165
+ gradient.addColorStop(0.4, `rgba(${cr}, ${cg}, ${cb}, 0.6)`);
166
+ gradient.addColorStop(0.6, `rgba(${cr}, ${cg}, ${cb}, 0.6)`);
167
+ gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
168
+ sourceCtx.fillStyle = gradient;
169
+ sourceCtx.beginPath();
170
+ sourceCtx.arc(0, 0, size, 0, Math.PI * 2);
171
+ sourceCtx.fill();
172
+ }
173
+
174
+ sourceCtx.restore();
175
+ }
176
+
177
+ sourceCtx.restore();
178
+
179
+ for (let index = 0; index < this.#segments; index++) {
180
+ const angle = segmentAngle * index;
181
+ const mirrored = index % 2 === 1;
182
+
183
+ ctx.save();
184
+ ctx.translate(centerX, centerY);
185
+ ctx.rotate(angle);
186
+
187
+ if (mirrored) {
188
+ ctx.scale(1, -1);
189
+ }
190
+
191
+ ctx.drawImage(this.#sourceCanvas, -radius, -radius);
192
+ ctx.restore();
193
+ }
194
+ }
195
+
196
+ #createShape(): KaleidoscopeShape {
197
+ const maxDist = this.#sourceRadius * 0.6;
198
+ const angle = MULBERRY.next() * Math.PI * 2;
199
+ const dist = MULBERRY.next() * maxDist;
200
+
201
+ return {
202
+ x: Math.cos(angle) * dist,
203
+ y: Math.sin(angle) * dist,
204
+ vx: (MULBERRY.next() - 0.5) * 1.2,
205
+ vy: (MULBERRY.next() - 0.5) * 1.2,
206
+ size: 8 + MULBERRY.next() * 25,
207
+ color: this.#colors[Math.floor(MULBERRY.next() * this.#colors.length)],
208
+ type: Math.floor(MULBERRY.next() * 4),
209
+ rotation: MULBERRY.next() * Math.PI * 2,
210
+ rotationSpeed: (MULBERRY.next() - 0.5) * 0.06
211
+ };
212
+ }
213
+ }
@@ -0,0 +1,19 @@
1
+ export interface KaleidoscopeConfig {
2
+ readonly segments?: number;
3
+ readonly speed?: number;
4
+ readonly shapes?: number;
5
+ readonly colors?: string[];
6
+ readonly scale?: number;
7
+ }
8
+
9
+ export type KaleidoscopeShape = {
10
+ x: number;
11
+ y: number;
12
+ vx: number;
13
+ vy: number;
14
+ size: number;
15
+ color: string;
16
+ type: number;
17
+ rotation: number;
18
+ rotationSpeed: number;
19
+ };
@@ -88,7 +88,8 @@ export class Lanterns extends Effect<LanternsConfig> {
88
88
  ctx.arc(px, py, glowRadius, 0, Math.PI * 2);
89
89
  ctx.fill();
90
90
 
91
- ctx.setTransform(1, 0, 0, 1, px, py);
91
+ ctx.save();
92
+ ctx.transform(1, 0, 0, 1, px, py);
92
93
 
93
94
  const bodyW = size * 0.8;
94
95
  const bodyH = size;
@@ -152,7 +153,7 @@ export class Lanterns extends Effect<LanternsConfig> {
152
153
  ctx.lineWidth = size * 0.04;
153
154
  ctx.stroke();
154
155
 
155
- ctx.resetTransform();
156
+ ctx.restore();
156
157
  }
157
158
  }
158
159
 
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(37);
@@ -0,0 +1,9 @@
1
+ import { Lava } from './layer';
2
+ import type { LavaConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createLava(config?: LavaConfig): Effect<LavaConfig> {
6
+ return new Lava(config);
7
+ }
8
+
9
+ export type { LavaConfig };