@basmilius/sparkle 2.1.0 → 2.2.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 +306 -459
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1106 -848
- 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 +12 -0
- 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/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 +201 -0
- package/src/shooting-stars/system.ts +26 -24
- package/src/shooting-stars/types.ts +2 -1
- package/src/simulation-canvas.ts +40 -4
- 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/petals/layer.ts
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Effect } from '../effect';
|
|
2
2
|
import { MULBERRY, PETAL_COLORS } from './consts';
|
|
3
|
-
import type { PetalSimulationConfig } from './simulation';
|
|
4
3
|
import type { Petal } from './types';
|
|
5
4
|
|
|
6
|
-
export
|
|
5
|
+
export interface PetalsConfig {
|
|
6
|
+
readonly count?: number;
|
|
7
|
+
readonly colors?: string[];
|
|
8
|
+
readonly size?: number;
|
|
9
|
+
readonly speed?: number;
|
|
10
|
+
readonly wind?: number;
|
|
11
|
+
readonly scale?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Petals extends Effect<PetalsConfig> {
|
|
7
15
|
readonly #scale: number;
|
|
8
16
|
readonly #size: number;
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
#speed: number;
|
|
18
|
+
#wind: number;
|
|
11
19
|
readonly #colors: string[];
|
|
12
20
|
#maxCount: number;
|
|
13
21
|
#time: number = 0;
|
|
14
22
|
#petals: Petal[] = [];
|
|
15
23
|
#sprites: HTMLCanvasElement[] = [];
|
|
16
24
|
|
|
17
|
-
constructor(config:
|
|
25
|
+
constructor(config: PetalsConfig = {}) {
|
|
18
26
|
super();
|
|
19
27
|
|
|
20
28
|
this.#scale = config.scale ?? 1;
|
|
@@ -35,14 +43,23 @@ export class PetalLayer extends SimulationLayer {
|
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
configure(config: Partial<PetalsConfig>): void {
|
|
47
|
+
if (config.speed !== undefined) {
|
|
48
|
+
this.#speed = config.speed;
|
|
49
|
+
}
|
|
50
|
+
if (config.wind !== undefined) {
|
|
51
|
+
this.#wind = config.wind;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
tick(dt: number, _width: number, height: number): void {
|
|
39
56
|
const speedFactor = (height / 540) / this.#speed;
|
|
40
57
|
|
|
41
58
|
this.#time += 0.012 * dt;
|
|
42
59
|
|
|
43
60
|
const globalWind = Math.sin(this.#time * 0.4) * 0.3
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
+ Math.sin(this.#time * 1.1 + 1.5) * 0.15
|
|
62
|
+
+ Math.sin(this.#time * 2.7) * 0.08;
|
|
46
63
|
|
|
47
64
|
for (let index = 0; index < this.#petals.length; index++) {
|
|
48
65
|
const petal = this.#petals[index];
|
|
@@ -78,11 +95,10 @@ export class PetalLayer extends SimulationLayer {
|
|
|
78
95
|
const py = petal.y * height;
|
|
79
96
|
const displaySize = petal.size * petal.depth;
|
|
80
97
|
const scaleX = Math.cos(petal.flipAngle);
|
|
98
|
+
const cos = Math.cos(petal.rotation);
|
|
99
|
+
const sin = Math.sin(petal.rotation);
|
|
81
100
|
|
|
82
|
-
ctx.
|
|
83
|
-
ctx.translate(px, py);
|
|
84
|
-
ctx.rotate(petal.rotation);
|
|
85
|
-
ctx.scale(scaleX, 1);
|
|
101
|
+
ctx.setTransform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
|
|
86
102
|
ctx.globalAlpha = 0.4 + petal.depth * 0.6;
|
|
87
103
|
ctx.drawImage(
|
|
88
104
|
this.#sprites[petal.colorIndex % this.#sprites.length],
|
|
@@ -91,9 +107,9 @@ export class PetalLayer extends SimulationLayer {
|
|
|
91
107
|
displaySize,
|
|
92
108
|
displaySize
|
|
93
109
|
);
|
|
94
|
-
ctx.restore();
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
ctx.resetTransform();
|
|
97
113
|
ctx.globalAlpha = 1;
|
|
98
114
|
}
|
|
99
115
|
|
package/src/plasma/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Plasma } from './layer';
|
|
2
|
+
import type { PlasmaConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createPlasma(config?: PlasmaConfig): Effect<PlasmaConfig> {
|
|
6
|
+
return new Plasma(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { PlasmaConfig };
|
|
4
10
|
export type { PlasmaColor } from './types';
|
package/src/plasma/layer.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { PlasmaSimulationConfig } from './simulation';
|
|
1
|
+
import { Effect } from '../effect';
|
|
3
2
|
import type { PlasmaColor } from './types';
|
|
4
3
|
|
|
4
|
+
export interface PlasmaConfig {
|
|
5
|
+
readonly speed?: number;
|
|
6
|
+
readonly scale?: number;
|
|
7
|
+
readonly resolution?: number;
|
|
8
|
+
readonly palette?: PlasmaColor[];
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
const DEFAULT_PALETTE: PlasmaColor[] = [
|
|
6
12
|
{r: 0, g: 255, b: 255},
|
|
7
13
|
{r: 255, g: 0, b: 255},
|
|
@@ -10,9 +16,9 @@ const DEFAULT_PALETTE: PlasmaColor[] = [
|
|
|
10
16
|
{r: 0, g: 255, b: 100}
|
|
11
17
|
];
|
|
12
18
|
|
|
13
|
-
export class
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
export class Plasma extends Effect<PlasmaConfig> {
|
|
20
|
+
#speed: number;
|
|
21
|
+
#scale: number;
|
|
16
22
|
readonly #resolution: number;
|
|
17
23
|
readonly #palette: PlasmaColor[];
|
|
18
24
|
#time: number = 0;
|
|
@@ -20,7 +26,7 @@ export class PlasmaLayer extends SimulationLayer {
|
|
|
20
26
|
#offscreenCtx: CanvasRenderingContext2D | null = null;
|
|
21
27
|
#imageData: ImageData | null = null;
|
|
22
28
|
|
|
23
|
-
constructor(config:
|
|
29
|
+
constructor(config: PlasmaConfig = {}) {
|
|
24
30
|
super();
|
|
25
31
|
|
|
26
32
|
this.#speed = config.speed ?? 1;
|
|
@@ -29,6 +35,15 @@ export class PlasmaLayer extends SimulationLayer {
|
|
|
29
35
|
this.#palette = config.palette ?? DEFAULT_PALETTE;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
configure(config: Partial<PlasmaConfig>): void {
|
|
39
|
+
if (config.speed !== undefined) {
|
|
40
|
+
this.#speed = config.speed;
|
|
41
|
+
}
|
|
42
|
+
if (config.scale !== undefined) {
|
|
43
|
+
this.#scale = config.scale;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
tick(dt: number, _width: number, _height: number): void {
|
|
33
48
|
this.#time += 0.02 * dt * this.#speed;
|
|
34
49
|
}
|
package/src/rain/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { Rain } from './layer';
|
|
2
|
+
import type { RainConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createRain(config?: RainConfig): Effect<RainConfig> {
|
|
6
|
+
return new Rain(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
2
9
|
export { RaindropParticle, SplashParticle } from './particle';
|
|
3
|
-
export {
|
|
10
|
+
export type { RainConfig };
|
|
4
11
|
export type { RaindropParticleConfig, SplashParticleConfig } from './particle';
|
|
5
|
-
export type { RainSimulationConfig } from './simulation';
|
|
6
12
|
export type { Raindrop, RainVariant, Splash } from './types';
|
package/src/rain/layer.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import { parseColor } from '../color';
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
3
|
import { MULBERRY } from './consts';
|
|
4
|
-
import type { RainSimulationConfig } from './simulation';
|
|
5
4
|
import type { Raindrop, RainVariant, Splash } from './types';
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
export interface RainConfig {
|
|
7
|
+
readonly variant?: RainVariant;
|
|
8
|
+
readonly drops?: number;
|
|
9
|
+
readonly wind?: number;
|
|
10
|
+
readonly speed?: number;
|
|
11
|
+
readonly splashes?: boolean;
|
|
12
|
+
readonly color?: string;
|
|
13
|
+
readonly groundLevel?: number;
|
|
14
|
+
readonly scale?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const VARIANT_PRESETS: Record<RainVariant, { drops: number; speed: number; wind: number; splashes: boolean }> = {
|
|
8
18
|
drizzle: {drops: 70, speed: 0.55, wind: 0.1, splashes: false},
|
|
9
19
|
downpour: {drops: 200, speed: 0.85, wind: 0.25, splashes: true},
|
|
10
20
|
thunderstorm: {drops: 300, speed: 1, wind: 0.4, splashes: true}
|
|
11
21
|
};
|
|
12
22
|
|
|
13
|
-
export class
|
|
23
|
+
export class Rain extends Effect<RainConfig> {
|
|
14
24
|
readonly #scale: number;
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
#speed: number;
|
|
26
|
+
#wind: number;
|
|
17
27
|
readonly #groundLevel: number;
|
|
18
|
-
|
|
28
|
+
#enableSplashes: boolean;
|
|
19
29
|
readonly #colorR: number;
|
|
20
30
|
readonly #colorG: number;
|
|
21
31
|
readonly #colorB: number;
|
|
@@ -23,7 +33,7 @@ export class RainLayer extends SimulationLayer {
|
|
|
23
33
|
#drops: Raindrop[] = [];
|
|
24
34
|
#splashes: Splash[] = [];
|
|
25
35
|
|
|
26
|
-
constructor(config:
|
|
36
|
+
constructor(config: RainConfig = {}) {
|
|
27
37
|
super();
|
|
28
38
|
|
|
29
39
|
const variant = config.variant ?? 'downpour';
|
|
@@ -50,6 +60,18 @@ export class RainLayer extends SimulationLayer {
|
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
configure(config: Partial<RainConfig>): void {
|
|
64
|
+
if (config.speed !== undefined) {
|
|
65
|
+
this.#speed = config.speed;
|
|
66
|
+
}
|
|
67
|
+
if (config.wind !== undefined) {
|
|
68
|
+
this.#wind = config.wind;
|
|
69
|
+
}
|
|
70
|
+
if (config.splashes !== undefined) {
|
|
71
|
+
this.#enableSplashes = config.splashes;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
tick(dt: number, width: number, height: number): void {
|
|
54
76
|
// Update raindrops
|
|
55
77
|
let aliveDrops = 0;
|
package/src/sandstorm/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Sandstorm } from './layer';
|
|
2
|
+
import type { SandstormConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createSandstorm(config?: SandstormConfig): Effect<SandstormConfig> {
|
|
6
|
+
return new Sandstorm(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { SandstormConfig };
|
|
4
10
|
export type { SandGrain } from './types';
|
package/src/sandstorm/layer.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Effect } from '../effect';
|
|
2
2
|
import { MULBERRY } from './consts';
|
|
3
|
-
import type { SandstormSimulationConfig } from './simulation';
|
|
4
3
|
import type { SandGrain } from './types';
|
|
5
4
|
|
|
6
|
-
export
|
|
5
|
+
export interface SandstormConfig {
|
|
6
|
+
readonly count?: number;
|
|
7
|
+
readonly wind?: number;
|
|
8
|
+
readonly turbulence?: number;
|
|
9
|
+
readonly color?: string;
|
|
10
|
+
readonly hazeOpacity?: number;
|
|
11
|
+
readonly scale?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Sandstorm extends Effect<SandstormConfig> {
|
|
7
15
|
readonly #scale: number;
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
#wind: number;
|
|
17
|
+
#turbulence: number;
|
|
10
18
|
readonly #colorR: number;
|
|
11
19
|
readonly #colorG: number;
|
|
12
20
|
readonly #colorB: number;
|
|
@@ -15,7 +23,7 @@ export class SandstormLayer extends SimulationLayer {
|
|
|
15
23
|
#time: number = 0;
|
|
16
24
|
#grains: SandGrain[] = [];
|
|
17
25
|
|
|
18
|
-
constructor(config:
|
|
26
|
+
constructor(config: SandstormConfig = {}) {
|
|
19
27
|
super();
|
|
20
28
|
|
|
21
29
|
this.#scale = config.scale ?? 1;
|
|
@@ -38,12 +46,21 @@ export class SandstormLayer extends SimulationLayer {
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
configure(config: Partial<SandstormConfig>): void {
|
|
50
|
+
if (config.wind !== undefined) {
|
|
51
|
+
this.#wind = config.wind;
|
|
52
|
+
}
|
|
53
|
+
if (config.turbulence !== undefined) {
|
|
54
|
+
this.#turbulence = config.turbulence;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
tick(dt: number, width: number, height: number): void {
|
|
42
59
|
this.#time += 0.02 * dt;
|
|
43
60
|
|
|
44
61
|
const gustX = Math.sin(this.#time * 0.3) * 0.5
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
+ Math.sin(this.#time * 0.8 + 1) * 0.3
|
|
63
|
+
+ Math.sin(this.#time * 2.1) * 0.2;
|
|
47
64
|
|
|
48
65
|
const gustY = Math.sin(this.#time * 0.5 + 2) * 0.15;
|
|
49
66
|
|
|
@@ -107,7 +124,7 @@ export class SandstormLayer extends SimulationLayer {
|
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
#parseColor(color: string): {r: number; g: number; b: number} {
|
|
127
|
+
#parseColor(color: string): { r: number; g: number; b: number } {
|
|
111
128
|
const canvas = document.createElement('canvas');
|
|
112
129
|
canvas.width = 1;
|
|
113
130
|
canvas.height = 1;
|
package/src/scene.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { LimitedFrameRateCanvas } from './canvas';
|
|
2
|
+
import { applyEdgeFade } from './fade';
|
|
3
|
+
import type { SimulationLayer } from './layer';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal canvas runner that drives all layers in a Scene.
|
|
7
|
+
*/
|
|
8
|
+
class SceneCanvas extends LimitedFrameRateCanvas {
|
|
9
|
+
readonly #layers: SimulationLayer[];
|
|
10
|
+
readonly #contextOptions: CanvasRenderingContext2DSettings;
|
|
11
|
+
#offscreen: HTMLCanvasElement | null = null;
|
|
12
|
+
#offscreenCtx: CanvasRenderingContext2D | null = null;
|
|
13
|
+
|
|
14
|
+
constructor(canvas: HTMLCanvasElement, layers: SimulationLayer[], frameRate: number, options: CanvasRenderingContext2DSettings) {
|
|
15
|
+
super(canvas, frameRate, options);
|
|
16
|
+
this.#layers = layers;
|
|
17
|
+
this.#contextOptions = options;
|
|
18
|
+
|
|
19
|
+
canvas.style.position = 'absolute';
|
|
20
|
+
canvas.style.top = '0';
|
|
21
|
+
canvas.style.left = '0';
|
|
22
|
+
canvas.style.height = '100%';
|
|
23
|
+
canvas.style.width = '100%';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
start(): void {
|
|
27
|
+
for (const layer of this.#layers) {
|
|
28
|
+
layer.onMount(this.canvas);
|
|
29
|
+
}
|
|
30
|
+
super.start();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
destroy(): void {
|
|
34
|
+
for (const layer of this.#layers) {
|
|
35
|
+
layer.onUnmount(this.canvas);
|
|
36
|
+
}
|
|
37
|
+
super.destroy();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
draw(): void {
|
|
41
|
+
this.canvas.height = this.height;
|
|
42
|
+
this.canvas.width = this.width;
|
|
43
|
+
|
|
44
|
+
const ctx = this.context;
|
|
45
|
+
ctx.clearRect(0, 0, this.width, this.height);
|
|
46
|
+
|
|
47
|
+
for (const layer of this.#layers) {
|
|
48
|
+
if (layer.fade) {
|
|
49
|
+
const offCtx = this.#getOffscreenCtx(this.width, this.height);
|
|
50
|
+
offCtx.clearRect(0, 0, this.width, this.height);
|
|
51
|
+
layer.draw(offCtx, this.width, this.height);
|
|
52
|
+
applyEdgeFade(offCtx, this.width, this.height, layer.fade);
|
|
53
|
+
ctx.drawImage(this.#offscreen!, 0, 0);
|
|
54
|
+
} else {
|
|
55
|
+
ctx.save();
|
|
56
|
+
layer.draw(ctx, this.width, this.height);
|
|
57
|
+
ctx.restore();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tick(): void {
|
|
63
|
+
const dt = (this.delta > 0 && this.delta < 200 ? this.delta / (1000 / 60) : 1) * this.speed * LimitedFrameRateCanvas.globalSpeed;
|
|
64
|
+
|
|
65
|
+
for (const layer of this.#layers) {
|
|
66
|
+
layer.tick(dt, this.width, this.height);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onResize(): void {
|
|
71
|
+
super.onResize();
|
|
72
|
+
|
|
73
|
+
if (this.#offscreen) {
|
|
74
|
+
this.#offscreen.width = this.width;
|
|
75
|
+
this.#offscreen.height = this.height;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const layer of this.#layers) {
|
|
79
|
+
layer.onResize(this.width, this.height);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#getOffscreenCtx(width: number, height: number): CanvasRenderingContext2D {
|
|
84
|
+
if (!this.#offscreen) {
|
|
85
|
+
this.#offscreen = document.createElement('canvas');
|
|
86
|
+
this.#offscreen.width = width;
|
|
87
|
+
this.#offscreen.height = height;
|
|
88
|
+
this.#offscreenCtx = this.#offscreen.getContext('2d', this.#contextOptions)!;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return this.#offscreenCtx!;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Composable canvas that renders multiple Effect layers in order (first = bottom, last = top).
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* const scene = new Scene()
|
|
100
|
+
* .mount(canvas)
|
|
101
|
+
* .layer(new Aurora({ bands: 5 }))
|
|
102
|
+
* .layer(new Stars().withFade({ bottom: 0.4 }))
|
|
103
|
+
* .start();
|
|
104
|
+
*/
|
|
105
|
+
export class Scene {
|
|
106
|
+
readonly #layers: SimulationLayer[] = [];
|
|
107
|
+
readonly #frameRate: number;
|
|
108
|
+
readonly #defaultOptions: CanvasRenderingContext2DSettings;
|
|
109
|
+
#runner: SceneCanvas | null = null;
|
|
110
|
+
|
|
111
|
+
constructor(frameRate: number = 60, options: CanvasRenderingContext2DSettings = {colorSpace: 'display-p3'}) {
|
|
112
|
+
this.#frameRate = frameRate;
|
|
113
|
+
this.#defaultOptions = options;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mount the scene to a canvas element or CSS selector.
|
|
118
|
+
*/
|
|
119
|
+
mount(canvas: HTMLCanvasElement | string, options?: CanvasRenderingContext2DSettings): this {
|
|
120
|
+
if (typeof canvas === 'string') {
|
|
121
|
+
const el = document.querySelector<HTMLCanvasElement>(canvas);
|
|
122
|
+
|
|
123
|
+
if (!el) {
|
|
124
|
+
throw new Error(`Scene.mount(): no element found for selector "${canvas}".`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
canvas = el;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.#runner?.destroy();
|
|
131
|
+
this.#runner = new SceneCanvas(canvas, this.#layers, this.#frameRate, options ?? this.#defaultOptions);
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Add an effect layer. Layers are rendered in the order they are added.
|
|
137
|
+
* If the scene is already running, the layer is mounted immediately.
|
|
138
|
+
*/
|
|
139
|
+
layer(effect: SimulationLayer): this {
|
|
140
|
+
this.#layers.push(effect);
|
|
141
|
+
|
|
142
|
+
if (this.#runner?.isTicking) {
|
|
143
|
+
effect.onMount(this.#runner.canvas);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Start the render loop.
|
|
151
|
+
*/
|
|
152
|
+
start(): this {
|
|
153
|
+
this.#runner?.start();
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Pause rendering without destroying state. Use resume() to continue.
|
|
159
|
+
*/
|
|
160
|
+
pause(): this {
|
|
161
|
+
this.#runner?.pause();
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resume rendering after pause().
|
|
167
|
+
*/
|
|
168
|
+
resume(): this {
|
|
169
|
+
this.#runner?.resume();
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Stop and destroy all layers.
|
|
175
|
+
*/
|
|
176
|
+
destroy(): void {
|
|
177
|
+
this.#runner?.destroy();
|
|
178
|
+
this.#runner = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get speed(): number {
|
|
182
|
+
return this.#runner?.speed ?? 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
set speed(value: number) {
|
|
186
|
+
if (this.#runner) {
|
|
187
|
+
this.#runner.speed = value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get isTicking(): boolean {
|
|
192
|
+
return this.#runner?.isTicking ?? false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Factory alternative to `new Scene()`. Call .mount() and .layer() on the returned instance.
|
|
198
|
+
*/
|
|
199
|
+
export function createScene(frameRate?: number, options?: CanvasRenderingContext2DSettings): Scene {
|
|
200
|
+
return new Scene(frameRate, options);
|
|
201
|
+
}
|
|
@@ -11,7 +11,6 @@ export interface ShootingStarSystemConfig {
|
|
|
11
11
|
readonly alphaRange?: number;
|
|
12
12
|
readonly decayMin?: number;
|
|
13
13
|
readonly decayRange?: number;
|
|
14
|
-
readonly verticalFade?: [number, number];
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export class ShootingStarSystem {
|
|
@@ -25,10 +24,8 @@ export class ShootingStarSystem {
|
|
|
25
24
|
readonly #alphaRange: number;
|
|
26
25
|
readonly #decayMin: number;
|
|
27
26
|
readonly #decayRange: number;
|
|
28
|
-
readonly #verticalFade: [number, number] | null;
|
|
29
27
|
readonly #rng: () => number;
|
|
30
28
|
#cooldown: number;
|
|
31
|
-
#height: number = 0;
|
|
32
29
|
#stars: ShootingStar[] = [];
|
|
33
30
|
|
|
34
31
|
constructor(config: ShootingStarSystemConfig, rng: () => number) {
|
|
@@ -42,13 +39,11 @@ export class ShootingStarSystem {
|
|
|
42
39
|
this.#alphaRange = config.alphaRange ?? 0.3;
|
|
43
40
|
this.#decayMin = config.decayMin ?? 0.008;
|
|
44
41
|
this.#decayRange = config.decayRange ?? 0.01;
|
|
45
|
-
this.#verticalFade = config.verticalFade ?? null;
|
|
46
42
|
this.#rng = rng;
|
|
47
43
|
this.#cooldown = this.#interval[0] + this.#rng() * (this.#interval[1] - this.#interval[0]);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
tick(dt: number, width: number, height: number): void {
|
|
51
|
-
this.#height = height;
|
|
52
47
|
this.#cooldown -= dt;
|
|
53
48
|
|
|
54
49
|
if (this.#cooldown <= 0) {
|
|
@@ -61,10 +56,17 @@ export class ShootingStarSystem {
|
|
|
61
56
|
for (let i = 0; i < this.#stars.length; i++) {
|
|
62
57
|
const star = this.#stars[i];
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const trail = star.trail;
|
|
60
|
+
const maxLen = this.#trailLength;
|
|
61
|
+
|
|
62
|
+
if (trail.length < maxLen) {
|
|
63
|
+
trail.push({x: star.x, y: star.y});
|
|
64
|
+
star.trailHead = trail.length - 1;
|
|
65
|
+
} else {
|
|
66
|
+
const next = (star.trailHead + 1) % maxLen;
|
|
67
|
+
trail[next].x = star.x;
|
|
68
|
+
trail[next].y = star.y;
|
|
69
|
+
star.trailHead = next;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
star.x += star.vx * this.#speed * dt;
|
|
@@ -72,9 +74,8 @@ export class ShootingStarSystem {
|
|
|
72
74
|
star.alpha -= star.decay * dt;
|
|
73
75
|
|
|
74
76
|
const inBounds = star.alpha > 0 && star.x > -50 && star.x < width + 50 && star.y < height + 50;
|
|
75
|
-
const fullyFaded = this.#verticalFade !== null && star.y / height >= this.#verticalFade[1];
|
|
76
77
|
|
|
77
|
-
if (inBounds
|
|
78
|
+
if (inBounds) {
|
|
78
79
|
this.#stars[alive++] = star;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -88,30 +89,30 @@ export class ShootingStarSystem {
|
|
|
88
89
|
ctx.globalCompositeOperation = 'lighter';
|
|
89
90
|
|
|
90
91
|
for (const star of this.#stars) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const progress = t / star.trail.length;
|
|
100
|
-
const trailAlpha = star.alpha * progress * this.#trailAlphaFactor * fadeFactor;
|
|
92
|
+
const trail = star.trail;
|
|
93
|
+
const trailLen = trail.length;
|
|
94
|
+
const isFull = trailLen === this.#trailLength;
|
|
95
|
+
const oldest = isFull ? (star.trailHead + 1) % trailLen : 0;
|
|
96
|
+
|
|
97
|
+
for (let t = 0; t < trailLen; t++) {
|
|
98
|
+
const progress = t / trailLen;
|
|
99
|
+
const trailAlpha = star.alpha * progress * this.#trailAlphaFactor;
|
|
101
100
|
const trailSize = star.size * progress * this.#scale;
|
|
102
101
|
|
|
103
102
|
if (trailAlpha < 0.01) {
|
|
104
103
|
continue;
|
|
105
104
|
}
|
|
106
105
|
|
|
106
|
+
const idx = (oldest + t) % trailLen;
|
|
107
|
+
|
|
107
108
|
ctx.globalAlpha = trailAlpha;
|
|
108
109
|
ctx.beginPath();
|
|
109
|
-
ctx.arc(
|
|
110
|
+
ctx.arc(trail[idx].x, trail[idx].y, trailSize, 0, Math.PI * 2);
|
|
110
111
|
ctx.fillStyle = `rgb(${cr}, ${cg}, ${cb})`;
|
|
111
112
|
ctx.fill();
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
const alpha = star.alpha
|
|
115
|
+
const alpha = star.alpha;
|
|
115
116
|
const headSize = star.size * 2 * this.#scale;
|
|
116
117
|
const glow = ctx.createRadialGradient(star.x, star.y, 0, star.x, star.y, headSize);
|
|
117
118
|
glow.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${alpha})`);
|
|
@@ -143,7 +144,8 @@ export class ShootingStarSystem {
|
|
|
143
144
|
alpha: this.#alphaMin + this.#rng() * this.#alphaRange,
|
|
144
145
|
size: 1.5 + this.#rng() * 2,
|
|
145
146
|
decay: this.#decayMin + this.#rng() * this.#decayRange,
|
|
146
|
-
trail: []
|
|
147
|
+
trail: [],
|
|
148
|
+
trailHead: 0
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
}
|