@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@basmilius/sparkle",
3
3
  "license": "MIT",
4
- "version": "2.3.0",
4
+ "version": "2.5.0",
5
5
  "author": {
6
6
  "email": "bas@mili.us",
7
7
  "name": "Bas Milius",
@@ -90,7 +90,8 @@ export class Balloons extends Effect<BalloonsConfig> {
90
90
  const cos = Math.cos(balloon.rotation);
91
91
  const sin = Math.sin(balloon.rotation);
92
92
 
93
- ctx.setTransform(cos, sin, -sin, cos, px, py);
93
+ ctx.save();
94
+ ctx.transform(cos, sin, -sin, cos, px, py);
94
95
 
95
96
  const gradient = ctx.createRadialGradient(
96
97
  -rx * 0.3, -ry * 0.3, rx * 0.1,
@@ -142,9 +143,9 @@ export class Balloons extends Effect<BalloonsConfig> {
142
143
  ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.4)`;
143
144
  ctx.lineWidth = 1;
144
145
  ctx.stroke();
145
- }
146
146
 
147
- ctx.resetTransform();
147
+ ctx.restore();
148
+ }
148
149
  }
149
150
 
150
151
  #createBalloon(initialSpread: boolean): Balloon {
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(23);
@@ -0,0 +1,10 @@
1
+ import { BlackHole } from './layer';
2
+ import type { BlackHoleConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createBlackHole(config?: BlackHoleConfig): Effect<BlackHoleConfig> {
6
+ return new BlackHole(config);
7
+ }
8
+
9
+ export type { BlackHoleConfig };
10
+ export type { BlackHoleParticle } from './types';
@@ -0,0 +1,193 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { BlackHoleParticle } from './types';
4
+
5
+ export interface BlackHoleConfig {
6
+ readonly count?: number;
7
+ readonly speed?: number;
8
+ readonly color?: string;
9
+ readonly size?: number;
10
+ readonly scale?: number;
11
+ }
12
+
13
+ export class BlackHole extends Effect<BlackHoleConfig> {
14
+ readonly #scale: number;
15
+ #speed: number;
16
+ #colorR: number;
17
+ #colorG: number;
18
+ #colorB: number;
19
+ readonly #baseSize: number;
20
+ #particles: BlackHoleParticle[] = [];
21
+ #maxCount: number;
22
+ #width: number = 960;
23
+ #height: number = 540;
24
+ #initialized: boolean = false;
25
+
26
+ constructor(config: BlackHoleConfig = {}) {
27
+ super();
28
+
29
+ this.#scale = config.scale ?? 1;
30
+ this.#speed = config.speed ?? 1;
31
+ this.#baseSize = (config.size ?? 2) * this.#scale;
32
+ this.#maxCount = config.count ?? 300;
33
+
34
+ const parsed = this.#parseHex(config.color ?? '#6644ff');
35
+ this.#colorR = parsed[0];
36
+ this.#colorG = parsed[1];
37
+ this.#colorB = parsed[2];
38
+
39
+ if (typeof globalThis.innerWidth !== 'undefined' && globalThis.innerWidth < 991) {
40
+ this.#maxCount = Math.floor(this.#maxCount / 2);
41
+ }
42
+ }
43
+
44
+ configure(config: Partial<BlackHoleConfig>): void {
45
+ if (config.speed !== undefined) {
46
+ this.#speed = config.speed;
47
+ }
48
+
49
+ if (config.color !== undefined) {
50
+ const parsed = this.#parseHex(config.color);
51
+ this.#colorR = parsed[0];
52
+ this.#colorG = parsed[1];
53
+ this.#colorB = parsed[2];
54
+ }
55
+ }
56
+
57
+ onResize(width: number, height: number): void {
58
+ this.#width = width;
59
+ this.#height = height;
60
+
61
+ if (!this.#initialized && width > 0 && height > 0) {
62
+ this.#initialized = true;
63
+ this.#particles = [];
64
+
65
+ for (let i = 0; i < this.#maxCount; ++i) {
66
+ this.#particles.push(this.#createParticle(true));
67
+ }
68
+ }
69
+ }
70
+
71
+ tick(dt: number, width: number, height: number): void {
72
+ this.#width = width;
73
+ this.#height = height;
74
+
75
+ const maxRadius = Math.sqrt((width / 2) ** 2 + (height / 2) ** 2) * 0.9;
76
+ const eventHorizon = 24 * this.#scale;
77
+
78
+ for (let i = 0; i < this.#particles.length; ++i) {
79
+ const particle = this.#particles[i];
80
+ const normalizedRadius = particle.radius / maxRadius;
81
+
82
+ particle.angle += particle.angularSpeed * this.#speed * (1 + (1 - normalizedRadius) * 4) * dt * 0.005;
83
+ particle.radius -= particle.radialSpeed * this.#speed * (1 + (1 - normalizedRadius) * 3) * dt * 0.15;
84
+
85
+ if (particle.radius <= eventHorizon) {
86
+ this.#particles[i] = this.#createParticle(false);
87
+ }
88
+ }
89
+ }
90
+
91
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
92
+ const cx = width / 2;
93
+ const cy = height / 2;
94
+ const maxRadius = Math.sqrt(cx * cx + cy * cy) * 0.9;
95
+ const eventHorizon = 24 * this.#scale;
96
+ const cr = this.#colorR;
97
+ const cg = this.#colorG;
98
+ const cb = this.#colorB;
99
+
100
+ ctx.globalCompositeOperation = 'source-over';
101
+ ctx.globalAlpha = 1;
102
+ ctx.fillStyle = 'rgb(2, 0, 10)';
103
+ ctx.fillRect(0, 0, width, height);
104
+
105
+ const accretionRadius = eventHorizon * 6;
106
+ const accretionGlow = ctx.createRadialGradient(cx, cy, eventHorizon * 0.5, cx, cy, accretionRadius);
107
+ accretionGlow.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0.0)`);
108
+ accretionGlow.addColorStop(0.4, `rgba(${cr}, ${cg}, ${cb}, 0.12)`);
109
+ accretionGlow.addColorStop(0.7, `rgba(255, 200, 100, 0.08)`);
110
+ accretionGlow.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
111
+
112
+ ctx.globalCompositeOperation = 'lighter';
113
+ ctx.globalAlpha = 1;
114
+ ctx.beginPath();
115
+ ctx.arc(cx, cy, accretionRadius, 0, Math.PI * 2);
116
+ ctx.fillStyle = accretionGlow;
117
+ ctx.fill();
118
+
119
+ for (const particle of this.#particles) {
120
+ const px = cx + Math.cos(particle.angle) * particle.radius;
121
+ const py = cy + Math.sin(particle.angle) * particle.radius;
122
+
123
+ const normalizedRadius = Math.max(0, Math.min(1, particle.radius / maxRadius));
124
+ const proximity = 1 - normalizedRadius;
125
+
126
+ const r = Math.round(cr + (255 - cr) * proximity * 0.8);
127
+ const g = Math.round(cg + (255 - cg) * proximity * 0.5);
128
+ const b = Math.round(cb + (255 - cb) * proximity * 0.3);
129
+
130
+ const alpha = Math.max(0.05, Math.min(1, particle.brightness * (0.2 + proximity * 0.8)));
131
+ const particleSize = Math.max(0.3, this.#baseSize * (0.4 + proximity * 0.6));
132
+
133
+ ctx.globalAlpha = alpha;
134
+ ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
135
+ ctx.beginPath();
136
+ ctx.arc(px, py, particleSize, 0, Math.PI * 2);
137
+ ctx.fill();
138
+
139
+ if (proximity > 0.6) {
140
+ ctx.globalAlpha = alpha * 0.3;
141
+ ctx.beginPath();
142
+ ctx.arc(px, py, particleSize * 3, 0, Math.PI * 2);
143
+ ctx.fill();
144
+ }
145
+ }
146
+
147
+ ctx.globalCompositeOperation = 'source-over';
148
+ ctx.globalAlpha = 1;
149
+
150
+ const holeGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, eventHorizon);
151
+ holeGrad.addColorStop(0, 'rgb(0, 0, 0)');
152
+ holeGrad.addColorStop(0.7, 'rgb(0, 0, 0)');
153
+ holeGrad.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
154
+
155
+ ctx.fillStyle = holeGrad;
156
+ ctx.beginPath();
157
+ ctx.arc(cx, cy, eventHorizon, 0, Math.PI * 2);
158
+ ctx.fill();
159
+
160
+ ctx.globalAlpha = 1;
161
+ ctx.resetTransform();
162
+ ctx.globalCompositeOperation = 'source-over';
163
+ }
164
+
165
+ #createParticle(spread: boolean): BlackHoleParticle {
166
+ const maxRadius = Math.sqrt((this.#width / 2) ** 2 + (this.#height / 2) ** 2) * 0.9;
167
+ const eventHorizon = 24 * this.#scale;
168
+
169
+ const angle = MULBERRY.next() * Math.PI * 2;
170
+ const radius = spread
171
+ ? eventHorizon + MULBERRY.next() * (maxRadius - eventHorizon)
172
+ : maxRadius * (0.75 + MULBERRY.next() * 0.25);
173
+
174
+ return {
175
+ angle,
176
+ radius,
177
+ angularSpeed: 0.4 + MULBERRY.next() * 1.2,
178
+ radialSpeed: 0.3 + MULBERRY.next() * 0.8,
179
+ size: 0.5 + MULBERRY.next() * 1.5,
180
+ brightness: 0.4 + MULBERRY.next() * 0.6
181
+ };
182
+ }
183
+
184
+ #parseHex(hex: string): [number, number, number] {
185
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
186
+
187
+ if (!result) {
188
+ return [102, 68, 255];
189
+ }
190
+
191
+ return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
192
+ }
193
+ }
@@ -0,0 +1,8 @@
1
+ export interface BlackHoleParticle {
2
+ angle: number;
3
+ radius: number;
4
+ angularSpeed: number;
5
+ radialSpeed: number;
6
+ size: number;
7
+ brightness: number;
8
+ }
@@ -0,0 +1,8 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(59);
4
+
5
+ export const PERCEPTION_RADIUS = 80;
6
+ export const SEPARATION_RADIUS = 25;
7
+ export const MAX_SPEED = 2.5;
8
+ export const MAX_FORCE = 0.08;
@@ -0,0 +1,9 @@
1
+ import { Boids } from './layer';
2
+ import type { BoidsConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createBoids(config?: BoidsConfig): Effect<BoidsConfig> {
6
+ return new Boids(config);
7
+ }
8
+
9
+ export type { BoidsConfig };
@@ -0,0 +1,245 @@
1
+ import { parseColor } from '../color';
2
+ import { Effect } from '../effect';
3
+ import { MAX_FORCE, MAX_SPEED, MULBERRY, PERCEPTION_RADIUS, SEPARATION_RADIUS } from './consts';
4
+ import type { Boid } from './types';
5
+
6
+ export interface BoidsConfig {
7
+ readonly count?: number;
8
+ readonly speed?: number;
9
+ readonly separation?: number;
10
+ readonly alignment?: number;
11
+ readonly cohesion?: number;
12
+ readonly color?: string;
13
+ readonly size?: number;
14
+ readonly scale?: number;
15
+ }
16
+
17
+ export class Boids extends Effect<BoidsConfig> {
18
+ readonly #scale: number;
19
+ #speed: number;
20
+ #separation: number;
21
+ #alignment: number;
22
+ #cohesion: number;
23
+ readonly #colorR: number;
24
+ readonly #colorG: number;
25
+ readonly #colorB: number;
26
+ readonly #size: number;
27
+ readonly #count: number;
28
+ #boids: Boid[] = [];
29
+ #width: number = 800;
30
+ #height: number = 600;
31
+ #initialized: boolean = false;
32
+
33
+ constructor(config: BoidsConfig = {}) {
34
+ super();
35
+
36
+ this.#scale = config.scale ?? 1;
37
+ this.#speed = config.speed ?? 1;
38
+ this.#separation = config.separation ?? 1;
39
+ this.#alignment = config.alignment ?? 1;
40
+ this.#cohesion = config.cohesion ?? 1;
41
+ this.#size = (config.size ?? 6) * this.#scale;
42
+ this.#count = config.count ?? 80;
43
+
44
+ const parsed = parseColor(config.color ?? '#44aaff');
45
+ this.#colorR = parsed.r;
46
+ this.#colorG = parsed.g;
47
+ this.#colorB = parsed.b;
48
+ }
49
+
50
+ configure(config: Partial<BoidsConfig>): void {
51
+ if (config.speed !== undefined) {
52
+ this.#speed = config.speed;
53
+ }
54
+ if (config.separation !== undefined) {
55
+ this.#separation = config.separation;
56
+ }
57
+ if (config.alignment !== undefined) {
58
+ this.#alignment = config.alignment;
59
+ }
60
+ if (config.cohesion !== undefined) {
61
+ this.#cohesion = config.cohesion;
62
+ }
63
+ }
64
+
65
+ onResize(width: number, height: number): void {
66
+ this.#width = width;
67
+ this.#height = height;
68
+
69
+ if (!this.#initialized && width > 0 && height > 0) {
70
+ this.#initialized = true;
71
+ this.#boids = [];
72
+ const count = innerWidth < 991 ? Math.floor(this.#count / 2) : this.#count;
73
+ for (let i = 0; i < count; i++) {
74
+ this.#boids.push(this.#createBoid());
75
+ }
76
+ }
77
+ }
78
+
79
+ tick(dt: number, width: number, height: number): void {
80
+ this.#width = width;
81
+ this.#height = height;
82
+
83
+ const cellSize = PERCEPTION_RADIUS;
84
+ const grid = new Map<string, Boid[]>();
85
+
86
+ for (const boid of this.#boids) {
87
+ const cellX = Math.floor(boid.x / cellSize);
88
+ const cellY = Math.floor(boid.y / cellSize);
89
+ const key = `${cellX},${cellY}`;
90
+ let cell = grid.get(key);
91
+ if (!cell) {
92
+ cell = [];
93
+ grid.set(key, cell);
94
+ }
95
+ cell.push(boid);
96
+ }
97
+
98
+ const speedFactor = this.#speed * dt / 16;
99
+ const maxSpeed = MAX_SPEED * this.#speed;
100
+ const maxForce = MAX_FORCE * this.#speed;
101
+ const perceptionR2 = PERCEPTION_RADIUS * PERCEPTION_RADIUS;
102
+ const separationR2 = SEPARATION_RADIUS * SEPARATION_RADIUS;
103
+
104
+ for (const boid of this.#boids) {
105
+ const cellX = Math.floor(boid.x / cellSize);
106
+ const cellY = Math.floor(boid.y / cellSize);
107
+
108
+ let sepX = 0, sepY = 0, sepCount = 0;
109
+ let alignVX = 0, alignVY = 0, alignCount = 0;
110
+ let cohX = 0, cohY = 0, cohCount = 0;
111
+
112
+ for (let dx = -1; dx <= 1; dx++) {
113
+ for (let dy = -1; dy <= 1; dy++) {
114
+ const neighbors = grid.get(`${cellX + dx},${cellY + dy}`);
115
+ if (!neighbors) {
116
+ continue;
117
+ }
118
+
119
+ for (const other of neighbors) {
120
+ if (other === boid) {
121
+ continue;
122
+ }
123
+
124
+ const diffX = boid.x - other.x;
125
+ const diffY = boid.y - other.y;
126
+ const dist2 = diffX * diffX + diffY * diffY;
127
+
128
+ if (dist2 < perceptionR2) {
129
+ alignVX += other.vx;
130
+ alignVY += other.vy;
131
+ alignCount++;
132
+
133
+ cohX += other.x;
134
+ cohY += other.y;
135
+ cohCount++;
136
+ }
137
+
138
+ if (dist2 < separationR2 && dist2 > 0) {
139
+ const dist = Math.sqrt(dist2);
140
+ sepX += diffX / dist;
141
+ sepY += diffY / dist;
142
+ sepCount++;
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ let forceX = 0;
149
+ let forceY = 0;
150
+
151
+ if (sepCount > 0) {
152
+ const fx = sepX / sepCount;
153
+ const fy = sepY / sepCount;
154
+ const len = Math.sqrt(fx * fx + fy * fy) || 1;
155
+ forceX += (fx / len * maxSpeed - boid.vx) * maxForce * this.#separation;
156
+ forceY += (fy / len * maxSpeed - boid.vy) * maxForce * this.#separation;
157
+ }
158
+
159
+ if (alignCount > 0) {
160
+ const fx = alignVX / alignCount;
161
+ const fy = alignVY / alignCount;
162
+ const len = Math.sqrt(fx * fx + fy * fy) || 1;
163
+ forceX += (fx / len * maxSpeed - boid.vx) * maxForce * this.#alignment;
164
+ forceY += (fy / len * maxSpeed - boid.vy) * maxForce * this.#alignment;
165
+ }
166
+
167
+ if (cohCount > 0) {
168
+ const targetX = cohX / cohCount;
169
+ const targetY = cohY / cohCount;
170
+ const diffX = targetX - boid.x;
171
+ const diffY = targetY - boid.y;
172
+ const len = Math.sqrt(diffX * diffX + diffY * diffY) || 1;
173
+ forceX += (diffX / len * maxSpeed - boid.vx) * maxForce * this.#cohesion;
174
+ forceY += (diffY / len * maxSpeed - boid.vy) * maxForce * this.#cohesion;
175
+ }
176
+
177
+ boid.vx += forceX * speedFactor;
178
+ boid.vy += forceY * speedFactor;
179
+
180
+ const speed = Math.sqrt(boid.vx * boid.vx + boid.vy * boid.vy);
181
+ if (speed > maxSpeed) {
182
+ boid.vx = (boid.vx / speed) * maxSpeed;
183
+ boid.vy = (boid.vy / speed) * maxSpeed;
184
+ } else if (speed < 0.5) {
185
+ boid.vx += (MULBERRY.next() - 0.5) * 0.2;
186
+ boid.vy += (MULBERRY.next() - 0.5) * 0.2;
187
+ }
188
+
189
+ boid.x += boid.vx * speedFactor;
190
+ boid.y += boid.vy * speedFactor;
191
+
192
+ boid.angle = Math.atan2(boid.vy, boid.vx);
193
+
194
+ if (boid.x < -10) { boid.x += width + 10; }
195
+ if (boid.x > width + 10) { boid.x -= width + 10; }
196
+ if (boid.y < -10) { boid.y += height + 10; }
197
+ if (boid.y > height + 10) { boid.y -= height + 10; }
198
+ }
199
+ }
200
+
201
+ draw(ctx: CanvasRenderingContext2D, _width: number, _height: number): void {
202
+ const size = this.#size;
203
+ const r = this.#colorR;
204
+ const g = this.#colorG;
205
+ const b = this.#colorB;
206
+
207
+ ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
208
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.4)`;
209
+ ctx.lineWidth = 0.5;
210
+
211
+ for (const boid of this.#boids) {
212
+ const cos = Math.cos(boid.angle);
213
+ const sin = Math.sin(boid.angle);
214
+
215
+ ctx.save();
216
+ ctx.transform(cos, sin, -sin, cos, boid.x, boid.y);
217
+
218
+ ctx.beginPath();
219
+ ctx.moveTo(size, 0);
220
+ ctx.lineTo(-size * 0.6, -size * 0.45);
221
+ ctx.lineTo(-size * 0.3, 0);
222
+ ctx.lineTo(-size * 0.6, size * 0.45);
223
+ ctx.closePath();
224
+ ctx.fill();
225
+ ctx.stroke();
226
+
227
+ ctx.restore();
228
+ }
229
+
230
+ ctx.globalAlpha = 1;
231
+ }
232
+
233
+ #createBoid(): Boid {
234
+ const angle = MULBERRY.next() * Math.PI * 2;
235
+ const speed = (MAX_SPEED * 0.5) + MULBERRY.next() * MAX_SPEED * 0.5;
236
+
237
+ return {
238
+ x: MULBERRY.next() * this.#width,
239
+ y: MULBERRY.next() * this.#height,
240
+ vx: Math.cos(angle) * speed,
241
+ vy: Math.sin(angle) * speed,
242
+ angle
243
+ };
244
+ }
245
+ }
@@ -0,0 +1,7 @@
1
+ export type Boid = {
2
+ x: number;
3
+ y: number;
4
+ vx: number;
5
+ vy: number;
6
+ angle: number;
7
+ };
@@ -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 { Butterflies } from './layer';
2
+ import type { ButterfliesConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createButterflies(config?: ButterfliesConfig): Effect<ButterfliesConfig> {
6
+ return new Butterflies(config);
7
+ }
8
+
9
+ export type { ButterfliesConfig };