@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,218 @@
1
+ import { parseColor } from '../color';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { RootSystem, RootTip } from './types';
5
+
6
+ export interface RootsConfig {
7
+ readonly count?: number;
8
+ readonly speed?: number;
9
+ readonly color?: string;
10
+ readonly branchProbability?: number;
11
+ readonly maxSegments?: number;
12
+ readonly scale?: number;
13
+ }
14
+
15
+ export class Roots extends Effect<RootsConfig> {
16
+ readonly #scale: number;
17
+ #speed: number;
18
+ readonly #colorR: number;
19
+ readonly #colorG: number;
20
+ readonly #colorB: number;
21
+ readonly #branchProbability: number;
22
+ readonly #maxSegments: number;
23
+ readonly #count: number;
24
+ #systems: RootSystem[] = [];
25
+ #initialized: boolean = false;
26
+ #width: number = 800;
27
+ #height: number = 600;
28
+
29
+ constructor(config: RootsConfig = {}) {
30
+ super();
31
+
32
+ this.#scale = config.scale ?? 1;
33
+ this.#speed = config.speed ?? 1;
34
+ this.#branchProbability = config.branchProbability ?? 0.3;
35
+ this.#maxSegments = config.maxSegments ?? 200;
36
+ this.#count = config.count ?? 5;
37
+
38
+ const parsed = parseColor(config.color ?? '#4a3728');
39
+ this.#colorR = parsed.r;
40
+ this.#colorG = parsed.g;
41
+ this.#colorB = parsed.b;
42
+ }
43
+
44
+ configure(config: Partial<RootsConfig>): void {
45
+ if (config.speed !== undefined) {
46
+ this.#speed = config.speed;
47
+ }
48
+ }
49
+
50
+ onResize(width: number, height: number): void {
51
+ this.#width = width;
52
+ this.#height = height;
53
+
54
+ if (!this.#initialized && width > 0 && height > 0) {
55
+ this.#initialized = true;
56
+ this.#systems = [];
57
+ for (let i = 0; i < this.#count; i++) {
58
+ this.#systems.push(this.#createSystem(width, height));
59
+ }
60
+ }
61
+ }
62
+
63
+ tick(dt: number, width: number, height: number): void {
64
+ this.#width = width;
65
+ this.#height = height;
66
+
67
+ const growSteps = Math.ceil(this.#speed * dt / 16 * 2);
68
+
69
+ for (const system of this.#systems) {
70
+ if (system.phase === 'growing') {
71
+ for (let step = 0; step < growSteps; step++) {
72
+ this.#growSystem(system);
73
+ }
74
+ } else {
75
+ system.opacity -= 0.04 * this.#speed * dt / 16;
76
+ if (system.opacity <= 0) {
77
+ this.#resetSystem(system, width, height);
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ draw(ctx: CanvasRenderingContext2D, _width: number, _height: number): void {
84
+ for (const system of this.#systems) {
85
+ if (system.opacity <= 0) {
86
+ continue;
87
+ }
88
+
89
+ ctx.globalAlpha = system.opacity;
90
+ this.#drawSystem(ctx, system);
91
+ }
92
+
93
+ ctx.globalAlpha = 1;
94
+ ctx.resetTransform();
95
+ }
96
+
97
+ #growSystem(system: RootSystem): void {
98
+ if (system.segmentCount >= this.#maxSegments) {
99
+ system.phase = 'fading';
100
+ return;
101
+ }
102
+
103
+ const activeTips = system.tips.filter(tip => tip.alive);
104
+ if (activeTips.length === 0) {
105
+ system.phase = 'fading';
106
+ return;
107
+ }
108
+
109
+ for (const tip of activeTips) {
110
+ // Grow upward with slight deviation
111
+ const angleDeviation = (MULBERRY.next() - 0.5) * 0.6;
112
+ tip.angle += angleDeviation;
113
+
114
+ // Keep growing upward (bias)
115
+ tip.angle = tip.angle * 0.85 + (-Math.PI / 2) * 0.15;
116
+
117
+ const stepSize = (3 + MULBERRY.next() * 4) * this.#scale;
118
+ const newX = tip.x + Math.cos(tip.angle) * stepSize;
119
+ const newY = tip.y + Math.sin(tip.angle) * stepSize;
120
+
121
+ tip.points.push({ x: newX, y: newY });
122
+ tip.x = newX;
123
+ tip.y = newY;
124
+ system.segmentCount++;
125
+
126
+ // Branch
127
+ if (tip.depth < 6 && MULBERRY.next() < this.#branchProbability * 0.04 && system.segmentCount < this.#maxSegments * 0.8) {
128
+ const branchAngle = tip.angle + (MULBERRY.next() > 0.5 ? 1 : -1) * (0.3 + MULBERRY.next() * 0.5);
129
+ const newTip: RootTip = {
130
+ x: tip.x,
131
+ y: tip.y,
132
+ angle: branchAngle,
133
+ depth: tip.depth + 1,
134
+ points: [{ x: tip.x, y: tip.y }],
135
+ alive: true,
136
+ lineWidth: Math.max(0.5, tip.lineWidth * 0.7),
137
+ colorVariant: MULBERRY.next() * 0.3
138
+ };
139
+ system.tips.push(newTip);
140
+ system.allTips.push(newTip);
141
+ }
142
+
143
+ // Kill tips that go off screen
144
+ if (newX < -50 || newX > this.#width + 50 || newY < -50) {
145
+ tip.alive = false;
146
+ }
147
+ }
148
+ }
149
+
150
+ #drawSystem(ctx: CanvasRenderingContext2D, system: RootSystem): void {
151
+ const r = this.#colorR;
152
+ const g = this.#colorG;
153
+ const b = this.#colorB;
154
+
155
+ for (const tip of system.allTips) {
156
+ if (tip.points.length < 2) {
157
+ continue;
158
+ }
159
+
160
+ const darkness = tip.colorVariant;
161
+ const cr = Math.max(0, r - darkness * 40);
162
+ const cg = Math.max(0, g - darkness * 30);
163
+ const cb = Math.max(0, b - darkness * 20);
164
+
165
+ ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`;
166
+ ctx.lineWidth = tip.lineWidth * this.#scale;
167
+ ctx.lineCap = 'round';
168
+ ctx.lineJoin = 'round';
169
+
170
+ ctx.beginPath();
171
+ ctx.moveTo(tip.points[0].x, tip.points[0].y);
172
+
173
+ for (let i = 1; i < tip.points.length - 1; i++) {
174
+ const mx = (tip.points[i].x + tip.points[i + 1].x) / 2;
175
+ const my = (tip.points[i].y + tip.points[i + 1].y) / 2;
176
+ ctx.quadraticCurveTo(tip.points[i].x, tip.points[i].y, mx, my);
177
+ }
178
+
179
+ const last = tip.points[tip.points.length - 1];
180
+ ctx.lineTo(last.x, last.y);
181
+ ctx.stroke();
182
+ }
183
+ }
184
+
185
+ #createSystem(width: number, height: number): RootSystem {
186
+ const startX = width * (0.2 + MULBERRY.next() * 0.6);
187
+ const startY = height + 10;
188
+ const baseWidth = 3 + MULBERRY.next() * 3;
189
+
190
+ const rootTip: RootTip = {
191
+ x: startX,
192
+ y: startY,
193
+ angle: -Math.PI / 2 + (MULBERRY.next() - 0.5) * 0.3,
194
+ depth: 0,
195
+ points: [{ x: startX, y: startY }],
196
+ alive: true,
197
+ lineWidth: baseWidth,
198
+ colorVariant: MULBERRY.next() * 0.2
199
+ };
200
+
201
+ return {
202
+ tips: [rootTip],
203
+ allTips: [rootTip],
204
+ segmentCount: 0,
205
+ phase: 'growing',
206
+ opacity: 0.8 + MULBERRY.next() * 0.2
207
+ };
208
+ }
209
+
210
+ #resetSystem(system: RootSystem, width: number, height: number): void {
211
+ const newSystem = this.#createSystem(width, height);
212
+ system.tips = newSystem.tips;
213
+ system.allTips = newSystem.allTips;
214
+ system.segmentCount = 0;
215
+ system.phase = 'growing';
216
+ system.opacity = 0.8 + MULBERRY.next() * 0.2;
217
+ }
218
+ }
@@ -0,0 +1,23 @@
1
+ export type RootPoint = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+
6
+ export type RootTip = {
7
+ x: number;
8
+ y: number;
9
+ angle: number;
10
+ depth: number;
11
+ points: RootPoint[];
12
+ alive: boolean;
13
+ lineWidth: number;
14
+ colorVariant: number;
15
+ };
16
+
17
+ export type RootSystem = {
18
+ tips: RootTip[];
19
+ allTips: RootTip[];
20
+ segmentCount: number;
21
+ phase: 'growing' | 'fading';
22
+ opacity: number;
23
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(7);
@@ -0,0 +1,9 @@
1
+ import { Smoke } from './layer';
2
+ import type { SmokeConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createSmoke(config?: SmokeConfig): Effect<SmokeConfig> {
6
+ return new Smoke(config);
7
+ }
8
+
9
+ export type { SmokeConfig };
@@ -0,0 +1,182 @@
1
+ import { parseColor } from '../color';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { SmokeParticle } from './types';
5
+
6
+ export interface SmokeConfig {
7
+ readonly color?: string;
8
+ readonly count?: number;
9
+ readonly scale?: number;
10
+ readonly speed?: number;
11
+ readonly spread?: number;
12
+ }
13
+
14
+ const SPRITE_SIZE = 128;
15
+ const SPRITE_CENTER = SPRITE_SIZE / 2;
16
+ const SPRITE_RADIUS = SPRITE_SIZE / 2;
17
+ const SPRITE_VARIANTS = 4;
18
+
19
+ export class Smoke extends Effect<SmokeConfig> {
20
+ readonly #scale: number;
21
+ #speed: number;
22
+ #count: number;
23
+ #spread: number;
24
+ #time: number = 0;
25
+ #particles: SmokeParticle[] = [];
26
+ #sprites: HTMLCanvasElement[] = [];
27
+ #colorR: number = 136;
28
+ #colorG: number = 136;
29
+ #colorB: number = 136;
30
+
31
+ constructor(config: SmokeConfig = {}) {
32
+ super();
33
+
34
+ this.#scale = config.scale ?? 1;
35
+ this.#speed = config.speed ?? 1;
36
+ this.#count = config.count ?? 40;
37
+ this.#spread = config.spread ?? 0.3;
38
+
39
+ const {r, g, b} = parseColor(config.color ?? '#888888');
40
+ this.#colorR = r;
41
+ this.#colorG = g;
42
+ this.#colorB = b;
43
+
44
+ if (innerWidth < 991) {
45
+ this.#count = Math.floor(this.#count / 2);
46
+ }
47
+
48
+ this.#sprites = this.#createSprites(r, g, b);
49
+
50
+ for (let i = 0; i < this.#count; ++i) {
51
+ this.#particles.push(this.#createParticle(true));
52
+ }
53
+ }
54
+
55
+ configure(config: Partial<SmokeConfig>): void {
56
+ if (config.speed !== undefined) {
57
+ this.#speed = config.speed;
58
+ }
59
+
60
+ if (config.spread !== undefined) {
61
+ this.#spread = config.spread;
62
+ }
63
+
64
+ if (config.color !== undefined) {
65
+ const {r, g, b} = parseColor(config.color);
66
+ this.#colorR = r;
67
+ this.#colorG = g;
68
+ this.#colorB = b;
69
+ this.#sprites = this.#createSprites(r, g, b);
70
+ }
71
+ }
72
+
73
+ tick(dt: number, _width: number, _height: number): void {
74
+ this.#time += 0.008 * dt;
75
+
76
+ for (let index = 0; index < this.#particles.length; index++) {
77
+ const particle = this.#particles[index];
78
+
79
+ particle.age += dt;
80
+
81
+ const progress = particle.age / particle.lifetime;
82
+
83
+ const turbulence = Math.sin(this.#time * particle.turbulenceSpeed + particle.turbulenceOffset) * 0.0002
84
+ + Math.sin(this.#time * particle.turbulenceSpeed * 1.7 + particle.turbulenceOffset + 2.1) * 0.0001;
85
+
86
+ particle.x += (particle.vx + turbulence) * dt;
87
+ particle.y += particle.vy * dt;
88
+ particle.radius = particle.maxRadius * Math.min(1, progress * 3);
89
+ particle.opacity = progress < 0.15
90
+ ? progress / 0.15 * 0.35
91
+ : (1 - progress) * 0.35;
92
+
93
+ if (particle.age >= particle.lifetime || particle.y < -0.3) {
94
+ this.#particles[index] = this.#createParticle(false);
95
+ }
96
+ }
97
+ }
98
+
99
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
100
+ ctx.globalCompositeOperation = 'screen';
101
+
102
+ for (const particle of this.#particles) {
103
+ const px = particle.x * width;
104
+ const py = particle.y * height;
105
+ const displayRadius = particle.radius * this.#scale * Math.min(width, height) * 0.5;
106
+
107
+ if (displayRadius < 1) {
108
+ continue;
109
+ }
110
+
111
+ ctx.globalAlpha = particle.opacity;
112
+ ctx.drawImage(
113
+ this.#sprites[particle.spriteIndex],
114
+ px - displayRadius,
115
+ py - displayRadius,
116
+ displayRadius * 2,
117
+ displayRadius * 2
118
+ );
119
+ }
120
+
121
+ ctx.globalCompositeOperation = 'source-over';
122
+ ctx.globalAlpha = 1;
123
+ ctx.resetTransform();
124
+ }
125
+
126
+ #createSprites(r: number, g: number, b: number): HTMLCanvasElement[] {
127
+ const sprites: HTMLCanvasElement[] = [];
128
+
129
+ for (let variant = 0; variant < SPRITE_VARIANTS; variant++) {
130
+ const canvas = document.createElement('canvas');
131
+ canvas.width = SPRITE_SIZE;
132
+ canvas.height = SPRITE_SIZE;
133
+ const spriteCtx = canvas.getContext('2d')!;
134
+
135
+ const offsets = [
136
+ {dx: 0, dy: 0, r: SPRITE_RADIUS},
137
+ {dx: SPRITE_RADIUS * 0.15, dy: -SPRITE_RADIUS * 0.1, r: SPRITE_RADIUS * 0.85},
138
+ {dx: -SPRITE_RADIUS * 0.1, dy: SPRITE_RADIUS * 0.05, r: SPRITE_RADIUS * 0.7}
139
+ ];
140
+
141
+ for (const offset of offsets) {
142
+ const cx = SPRITE_CENTER + offset.dx;
143
+ const cy = SPRITE_CENTER + offset.dy;
144
+ const gradient = spriteCtx.createRadialGradient(cx, cy, 0, cx, cy, offset.r);
145
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.25)`);
146
+ gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.12)`);
147
+ gradient.addColorStop(0.75, `rgba(${r}, ${g}, ${b}, 0.03)`);
148
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
149
+
150
+ spriteCtx.fillStyle = gradient;
151
+ spriteCtx.beginPath();
152
+ spriteCtx.arc(cx, cy, offset.r, 0, Math.PI * 2);
153
+ spriteCtx.fill();
154
+ }
155
+
156
+ sprites.push(canvas);
157
+ }
158
+
159
+ return sprites;
160
+ }
161
+
162
+ #createParticle(initialSpread: boolean): SmokeParticle {
163
+ const lifetime = (4 + MULBERRY.next() * 6) * (1 / this.#speed) * 60;
164
+ const startX = 0.5 + (MULBERRY.next() - 0.5) * this.#spread;
165
+ const startY = initialSpread ? 0.6 + MULBERRY.next() * 0.5 : 1.05;
166
+
167
+ return {
168
+ x: startX,
169
+ y: startY,
170
+ vx: (MULBERRY.next() - 0.5) * 0.0002,
171
+ vy: -(0.0008 + MULBERRY.next() * 0.001) * this.#speed,
172
+ age: initialSpread ? MULBERRY.next() * lifetime : 0,
173
+ lifetime,
174
+ radius: 0,
175
+ maxRadius: 0.15 + MULBERRY.next() * 0.25,
176
+ opacity: 0,
177
+ turbulenceOffset: MULBERRY.next() * Math.PI * 2,
178
+ turbulenceSpeed: 0.5 + MULBERRY.next() * 1.5,
179
+ spriteIndex: Math.floor(MULBERRY.next() * SPRITE_VARIANTS)
180
+ };
181
+ }
182
+ }
@@ -0,0 +1,14 @@
1
+ export interface SmokeParticle {
2
+ x: number;
3
+ y: number;
4
+ vx: number;
5
+ vy: number;
6
+ age: number;
7
+ lifetime: number;
8
+ radius: number;
9
+ maxRadius: number;
10
+ opacity: number;
11
+ turbulenceOffset: number;
12
+ turbulenceSpeed: number;
13
+ spriteIndex: number;
14
+ }
package/src/snow/layer.ts CHANGED
@@ -119,7 +119,8 @@ export class Snow extends Effect<SnowConfig> {
119
119
  if (snowflake.spriteIndex === 3) {
120
120
  const cos = Math.cos(snowflake.rotation);
121
121
  const sin = Math.sin(snowflake.rotation);
122
- ctx.setTransform(cos, sin, -sin, cos, px, py);
122
+ ctx.save();
123
+ ctx.transform(cos, sin, -sin, cos, px, py);
123
124
  ctx.drawImage(
124
125
  this.#sprites[snowflake.spriteIndex],
125
126
  -displayRadius,
@@ -127,7 +128,7 @@ export class Snow extends Effect<SnowConfig> {
127
128
  displaySize,
128
129
  displaySize
129
130
  );
130
- ctx.resetTransform();
131
+ ctx.restore();
131
132
  } else {
132
133
  ctx.drawImage(
133
134
  this.#sprites[snowflake.spriteIndex],
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,9 @@
1
+ import { Topography } from './layer';
2
+ import type { TopographyConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createTopography(config?: TopographyConfig): Effect<TopographyConfig> {
6
+ return new Topography(config);
7
+ }
8
+
9
+ export type { TopographyConfig };
@@ -0,0 +1,141 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+
4
+ export interface TopographyConfig {
5
+ readonly speed?: number;
6
+ readonly scale?: number;
7
+ readonly resolution?: number;
8
+ readonly contourSpacing?: number;
9
+ readonly lineWidth?: number;
10
+ readonly color?: string;
11
+ }
12
+
13
+ export class Topography extends Effect<TopographyConfig> {
14
+ #speed: number;
15
+ #scale: number;
16
+ readonly #resolution: number;
17
+ readonly #contourSpacing: number;
18
+ readonly #lineWidth: number;
19
+ readonly #colorR: number;
20
+ readonly #colorG: number;
21
+ readonly #colorB: number;
22
+ #time: number = 0;
23
+ #offscreen: HTMLCanvasElement | null = null;
24
+ #offscreenCtx: CanvasRenderingContext2D | null = null;
25
+ #imageData: ImageData | null = null;
26
+
27
+ constructor(config: TopographyConfig = {}) {
28
+ super();
29
+
30
+ this.#speed = config.speed ?? 0.5;
31
+ this.#scale = config.scale ?? 1;
32
+ this.#resolution = config.resolution ?? 2;
33
+ this.#contourSpacing = config.contourSpacing ?? 0.1;
34
+ this.#lineWidth = config.lineWidth ?? 1.5;
35
+
36
+ const [cr, cg, cb] = hexToRGB(config.color ?? '#2d5016');
37
+ this.#colorR = cr;
38
+ this.#colorG = cg;
39
+ this.#colorB = cb;
40
+ }
41
+
42
+ configure(config: Partial<TopographyConfig>): void {
43
+ if (config.speed !== undefined) {
44
+ this.#speed = config.speed;
45
+ }
46
+ if (config.scale !== undefined) {
47
+ this.#scale = config.scale;
48
+ }
49
+ }
50
+
51
+ tick(dt: number, _width: number, _height: number): void {
52
+ this.#time += 0.02 * dt * this.#speed;
53
+ }
54
+
55
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
56
+ const resolution = this.#resolution;
57
+ const offWidth = Math.ceil(width / resolution);
58
+ const offHeight = Math.ceil(height / resolution);
59
+
60
+ if (!this.#offscreen || this.#offscreen.width !== offWidth || this.#offscreen.height !== offHeight) {
61
+ this.#offscreen = document.createElement('canvas');
62
+ this.#offscreen.width = offWidth;
63
+ this.#offscreen.height = offHeight;
64
+ this.#offscreenCtx = this.#offscreen.getContext('2d');
65
+ this.#imageData = this.#offscreenCtx!.createImageData(offWidth, offHeight);
66
+ }
67
+
68
+ const data = this.#imageData!.data;
69
+ const time = this.#time;
70
+ const scale = this.#scale;
71
+ const spacing = this.#contourSpacing;
72
+ const lineWidth = this.#lineWidth;
73
+ const colorR = this.#colorR;
74
+ const colorG = this.#colorG;
75
+ const colorB = this.#colorB;
76
+
77
+ const freq1 = 80 * scale;
78
+ const freq2 = 50 * scale;
79
+ const freq3 = 30 * scale;
80
+
81
+ const heightField = new Float32Array(offWidth * offHeight);
82
+
83
+ for (let py = 0; py < offHeight; py++) {
84
+ const worldY = py * resolution;
85
+
86
+ for (let px = 0; px < offWidth; px++) {
87
+ const worldX = px * resolution;
88
+
89
+ const value = Math.sin(worldX / freq1 + time) * Math.sin(worldY / freq1 + time * 0.7)
90
+ + 0.5 * Math.sin(worldX / freq2 + worldY / freq2 + time * 1.3)
91
+ + 0.25 * Math.sin(worldX / freq3 - time * 0.5) * Math.sin(worldY / freq3 + time * 0.9);
92
+
93
+ heightField[py * offWidth + px] = value;
94
+ }
95
+ }
96
+
97
+ for (let py = 0; py < offHeight; py++) {
98
+ for (let px = 0; px < offWidth; px++) {
99
+ const index = py * offWidth + px;
100
+ const value = heightField[index];
101
+ const contourLevel = Math.floor(value / spacing);
102
+
103
+ let isContour = false;
104
+
105
+ if (px < offWidth - 1) {
106
+ const rightLevel = Math.floor(heightField[index + 1] / spacing);
107
+ if (contourLevel !== rightLevel) {
108
+ isContour = true;
109
+ }
110
+ }
111
+
112
+ if (!isContour && py < offHeight - 1) {
113
+ const belowLevel = Math.floor(heightField[index + offWidth] / spacing);
114
+ if (contourLevel !== belowLevel) {
115
+ isContour = true;
116
+ }
117
+ }
118
+
119
+ const offset = index * 4;
120
+
121
+ if (isContour) {
122
+ const alpha = Math.min(255, 255 * lineWidth);
123
+ data[offset] = colorR;
124
+ data[offset + 1] = colorG;
125
+ data[offset + 2] = colorB;
126
+ data[offset + 3] = alpha;
127
+ } else {
128
+ data[offset] = colorR;
129
+ data[offset + 1] = colorG;
130
+ data[offset + 2] = colorB;
131
+ data[offset + 3] = 10;
132
+ }
133
+ }
134
+ }
135
+
136
+ this.#offscreenCtx!.putImageData(this.#imageData!, 0, 0);
137
+
138
+ ctx.imageSmoothingEnabled = true;
139
+ ctx.drawImage(this.#offscreen!, 0, 0, width, height);
140
+ }
141
+ }
@@ -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 { Tornado } from './layer';
2
+ import type { TornadoConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createTornado(config?: TornadoConfig): Effect<TornadoConfig> {
6
+ return new Tornado(config);
7
+ }
8
+
9
+ export type { TornadoConfig };
10
+ export type { TornadoDebris, TornadoParticle } from './types';