@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.
- package/dist/index.d.mts +317 -459
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1258 -949
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
- package/src/aurora/index.ts +9 -3
- package/src/aurora/layer.ts +57 -29
- package/src/balloons/index.ts +9 -3
- package/src/balloons/layer.ts +50 -19
- package/src/bubbles/index.ts +9 -3
- package/src/bubbles/layer.ts +30 -17
- package/src/canvas.ts +92 -2
- package/src/color.ts +11 -2
- package/src/confetti/index.ts +15 -3
- package/src/confetti/layer.ts +8 -5
- package/src/confetti/particle.ts +12 -11
- package/src/confetti/shapes.ts +84 -97
- package/src/donuts/consts.ts +2 -2
- package/src/donuts/index.ts +9 -3
- package/src/donuts/layer.ts +43 -12
- package/src/effect.ts +107 -0
- package/src/fade.ts +87 -0
- package/src/fireflies/index.ts +9 -3
- package/src/fireflies/layer.ts +26 -9
- package/src/fireflies/particle.ts +2 -2
- package/src/firepit/index.ts +9 -3
- package/src/firepit/layer.ts +26 -7
- package/src/fireworks/create-explosion.ts +237 -0
- package/src/fireworks/explosion.ts +1 -1
- package/src/fireworks/index.ts +15 -3
- package/src/fireworks/layer.ts +55 -304
- package/src/fireworks/spark.ts +2 -2
- package/src/fireworks/types.ts +2 -2
- package/src/glitter/index.ts +9 -4
- package/src/glitter/layer.ts +15 -7
- package/src/glitter/types.ts +10 -0
- package/src/index.ts +3 -4
- package/src/lanterns/index.ts +9 -4
- package/src/lanterns/layer.ts +22 -10
- package/src/lanterns/types.ts +8 -0
- package/src/layer.ts +13 -11
- package/src/leaves/index.ts +9 -4
- package/src/leaves/layer.ts +21 -14
- package/src/leaves/types.ts +9 -0
- package/src/lightning/index.ts +9 -4
- package/src/lightning/layer.ts +4 -4
- package/src/lightning/system.ts +3 -3
- package/src/lightning/types.ts +10 -2
- package/src/matrix/index.ts +9 -4
- package/src/matrix/layer.ts +15 -7
- package/src/matrix/types.ts +9 -0
- package/src/orbits/index.ts +9 -4
- package/src/orbits/layer.ts +51 -21
- package/src/orbits/types.ts +12 -1
- package/src/particles/index.ts +9 -3
- package/src/particles/layer.ts +55 -12
- package/src/petals/index.ts +9 -3
- package/src/petals/layer.ts +29 -13
- package/src/plasma/index.ts +9 -3
- package/src/plasma/layer.ts +21 -6
- package/src/rain/index.ts +9 -3
- package/src/rain/layer.ts +30 -8
- package/src/sandstorm/index.ts +9 -3
- package/src/sandstorm/layer.ts +26 -9
- package/src/scene.ts +204 -0
- package/src/shooting-stars/system.ts +26 -24
- package/src/shooting-stars/types.ts +2 -1
- package/src/simulation-canvas.ts +45 -6
- package/src/snow/index.ts +9 -3
- package/src/snow/layer.ts +24 -11
- package/src/sparklers/index.ts +13 -3
- package/src/sparklers/layer.ts +61 -15
- package/src/stars/index.ts +9 -3
- package/src/stars/layer.ts +28 -22
- package/src/streamers/index.ts +9 -3
- package/src/streamers/layer.ts +18 -6
- package/src/streamers/types.ts +1 -1
- package/src/waves/index.ts +9 -3
- package/src/waves/layer.ts +42 -45
- package/src/waves/types.ts +1 -0
- package/src/wormhole/index.ts +9 -3
- package/src/wormhole/layer.ts +22 -6
- package/src/aurora/simulation.ts +0 -19
- package/src/balloons/simulation.ts +0 -19
- package/src/bubbles/simulation.ts +0 -20
- package/src/confetti/simulation.ts +0 -27
- package/src/donuts/simulation.ts +0 -25
- package/src/fireflies/simulation.ts +0 -18
- package/src/firepit/simulation.ts +0 -17
- package/src/fireworks/simulation.ts +0 -18
- package/src/glitter/simulation.ts +0 -19
- package/src/lanterns/simulation.ts +0 -17
- package/src/layered.ts +0 -185
- package/src/leaves/simulation.ts +0 -18
- package/src/lightning/simulation.ts +0 -17
- package/src/matrix/simulation.ts +0 -18
- package/src/orbits/simulation.ts +0 -19
- package/src/particles/simulation.ts +0 -26
- package/src/petals/simulation.ts +0 -18
- package/src/plasma/simulation.ts +0 -17
- package/src/rain/simulation.ts +0 -21
- package/src/sandstorm/simulation.ts +0 -18
- package/src/snow/simulation.ts +0 -17
- package/src/sparklers/simulation.ts +0 -30
- package/src/stars/simulation.ts +0 -22
- package/src/streamers/simulation.ts +0 -16
- package/src/waves/simulation.ts +0 -18
- package/src/wormhole/simulation.ts +0 -17
package/src/confetti/particle.ts
CHANGED
|
@@ -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
|
-
- (
|
|
51
|
-
const speed = startVelocity * (0.5 +
|
|
52
|
-
const rotAngle =
|
|
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 +
|
|
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 +
|
|
64
|
-
this.#flipAngle =
|
|
65
|
-
this.#flipSpeed = 0.03 +
|
|
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 = (
|
|
70
|
-
this.#swing =
|
|
71
|
-
this.#swingAmp = 0.5 +
|
|
72
|
-
this.#swingSpeed = 0.025 +
|
|
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 {
|
package/src/confetti/shapes.ts
CHANGED
|
@@ -2,103 +2,90 @@ import type { Shape } from './types';
|
|
|
2
2
|
|
|
3
3
|
const TWO_PI = Math.PI * 2;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
bowtie
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
ribbon
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ring
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
triangle
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
+
});
|
package/src/donuts/consts.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type Mulberry32, mulberry32 } from '@basmilius/utils';
|
|
2
|
-
import type {
|
|
2
|
+
import type { DonutsConfig } from './layer';
|
|
3
3
|
|
|
4
4
|
export const MULBERRY: Mulberry32 = mulberry32(13);
|
|
5
5
|
|
|
6
|
-
export const DEFAULT_CONFIG:
|
|
6
|
+
export const DEFAULT_CONFIG: DonutsConfig = {
|
|
7
7
|
background: '#a51955',
|
|
8
8
|
collisionPadding: 20,
|
|
9
9
|
colors: ['#bd1961', '#da287c'],
|
package/src/donuts/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 };
|
package/src/donuts/layer.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
#mouseAvoidance: boolean;
|
|
27
|
+
#mouseAvoidanceRadius: number;
|
|
28
|
+
#mouseAvoidanceStrength: number;
|
|
14
29
|
readonly #radiusRange: [number, number];
|
|
15
|
-
|
|
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:
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
ctx.
|
|
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
|
+
}
|
package/src/fireflies/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
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 {
|
|
10
|
+
export type { FirefliesConfig };
|
|
4
11
|
export type { FireflyParticleConfig } from './particle';
|
|
5
|
-
export type { FireflySimulationConfig } from './simulation';
|
|
6
12
|
export type { Firefly } from './types';
|
package/src/fireflies/layer.ts
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|