@basmilius/sparkle 2.1.0 → 2.3.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 (108) hide show
  1. package/dist/index.d.mts +317 -459
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +1258 -949
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +6 -2
  6. package/src/aurora/index.ts +9 -3
  7. package/src/aurora/layer.ts +57 -29
  8. package/src/balloons/index.ts +9 -3
  9. package/src/balloons/layer.ts +50 -19
  10. package/src/bubbles/index.ts +9 -3
  11. package/src/bubbles/layer.ts +30 -17
  12. package/src/canvas.ts +92 -2
  13. package/src/color.ts +11 -2
  14. package/src/confetti/index.ts +15 -3
  15. package/src/confetti/layer.ts +8 -5
  16. package/src/confetti/particle.ts +12 -11
  17. package/src/confetti/shapes.ts +84 -97
  18. package/src/donuts/consts.ts +2 -2
  19. package/src/donuts/index.ts +9 -3
  20. package/src/donuts/layer.ts +43 -12
  21. package/src/effect.ts +107 -0
  22. package/src/fade.ts +87 -0
  23. package/src/fireflies/index.ts +9 -3
  24. package/src/fireflies/layer.ts +26 -9
  25. package/src/fireflies/particle.ts +2 -2
  26. package/src/firepit/index.ts +9 -3
  27. package/src/firepit/layer.ts +26 -7
  28. package/src/fireworks/create-explosion.ts +237 -0
  29. package/src/fireworks/explosion.ts +1 -1
  30. package/src/fireworks/index.ts +15 -3
  31. package/src/fireworks/layer.ts +55 -304
  32. package/src/fireworks/spark.ts +2 -2
  33. package/src/fireworks/types.ts +2 -2
  34. package/src/glitter/index.ts +9 -4
  35. package/src/glitter/layer.ts +15 -7
  36. package/src/glitter/types.ts +10 -0
  37. package/src/index.ts +3 -4
  38. package/src/lanterns/index.ts +9 -4
  39. package/src/lanterns/layer.ts +22 -10
  40. package/src/lanterns/types.ts +8 -0
  41. package/src/layer.ts +13 -11
  42. package/src/leaves/index.ts +9 -4
  43. package/src/leaves/layer.ts +21 -14
  44. package/src/leaves/types.ts +9 -0
  45. package/src/lightning/index.ts +9 -4
  46. package/src/lightning/layer.ts +4 -4
  47. package/src/lightning/system.ts +3 -3
  48. package/src/lightning/types.ts +10 -2
  49. package/src/matrix/index.ts +9 -4
  50. package/src/matrix/layer.ts +15 -7
  51. package/src/matrix/types.ts +9 -0
  52. package/src/orbits/index.ts +9 -4
  53. package/src/orbits/layer.ts +51 -21
  54. package/src/orbits/types.ts +12 -1
  55. package/src/particles/index.ts +9 -3
  56. package/src/particles/layer.ts +55 -12
  57. package/src/petals/index.ts +9 -3
  58. package/src/petals/layer.ts +29 -13
  59. package/src/plasma/index.ts +9 -3
  60. package/src/plasma/layer.ts +21 -6
  61. package/src/rain/index.ts +9 -3
  62. package/src/rain/layer.ts +30 -8
  63. package/src/sandstorm/index.ts +9 -3
  64. package/src/sandstorm/layer.ts +26 -9
  65. package/src/scene.ts +204 -0
  66. package/src/shooting-stars/system.ts +26 -24
  67. package/src/shooting-stars/types.ts +2 -1
  68. package/src/simulation-canvas.ts +45 -6
  69. package/src/snow/index.ts +9 -3
  70. package/src/snow/layer.ts +24 -11
  71. package/src/sparklers/index.ts +13 -3
  72. package/src/sparklers/layer.ts +61 -15
  73. package/src/stars/index.ts +9 -3
  74. package/src/stars/layer.ts +28 -22
  75. package/src/streamers/index.ts +9 -3
  76. package/src/streamers/layer.ts +18 -6
  77. package/src/streamers/types.ts +1 -1
  78. package/src/waves/index.ts +9 -3
  79. package/src/waves/layer.ts +42 -45
  80. package/src/waves/types.ts +1 -0
  81. package/src/wormhole/index.ts +9 -3
  82. package/src/wormhole/layer.ts +22 -6
  83. package/src/aurora/simulation.ts +0 -19
  84. package/src/balloons/simulation.ts +0 -19
  85. package/src/bubbles/simulation.ts +0 -20
  86. package/src/confetti/simulation.ts +0 -27
  87. package/src/donuts/simulation.ts +0 -25
  88. package/src/fireflies/simulation.ts +0 -18
  89. package/src/firepit/simulation.ts +0 -17
  90. package/src/fireworks/simulation.ts +0 -18
  91. package/src/glitter/simulation.ts +0 -19
  92. package/src/lanterns/simulation.ts +0 -17
  93. package/src/layered.ts +0 -185
  94. package/src/leaves/simulation.ts +0 -18
  95. package/src/lightning/simulation.ts +0 -17
  96. package/src/matrix/simulation.ts +0 -18
  97. package/src/orbits/simulation.ts +0 -19
  98. package/src/particles/simulation.ts +0 -26
  99. package/src/petals/simulation.ts +0 -18
  100. package/src/plasma/simulation.ts +0 -17
  101. package/src/rain/simulation.ts +0 -21
  102. package/src/sandstorm/simulation.ts +0 -18
  103. package/src/snow/simulation.ts +0 -17
  104. package/src/sparklers/simulation.ts +0 -30
  105. package/src/stars/simulation.ts +0 -22
  106. package/src/streamers/simulation.ts +0 -16
  107. package/src/waves/simulation.ts +0 -18
  108. package/src/wormhole/simulation.ts +0 -17
@@ -1,4 +1,5 @@
1
1
  import type { Point } from '../point';
2
+ import { MULBERRY } from './consts';
2
3
  import { SHAPE_PATHS } from './shapes';
3
4
  import type { Shape } from './types';
4
5
 
@@ -47,29 +48,29 @@ export class ConfettiParticle {
47
48
  const startVelocity = (config.startVelocity ?? 45) * scale;
48
49
  const launchAngle = -(direction * Math.PI / 180)
49
50
  + (0.5 * spread * Math.PI / 180)
50
- - (Math.random() * spread * Math.PI / 180);
51
- const speed = startVelocity * (0.5 + Math.random());
52
- const rotAngle = Math.random() * Math.PI * 2;
51
+ - (MULBERRY.next() * spread * Math.PI / 180);
52
+ const speed = startVelocity * (0.5 + MULBERRY.next());
53
+ const rotAngle = MULBERRY.next() * Math.PI * 2;
53
54
 
54
55
  this.#colorStr = color;
55
56
  this.#gravity = (config.gravity ?? 1) * scale;
56
57
  this.#shape = shape;
57
- this.#size = (5 + Math.random() * 5) * scale;
58
+ this.#size = (5 + MULBERRY.next() * 5) * scale;
58
59
  this.#totalTicks = config.ticks ?? 200;
59
60
  this.#x = position.x;
60
61
  this.#y = position.y;
61
62
  this.#vx = Math.cos(launchAngle) * speed;
62
63
  this.#vy = Math.sin(launchAngle) * speed;
63
- this.#decay = (config.decay ?? 0.9) - 0.05 + Math.random() * 0.1;
64
- this.#flipAngle = Math.random() * Math.PI * 2;
65
- this.#flipSpeed = 0.03 + Math.random() * 0.05;
64
+ this.#decay = (config.decay ?? 0.9) - 0.05 + MULBERRY.next() * 0.1;
65
+ this.#flipAngle = MULBERRY.next() * Math.PI * 2;
66
+ this.#flipSpeed = 0.03 + MULBERRY.next() * 0.05;
66
67
  this.#rotAngle = rotAngle;
67
68
  this.#rotCos = Math.cos(rotAngle);
68
69
  this.#rotSin = Math.sin(rotAngle);
69
- this.#rotSpeed = (Math.random() - 0.5) * 0.06;
70
- this.#swing = Math.random() * Math.PI * 2;
71
- this.#swingAmp = 0.5 + Math.random() * 1.5;
72
- this.#swingSpeed = 0.025 + Math.random() * 0.035;
70
+ this.#rotSpeed = (MULBERRY.next() - 0.5) * 0.06;
71
+ this.#swing = MULBERRY.next() * Math.PI * 2;
72
+ this.#swingAmp = 0.5 + MULBERRY.next() * 1.5;
73
+ this.#swingSpeed = 0.025 + MULBERRY.next() * 0.035;
73
74
  }
74
75
 
75
76
  draw(ctx: CanvasRenderingContext2D): void {
@@ -2,103 +2,90 @@ import type { Shape } from './types';
2
2
 
3
3
  const TWO_PI = Math.PI * 2;
4
4
 
5
- export const SHAPE_PATHS: Record<Shape, Path2D> = {
6
- bowtie: (() => {
7
- const path = new Path2D();
8
- path.moveTo(-1, -0.7);
9
- path.lineTo(0, 0);
10
- path.lineTo(-1, 0.7);
11
- path.closePath();
12
- path.moveTo(1, -0.7);
13
- path.lineTo(0, 0);
14
- path.lineTo(1, 0.7);
15
- path.closePath();
16
- return path;
17
- })(),
18
- circle: (() => {
19
- const path = new Path2D();
20
- path.ellipse(0, 0, 0.6, 1, 0, 0, TWO_PI);
21
- return path;
22
- })(),
23
- crescent: (() => {
24
- const path = new Path2D();
25
- path.arc(0, 0, 1, 0, TWO_PI, false);
26
- path.arc(0.45, 0, 0.9, 0, TWO_PI, true);
27
- return path;
28
- })(),
29
- diamond: (() => {
30
- const path = new Path2D();
31
- path.moveTo(0, -1);
32
- path.lineTo(0.6, 0);
33
- path.lineTo(0, 1);
34
- path.lineTo(-0.6, 0);
35
- path.closePath();
36
- return path;
37
- })(),
38
- heart: (() => {
39
- const path = new Path2D();
40
- path.moveTo(0, 1);
41
- path.bezierCurveTo(-0.4, 0.55, -1, 0.1, -1, -0.35);
42
- path.bezierCurveTo(-1, -0.8, -0.5, -1, 0, -0.6);
43
- path.bezierCurveTo(0.5, -1, 1, -0.8, 1, -0.35);
44
- path.bezierCurveTo(1, 0.1, 0.4, 0.55, 0, 1);
45
- path.closePath();
46
- return path;
47
- })(),
48
- hexagon: (() => {
49
- const path = new Path2D();
50
- for (let i = 0; i < 6; i++) {
51
- const angle = (i * Math.PI / 3) - Math.PI / 2;
52
- if (i === 0) {
53
- path.moveTo(Math.cos(angle), Math.sin(angle));
54
- } else {
55
- path.lineTo(Math.cos(angle), Math.sin(angle));
56
- }
5
+ function buildShapePaths(): Record<Shape, Path2D> {
6
+ const bowtie = new Path2D();
7
+ bowtie.moveTo(-1, -0.7);
8
+ bowtie.lineTo(0, 0);
9
+ bowtie.lineTo(-1, 0.7);
10
+ bowtie.closePath();
11
+ bowtie.moveTo(1, -0.7);
12
+ bowtie.lineTo(0, 0);
13
+ bowtie.lineTo(1, 0.7);
14
+ bowtie.closePath();
15
+
16
+ const circle = new Path2D();
17
+ circle.ellipse(0, 0, 0.6, 1, 0, 0, TWO_PI);
18
+
19
+ const crescent = new Path2D();
20
+ crescent.arc(0, 0, 1, 0, TWO_PI, false);
21
+ crescent.arc(0.45, 0, 0.9, 0, TWO_PI, true);
22
+
23
+ const diamond = new Path2D();
24
+ diamond.moveTo(0, -1);
25
+ diamond.lineTo(0.6, 0);
26
+ diamond.lineTo(0, 1);
27
+ diamond.lineTo(-0.6, 0);
28
+ diamond.closePath();
29
+
30
+ const heart = new Path2D();
31
+ heart.moveTo(0, 1);
32
+ heart.bezierCurveTo(-0.4, 0.55, -1, 0.1, -1, -0.35);
33
+ heart.bezierCurveTo(-1, -0.8, -0.5, -1, 0, -0.6);
34
+ heart.bezierCurveTo(0.5, -1, 1, -0.8, 1, -0.35);
35
+ heart.bezierCurveTo(1, 0.1, 0.4, 0.55, 0, 1);
36
+ heart.closePath();
37
+
38
+ const hexagon = new Path2D();
39
+ for (let i = 0; i < 6; i++) {
40
+ const angle = (i * Math.PI / 3) - Math.PI / 2;
41
+ if (i === 0) {
42
+ hexagon.moveTo(Math.cos(angle), Math.sin(angle));
43
+ } else {
44
+ hexagon.lineTo(Math.cos(angle), Math.sin(angle));
57
45
  }
58
- path.closePath();
59
- return path;
60
- })(),
61
- ribbon: (() => {
62
- const path = new Path2D();
63
- path.rect(-0.2, -1, 0.4, 2);
64
- return path;
65
- })(),
66
- ring: (() => {
67
- const path = new Path2D();
68
- path.arc(0, 0, 1, 0, TWO_PI, false);
69
- path.arc(0, 0, 0.55, 0, TWO_PI, true);
70
- return path;
71
- })(),
72
- square: (() => {
73
- const path = new Path2D();
74
- path.rect(-0.7, -0.7, 1.4, 1.4);
75
- return path;
76
- })(),
77
- star: (() => {
78
- const path = new Path2D();
79
- for (let i = 0; i < 10; i++) {
80
- const r = i % 2 === 0 ? 1 : 0.42;
81
- const angle = (i * Math.PI / 5) - Math.PI / 2;
82
- if (i === 0) {
83
- path.moveTo(r * Math.cos(angle), r * Math.sin(angle));
84
- } else {
85
- path.lineTo(r * Math.cos(angle), r * Math.sin(angle));
86
- }
46
+ }
47
+ hexagon.closePath();
48
+
49
+ const ribbon = new Path2D();
50
+ ribbon.rect(-0.2, -1, 0.4, 2);
51
+
52
+ const ring = new Path2D();
53
+ ring.arc(0, 0, 1, 0, TWO_PI, false);
54
+ ring.arc(0, 0, 0.55, 0, TWO_PI, true);
55
+
56
+ const square = new Path2D();
57
+ square.rect(-0.7, -0.7, 1.4, 1.4);
58
+
59
+ const star = new Path2D();
60
+ for (let i = 0; i < 10; i++) {
61
+ const r = i % 2 === 0 ? 1 : 0.42;
62
+ const angle = (i * Math.PI / 5) - Math.PI / 2;
63
+ if (i === 0) {
64
+ star.moveTo(r * Math.cos(angle), r * Math.sin(angle));
65
+ } else {
66
+ star.lineTo(r * Math.cos(angle), r * Math.sin(angle));
87
67
  }
88
- path.closePath();
89
- return path;
90
- })(),
91
- triangle: (() => {
92
- const path = new Path2D();
93
- for (let i = 0; i < 3; i++) {
94
- const angle = (i * 2 * Math.PI / 3) - Math.PI / 2;
95
- if (i === 0) {
96
- path.moveTo(Math.cos(angle), Math.sin(angle));
97
- } else {
98
- path.lineTo(Math.cos(angle), Math.sin(angle));
99
- }
68
+ }
69
+ star.closePath();
70
+
71
+ const triangle = new Path2D();
72
+ for (let i = 0; i < 3; i++) {
73
+ const angle = (i * 2 * Math.PI / 3) - Math.PI / 2;
74
+ if (i === 0) {
75
+ triangle.moveTo(Math.cos(angle), Math.sin(angle));
76
+ } else {
77
+ triangle.lineTo(Math.cos(angle), Math.sin(angle));
100
78
  }
101
- path.closePath();
102
- return path;
103
- })()
104
- };
79
+ }
80
+ triangle.closePath();
81
+
82
+ return {bowtie, circle, crescent, diamond, heart, hexagon, ribbon, ring, square, star, triangle};
83
+ }
84
+
85
+ let _shapePaths: Record<Shape, Path2D> | null = null;
86
+
87
+ export const SHAPE_PATHS: Record<Shape, Path2D> = new Proxy({} as Record<Shape, Path2D>, {
88
+ get(_, key: string) {
89
+ return (_shapePaths ??= buildShapePaths())[key as Shape];
90
+ }
91
+ });
@@ -1,9 +1,9 @@
1
1
  import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
- import type { DonutSimulationConfig } from './simulation';
2
+ import type { DonutsConfig } from './layer';
3
3
 
4
4
  export const MULBERRY: Mulberry32 = mulberry32(13);
5
5
 
6
- export const DEFAULT_CONFIG: DonutSimulationConfig = {
6
+ export const DEFAULT_CONFIG: DonutsConfig = {
7
7
  background: '#a51955',
8
8
  collisionPadding: 20,
9
9
  colors: ['#bd1961', '#da287c'],
@@ -1,3 +1,9 @@
1
- export { DonutLayer } from './layer';
2
- export { DonutSimulation } from './simulation';
3
- export type { DonutSimulationConfig } from './simulation';
1
+ import { Donuts } from './layer';
2
+ import type { DonutsConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createDonuts(config?: DonutsConfig): Effect<DonutsConfig> {
6
+ return new Donuts(config);
7
+ }
8
+
9
+ export type { DonutsConfig };
@@ -1,18 +1,33 @@
1
- import { SimulationLayer } from '../layer';
1
+ import { Effect } from '../effect';
2
2
  import { DEFAULT_CONFIG, MULBERRY } from './consts';
3
3
  import type { Donut } from './donut';
4
- import type { DonutSimulationConfig } from './simulation';
5
4
 
6
- export class DonutLayer extends SimulationLayer {
5
+ export interface DonutsConfig {
6
+ readonly background?: string;
7
+ readonly collisionPadding?: number;
8
+ readonly colors?: string[];
9
+ readonly count?: number;
10
+ readonly mouseAvoidance?: boolean;
11
+ readonly mouseAvoidanceRadius?: number;
12
+ readonly mouseAvoidanceStrength?: number;
13
+ readonly radiusRange?: [number, number];
14
+ readonly repulsionStrength?: number;
15
+ readonly rotationSpeedRange?: [number, number];
16
+ readonly scale?: number;
17
+ readonly speedRange?: [number, number];
18
+ readonly thickness?: number;
19
+ }
20
+
21
+ export class Donuts extends Effect<DonutsConfig> {
7
22
  readonly #background: string;
8
23
  readonly #collisionPadding: number;
9
24
  readonly #colors: string[];
10
25
  readonly #count: number;
11
- readonly #mouseAvoidance: boolean;
12
- readonly #mouseAvoidanceRadius: number;
13
- readonly #mouseAvoidanceStrength: number;
26
+ #mouseAvoidance: boolean;
27
+ #mouseAvoidanceRadius: number;
28
+ #mouseAvoidanceStrength: number;
14
29
  readonly #radiusRange: [number, number];
15
- readonly #repulsionStrength: number;
30
+ #repulsionStrength: number;
16
31
  readonly #rotationSpeedRange: [number, number];
17
32
  readonly #scale: number;
18
33
  readonly #speedRange: [number, number];
@@ -27,7 +42,7 @@ export class DonutLayer extends SimulationLayer {
27
42
  #height: number = 540;
28
43
  #initialized: boolean = false;
29
44
 
30
- constructor(config: DonutSimulationConfig = {}) {
45
+ constructor(config: DonutsConfig = {}) {
31
46
  super();
32
47
 
33
48
  const scale = config.scale ?? 1;
@@ -82,6 +97,21 @@ export class DonutLayer extends SimulationLayer {
82
97
  canvas.removeEventListener('mouseleave', this.#onMouseLeaveBound);
83
98
  }
84
99
 
100
+ configure(config: Partial<DonutsConfig>): void {
101
+ if (config.mouseAvoidance !== undefined) {
102
+ this.#mouseAvoidance = config.mouseAvoidance;
103
+ }
104
+ if (config.mouseAvoidanceRadius !== undefined) {
105
+ this.#mouseAvoidanceRadius = config.mouseAvoidanceRadius;
106
+ }
107
+ if (config.mouseAvoidanceStrength !== undefined) {
108
+ this.#mouseAvoidanceStrength = config.mouseAvoidanceStrength;
109
+ }
110
+ if (config.repulsionStrength !== undefined) {
111
+ this.#repulsionStrength = config.repulsionStrength;
112
+ }
113
+ }
114
+
85
115
  tick(dt: number, width: number, height: number): void {
86
116
  this.#width = width;
87
117
  this.#height = height;
@@ -103,9 +133,9 @@ export class DonutLayer extends SimulationLayer {
103
133
  ctx.fillRect(0, 0, width, height);
104
134
 
105
135
  for (const donut of this.#donuts) {
106
- ctx.save();
107
- ctx.translate(donut.x, donut.y);
108
- ctx.rotate(donut.angle);
136
+ const cos = Math.cos(donut.angle);
137
+ const sin = Math.sin(donut.angle);
138
+ ctx.setTransform(cos, sin, -sin, cos, donut.x, donut.y);
109
139
 
110
140
  ctx.beginPath();
111
141
  ctx.arc(0, 0, donut.outerRadius, 0, Math.PI * 2);
@@ -114,8 +144,9 @@ export class DonutLayer extends SimulationLayer {
114
144
 
115
145
  ctx.fillStyle = donut.color;
116
146
  ctx.fill();
117
- ctx.restore();
118
147
  }
148
+
149
+ ctx.resetTransform();
119
150
  }
120
151
 
121
152
  #updateDonut(donut: Donut, dt: number): void {
package/src/effect.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { SimulationCanvas } from './simulation-canvas';
2
+ import type { EdgeFade, EdgeFadeSide, SimulationLayer } from './layer';
3
+
4
+ export type { EdgeFade, EdgeFadeSide };
5
+
6
+ /**
7
+ * Base class for all visual effects. Implements the internal SimulationLayer interface
8
+ * so that effects can be used both standalone (via mount()) and composed in a Scene.
9
+ *
10
+ * @example Standalone usage
11
+ * const snow = new Snow({ particles: 200 });
12
+ * snow.mount(canvas).start();
13
+ *
14
+ * @example Scene composition
15
+ * const scene = new Scene()
16
+ * .mount(canvas)
17
+ * .layer(new Aurora())
18
+ * .layer(new Snow())
19
+ * .start();
20
+ */
21
+ export abstract class Effect<TConfig = Record<string, unknown>> implements SimulationLayer {
22
+ #canvas: SimulationCanvas | null = null;
23
+ fade: EdgeFade | null = null;
24
+
25
+ abstract tick(dt: number, width: number, height: number): void;
26
+
27
+ abstract draw(ctx: CanvasRenderingContext2D, width: number, height: number): void;
28
+
29
+ configure(_config: Partial<TConfig>): void {
30
+ }
31
+
32
+ onResize(_width: number, _height: number): void {
33
+ }
34
+
35
+ onMount(_canvas: HTMLCanvasElement): void {
36
+ }
37
+
38
+ onUnmount(_canvas: HTMLCanvasElement): void {
39
+ }
40
+
41
+ /**
42
+ * Apply an edge fade mask when rendering this effect standalone or in a Scene.
43
+ */
44
+ withFade(fade: EdgeFade): this {
45
+ this.fade = fade;
46
+ return this;
47
+ }
48
+
49
+ /**
50
+ * Mount this effect to a canvas element or CSS selector, creating the render loop.
51
+ * Must be called before start().
52
+ */
53
+ mount(canvas: HTMLCanvasElement | string, options: CanvasRenderingContext2DSettings = {colorSpace: 'display-p3'}, frameRate: number = 60): this {
54
+ if (typeof canvas === 'string') {
55
+ const el = document.querySelector<HTMLCanvasElement>(canvas);
56
+
57
+ if (!el) {
58
+ throw new Error(`Effect.mount(): no element found for selector "${canvas}".`);
59
+ }
60
+
61
+ canvas = el;
62
+ }
63
+
64
+ this.#canvas = new SimulationCanvas(canvas, this as unknown as SimulationLayer, frameRate, options);
65
+ return this;
66
+ }
67
+
68
+ /**
69
+ * Remove this effect from its canvas and clean up the render loop.
70
+ */
71
+ unmount(): this {
72
+ this.#canvas?.destroy();
73
+ this.#canvas = null;
74
+ return this;
75
+ }
76
+
77
+ /**
78
+ * Start the render loop. Call mount() first.
79
+ */
80
+ start(): this {
81
+ this.#canvas?.start();
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Pause rendering without destroying state. Use resume() to continue.
87
+ */
88
+ pause(): this {
89
+ this.#canvas?.pause();
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * Resume rendering after a pause().
95
+ */
96
+ resume(): this {
97
+ this.#canvas?.resume();
98
+ return this;
99
+ }
100
+
101
+ /**
102
+ * Stop rendering and call onUnmount(). Safe to call multiple times.
103
+ */
104
+ destroy(): void {
105
+ this.unmount();
106
+ }
107
+ }
package/src/fade.ts ADDED
@@ -0,0 +1,87 @@
1
+ import type { EdgeFade, EdgeFadeSide } from './layer';
2
+
3
+ function parseSide(side: EdgeFadeSide): [number, number] {
4
+ return typeof side === 'number' ? [0, side] : side;
5
+ }
6
+
7
+ export function applyEdgeFade(ctx: CanvasRenderingContext2D, width: number, height: number, fade: EdgeFade): void {
8
+ ctx.globalCompositeOperation = 'destination-out';
9
+
10
+ if (fade.top !== undefined) {
11
+ const [near, far] = parseSide(fade.top);
12
+ const nearPx = near * height;
13
+ const farPx = far * height;
14
+
15
+ if (nearPx > 0) {
16
+ ctx.fillStyle = 'rgba(0,0,0,1)';
17
+ ctx.fillRect(0, 0, width, nearPx);
18
+ }
19
+
20
+ if (farPx > nearPx) {
21
+ const gradient = ctx.createLinearGradient(0, nearPx, 0, farPx);
22
+ gradient.addColorStop(0, 'rgba(0,0,0,1)');
23
+ gradient.addColorStop(1, 'rgba(0,0,0,0)');
24
+ ctx.fillStyle = gradient;
25
+ ctx.fillRect(0, nearPx, width, farPx - nearPx);
26
+ }
27
+ }
28
+
29
+ if (fade.bottom !== undefined) {
30
+ const [near, far] = parseSide(fade.bottom);
31
+ const nearPx = near * height;
32
+ const farPx = far * height;
33
+
34
+ if (nearPx > 0) {
35
+ ctx.fillStyle = 'rgba(0,0,0,1)';
36
+ ctx.fillRect(0, height - nearPx, width, nearPx);
37
+ }
38
+
39
+ if (farPx > nearPx) {
40
+ const gradient = ctx.createLinearGradient(0, height - farPx, 0, height - nearPx);
41
+ gradient.addColorStop(0, 'rgba(0,0,0,0)');
42
+ gradient.addColorStop(1, 'rgba(0,0,0,1)');
43
+ ctx.fillStyle = gradient;
44
+ ctx.fillRect(0, height - farPx, width, farPx - nearPx);
45
+ }
46
+ }
47
+
48
+ if (fade.left !== undefined) {
49
+ const [near, far] = parseSide(fade.left);
50
+ const nearPx = near * width;
51
+ const farPx = far * width;
52
+
53
+ if (nearPx > 0) {
54
+ ctx.fillStyle = 'rgba(0,0,0,1)';
55
+ ctx.fillRect(0, 0, nearPx, height);
56
+ }
57
+
58
+ if (farPx > nearPx) {
59
+ const gradient = ctx.createLinearGradient(nearPx, 0, farPx, 0);
60
+ gradient.addColorStop(0, 'rgba(0,0,0,1)');
61
+ gradient.addColorStop(1, 'rgba(0,0,0,0)');
62
+ ctx.fillStyle = gradient;
63
+ ctx.fillRect(nearPx, 0, farPx - nearPx, height);
64
+ }
65
+ }
66
+
67
+ if (fade.right !== undefined) {
68
+ const [near, far] = parseSide(fade.right);
69
+ const nearPx = near * width;
70
+ const farPx = far * width;
71
+
72
+ if (nearPx > 0) {
73
+ ctx.fillStyle = 'rgba(0,0,0,1)';
74
+ ctx.fillRect(width - nearPx, 0, nearPx, height);
75
+ }
76
+
77
+ if (farPx > nearPx) {
78
+ const gradient = ctx.createLinearGradient(width - farPx, 0, width - nearPx, 0);
79
+ gradient.addColorStop(0, 'rgba(0,0,0,0)');
80
+ gradient.addColorStop(1, 'rgba(0,0,0,1)');
81
+ ctx.fillStyle = gradient;
82
+ ctx.fillRect(width - farPx, 0, farPx - nearPx, height);
83
+ }
84
+ }
85
+
86
+ ctx.globalCompositeOperation = 'source-over';
87
+ }
@@ -1,6 +1,12 @@
1
- export { FireflyLayer } from './layer';
1
+ import { Fireflies } from './layer';
2
+ import type { FirefliesConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createFireflies(config?: FirefliesConfig): Effect<FirefliesConfig> {
6
+ return new Fireflies(config);
7
+ }
8
+
2
9
  export { FireflyParticle, createFireflySprite } from './particle';
3
- export { FireflySimulation } from './simulation';
10
+ export type { FirefliesConfig };
4
11
  export type { FireflyParticleConfig } from './particle';
5
- export type { FireflySimulationConfig } from './simulation';
6
12
  export type { Firefly } from './types';
@@ -1,23 +1,31 @@
1
- import { SimulationLayer } from '../layer';
1
+ import { Effect } from '../effect';
2
2
  import { MULBERRY } from './consts';
3
- import type { FireflySimulationConfig } from './simulation';
4
3
  import type { Firefly } from './types';
5
4
 
6
5
  const SPRITE_SIZE = 64;
7
6
  const SPRITE_CENTER = SPRITE_SIZE / 2;
8
7
  const SPRITE_RADIUS = SPRITE_SIZE / 2;
9
8
 
10
- export class FireflyLayer extends SimulationLayer {
9
+ export interface FirefliesConfig {
10
+ readonly count?: number;
11
+ readonly color?: string;
12
+ readonly size?: number;
13
+ readonly speed?: number;
14
+ readonly glowSpeed?: number;
15
+ readonly scale?: number;
16
+ }
17
+
18
+ export class Fireflies extends Effect<FirefliesConfig> {
11
19
  readonly #scale: number;
12
20
  readonly #size: number;
13
- readonly #speed: number;
14
- readonly #glowSpeed: number;
21
+ #speed: number;
22
+ #glowSpeed: number;
15
23
  #maxCount: number;
16
24
  #time: number = 0;
17
25
  #fireflies: Firefly[] = [];
18
26
  #sprite: HTMLCanvasElement;
19
27
 
20
- constructor(config: FireflySimulationConfig = {}) {
28
+ constructor(config: FirefliesConfig = {}) {
21
29
  super();
22
30
 
23
31
  this.#scale = config.scale ?? 1;
@@ -39,15 +47,24 @@ export class FireflyLayer extends SimulationLayer {
39
47
  }
40
48
  }
41
49
 
50
+ configure(config: Partial<FirefliesConfig>): void {
51
+ if (config.speed !== undefined) {
52
+ this.#speed = config.speed;
53
+ }
54
+ if (config.glowSpeed !== undefined) {
55
+ this.#glowSpeed = config.glowSpeed;
56
+ }
57
+ }
58
+
42
59
  tick(dt: number, _width: number, _height: number): void {
43
60
  this.#time += 0.02 * dt * this.#speed;
44
61
 
45
62
  for (const firefly of this.#fireflies) {
46
63
  const moveX = Math.sin(this.#time * firefly.freqX1 + firefly.phaseX1) * firefly.amplitudeX
47
- + Math.sin(this.#time * firefly.freqX2 + firefly.phaseX2) * firefly.amplitudeX * 0.5;
64
+ + Math.sin(this.#time * firefly.freqX2 + firefly.phaseX2) * firefly.amplitudeX * 0.5;
48
65
 
49
66
  const moveY = Math.sin(this.#time * firefly.freqY1 + firefly.phaseY1) * firefly.amplitudeY
50
- + Math.sin(this.#time * firefly.freqY2 + firefly.phaseY2) * firefly.amplitudeY * 0.5;
67
+ + Math.sin(this.#time * firefly.freqY2 + firefly.phaseY2) * firefly.amplitudeY * 0.5;
51
68
 
52
69
  firefly.x += moveX * dt / (3000 * (1 / this.#speed));
53
70
  firefly.y += moveY * dt / (3000 * (1 / this.#speed));
@@ -94,7 +111,7 @@ export class FireflyLayer extends SimulationLayer {
94
111
  ctx.globalAlpha = 1;
95
112
  }
96
113
 
97
- #parseColor(color: string): {r: number; g: number; b: number} {
114
+ #parseColor(color: string): { r: number; g: number; b: number } {
98
115
  const canvas = document.createElement('canvas');
99
116
  canvas.width = 1;
100
117
  canvas.height = 1;
@@ -101,10 +101,10 @@ export class FireflyParticle {
101
101
  this.#time += 0.02 * dt * this.#speed;
102
102
 
103
103
  const moveX = Math.sin(this.#time * this.#freqX1 + this.#phaseX1) * this.#amplitudeX * this.#bounds.width
104
- + Math.sin(this.#time * this.#freqX2 + this.#phaseX2) * this.#amplitudeX * this.#bounds.width * 0.5;
104
+ + Math.sin(this.#time * this.#freqX2 + this.#phaseX2) * this.#amplitudeX * this.#bounds.width * 0.5;
105
105
 
106
106
  const moveY = Math.sin(this.#time * this.#freqY1 + this.#phaseY1) * this.#amplitudeY * this.#bounds.height
107
- + Math.sin(this.#time * this.#freqY2 + this.#phaseY2) * this.#amplitudeY * this.#bounds.height * 0.5;
107
+ + Math.sin(this.#time * this.#freqY2 + this.#phaseY2) * this.#amplitudeY * this.#bounds.height * 0.5;
108
108
 
109
109
  this.#x += (moveX / 3000) * dt;
110
110
  this.#y += (moveY / 3000) * dt;