@basmilius/sparkle 2.0.0 → 2.1.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 +1192 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4552 -370
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/aurora/consts.ts +3 -0
- package/src/aurora/index.ts +4 -0
- package/src/aurora/layer.ts +152 -0
- package/src/aurora/simulation.ts +19 -0
- package/src/aurora/types.ts +13 -0
- package/src/balloons/consts.ts +3 -0
- package/src/balloons/index.ts +6 -0
- package/src/balloons/layer.ts +138 -0
- package/src/balloons/particle.ts +110 -0
- package/src/balloons/simulation.ts +19 -0
- package/src/balloons/types.ts +14 -0
- package/src/bubbles/consts.ts +3 -0
- package/src/bubbles/index.ts +4 -0
- package/src/bubbles/layer.ts +233 -0
- package/src/bubbles/simulation.ts +20 -0
- package/src/bubbles/types.ts +21 -0
- package/src/canvas.ts +20 -1
- package/src/color.ts +10 -0
- package/src/confetti/consts.ts +13 -13
- package/src/confetti/index.ts +6 -0
- package/src/confetti/layer.ts +152 -0
- package/src/confetti/particle.ts +105 -0
- package/src/confetti/shapes.ts +104 -0
- package/src/confetti/simulation.ts +9 -203
- package/src/confetti/types.ts +4 -1
- package/src/distance.ts +1 -1
- package/src/donuts/consts.ts +19 -0
- package/src/donuts/donut.ts +12 -0
- package/src/donuts/index.ts +3 -0
- package/src/donuts/layer.ts +270 -0
- package/src/donuts/simulation.ts +25 -0
- package/src/fireflies/consts.ts +3 -0
- package/src/fireflies/index.ts +6 -0
- package/src/fireflies/layer.ts +152 -0
- package/src/fireflies/particle.ts +124 -0
- package/src/fireflies/simulation.ts +18 -0
- package/src/fireflies/types.ts +17 -0
- package/src/firepit/consts.ts +3 -0
- package/src/firepit/index.ts +4 -0
- package/src/firepit/layer.ts +174 -0
- package/src/firepit/simulation.ts +17 -0
- package/src/firepit/types.ts +20 -0
- package/src/fireworks/explosion.ts +8 -8
- package/src/fireworks/firework.ts +9 -8
- package/src/fireworks/index.ts +6 -2
- package/src/fireworks/layer.ts +452 -0
- package/src/fireworks/simulation.ts +9 -484
- package/src/fireworks/spark.ts +7 -7
- package/src/glitter/consts.ts +13 -0
- package/src/glitter/index.ts +4 -0
- package/src/glitter/layer.ts +173 -0
- package/src/glitter/simulation.ts +19 -0
- package/src/glitter/types.ts +23 -0
- package/src/index.ts +28 -0
- package/src/lanterns/consts.ts +13 -0
- package/src/lanterns/index.ts +4 -0
- package/src/lanterns/layer.ts +166 -0
- package/src/lanterns/simulation.ts +17 -0
- package/src/lanterns/types.ts +14 -0
- package/src/layer.ts +24 -0
- package/src/layered.ts +185 -0
- package/src/leaves/consts.ts +16 -0
- package/src/leaves/index.ts +4 -0
- package/src/leaves/layer.ts +251 -0
- package/src/leaves/simulation.ts +18 -0
- package/src/leaves/types.ts +16 -0
- package/src/lightning/consts.ts +3 -0
- package/src/lightning/index.ts +6 -0
- package/src/lightning/layer.ts +41 -0
- package/src/lightning/simulation.ts +17 -0
- package/src/lightning/system.ts +196 -0
- package/src/lightning/types.ts +12 -0
- package/src/matrix/consts.ts +5 -0
- package/src/matrix/index.ts +4 -0
- package/src/matrix/layer.ts +146 -0
- package/src/matrix/simulation.ts +18 -0
- package/src/matrix/types.ts +8 -0
- package/src/orbits/consts.ts +13 -0
- package/src/orbits/index.ts +4 -0
- package/src/orbits/layer.ts +183 -0
- package/src/orbits/simulation.ts +19 -0
- package/src/orbits/types.ts +16 -0
- package/src/particles/consts.ts +3 -0
- package/src/particles/index.ts +4 -0
- package/src/particles/layer.ts +317 -0
- package/src/particles/simulation.ts +26 -0
- package/src/particles/types.ts +10 -0
- package/src/petals/consts.ts +13 -0
- package/src/petals/index.ts +4 -0
- package/src/petals/layer.ts +158 -0
- package/src/petals/simulation.ts +18 -0
- package/src/petals/types.ts +15 -0
- package/src/plasma/consts.ts +3 -0
- package/src/plasma/index.ts +4 -0
- package/src/plasma/layer.ts +92 -0
- package/src/plasma/simulation.ts +17 -0
- package/src/plasma/types.ts +5 -0
- package/src/rain/consts.ts +3 -0
- package/src/rain/index.ts +6 -0
- package/src/rain/layer.ts +172 -0
- package/src/rain/particle.ts +132 -0
- package/src/rain/simulation.ts +21 -0
- package/src/rain/types.ts +22 -0
- package/src/sandstorm/consts.ts +3 -0
- package/src/sandstorm/index.ts +4 -0
- package/src/sandstorm/layer.ts +135 -0
- package/src/sandstorm/simulation.ts +18 -0
- package/src/sandstorm/types.ts +10 -0
- package/src/shooting-stars/index.ts +3 -0
- package/src/shooting-stars/system.ts +149 -0
- package/src/shooting-stars/types.ts +10 -0
- package/src/simulation-canvas.ts +47 -0
- package/src/snow/consts.ts +2 -2
- package/src/snow/index.ts +1 -0
- package/src/snow/layer.ts +263 -0
- package/src/snow/simulation.ts +4 -288
- package/src/sparklers/consts.ts +3 -0
- package/src/sparklers/index.ts +6 -0
- package/src/sparklers/layer.ts +174 -0
- package/src/sparklers/particle.ts +89 -0
- package/src/sparklers/simulation.ts +30 -0
- package/src/sparklers/types.ts +13 -0
- package/src/stars/consts.ts +3 -0
- package/src/stars/index.ts +4 -0
- package/src/stars/layer.ts +133 -0
- package/src/stars/simulation.ts +22 -0
- package/src/stars/types.ts +12 -0
- package/src/streamers/consts.ts +14 -0
- package/src/streamers/index.ts +4 -0
- package/src/streamers/layer.ts +211 -0
- package/src/streamers/simulation.ts +16 -0
- package/src/streamers/types.ts +14 -0
- package/src/trail.ts +140 -0
- package/src/waves/consts.ts +3 -0
- package/src/waves/index.ts +4 -0
- package/src/waves/layer.ts +167 -0
- package/src/waves/simulation.ts +18 -0
- package/src/waves/types.ts +9 -0
- package/src/wormhole/consts.ts +3 -0
- package/src/wormhole/index.ts +4 -0
- package/src/wormhole/layer.ts +181 -0
- package/src/wormhole/simulation.ts +17 -0
- package/src/wormhole/types.ts +10 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { SimulationLayer } from '../layer';
|
|
3
|
+
import { GLITTER_COLORS, MULBERRY } from './consts';
|
|
4
|
+
import type { GlitterSimulationConfig } from './simulation';
|
|
5
|
+
import type { FallingGlitter, SettledGlitter } from './types';
|
|
6
|
+
|
|
7
|
+
export class GlitterLayer extends SimulationLayer {
|
|
8
|
+
readonly #scale: number;
|
|
9
|
+
readonly #size: number;
|
|
10
|
+
readonly #speed: number;
|
|
11
|
+
readonly #groundLevel: number;
|
|
12
|
+
readonly #maxSettled: number;
|
|
13
|
+
readonly #colorRGBs: [number, number, number][];
|
|
14
|
+
#maxCount: number;
|
|
15
|
+
#time: number = 0;
|
|
16
|
+
#falling: FallingGlitter[] = [];
|
|
17
|
+
#settled: SettledGlitter[] = [];
|
|
18
|
+
|
|
19
|
+
constructor(config: GlitterSimulationConfig = {}) {
|
|
20
|
+
super();
|
|
21
|
+
|
|
22
|
+
this.#scale = config.scale ?? 1;
|
|
23
|
+
this.#maxCount = config.count ?? 80;
|
|
24
|
+
this.#size = (config.size ?? 4) * this.#scale;
|
|
25
|
+
this.#speed = config.speed ?? 1;
|
|
26
|
+
this.#groundLevel = config.groundLevel ?? 0.85;
|
|
27
|
+
this.#maxSettled = config.maxSettled ?? 200;
|
|
28
|
+
|
|
29
|
+
const colors = config.colors ?? GLITTER_COLORS;
|
|
30
|
+
this.#colorRGBs = colors.map(c => hexToRGB(c));
|
|
31
|
+
|
|
32
|
+
if (innerWidth < 991) {
|
|
33
|
+
this.#maxCount = Math.floor(this.#maxCount / 2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < this.#maxCount; ++i) {
|
|
37
|
+
this.#falling.push(this.#createFallingPiece(true));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
tick(dt: number, _width: number, _height: number): void {
|
|
42
|
+
this.#time += 0.03 * dt;
|
|
43
|
+
|
|
44
|
+
let alive = 0;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < this.#falling.length; i++) {
|
|
47
|
+
const piece = this.#falling[i];
|
|
48
|
+
|
|
49
|
+
piece.y += piece.vy * this.#speed * dt;
|
|
50
|
+
piece.rotation += piece.rotationSpeed * dt;
|
|
51
|
+
piece.flipAngle += piece.flipSpeed * dt;
|
|
52
|
+
|
|
53
|
+
piece.sparkle = 0.3 + 0.7 * Math.max(0, Math.sin(this.#time * 3 + piece.flipAngle * 2));
|
|
54
|
+
|
|
55
|
+
if (piece.y >= this.#groundLevel) {
|
|
56
|
+
this.#settleGlitter(piece);
|
|
57
|
+
this.#falling[alive++] = this.#createFallingPiece(false);
|
|
58
|
+
} else if (piece.y > 1.1) {
|
|
59
|
+
this.#falling[alive++] = this.#createFallingPiece(false);
|
|
60
|
+
} else {
|
|
61
|
+
this.#falling[alive++] = piece;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.#falling.length = alive;
|
|
66
|
+
|
|
67
|
+
while (this.#settled.length > this.#maxSettled) {
|
|
68
|
+
this.#settled.shift();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
73
|
+
|
|
74
|
+
for (const piece of this.#settled) {
|
|
75
|
+
const px = piece.x * width;
|
|
76
|
+
const py = piece.y * height;
|
|
77
|
+
const [r, g, b] = this.#colorRGBs[piece.colorIndex % this.#colorRGBs.length];
|
|
78
|
+
|
|
79
|
+
const sparkle = 0.3 + 0.7 * Math.max(0, Math.sin(this.#time * piece.sparkleSpeed + piece.sparklePhase));
|
|
80
|
+
const alpha = 0.4 + 0.6 * sparkle;
|
|
81
|
+
|
|
82
|
+
this.#drawDiamond(ctx, px, py, piece.size, piece.rotation, r, g, b, alpha, sparkle);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const piece of this.#falling) {
|
|
86
|
+
const px = piece.x * width;
|
|
87
|
+
const py = piece.y * height;
|
|
88
|
+
const [r, g, b] = this.#colorRGBs[piece.colorIndex % this.#colorRGBs.length];
|
|
89
|
+
|
|
90
|
+
const flipFactor = Math.abs(Math.cos(piece.flipAngle));
|
|
91
|
+
const alpha = 0.5 + 0.5 * piece.sparkle;
|
|
92
|
+
|
|
93
|
+
this.#drawDiamond(ctx, px, py, piece.size, piece.rotation, r, g, b, alpha, piece.sparkle, flipFactor);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#drawDiamond(
|
|
98
|
+
ctx: CanvasRenderingContext2D,
|
|
99
|
+
cx: number,
|
|
100
|
+
cy: number,
|
|
101
|
+
size: number,
|
|
102
|
+
rotation: number,
|
|
103
|
+
r: number,
|
|
104
|
+
g: number,
|
|
105
|
+
b: number,
|
|
106
|
+
alpha: number,
|
|
107
|
+
sparkle: number,
|
|
108
|
+
flipFactor: number = 1
|
|
109
|
+
): void {
|
|
110
|
+
const halfW = size * flipFactor;
|
|
111
|
+
const halfH = size;
|
|
112
|
+
|
|
113
|
+
if (halfW < 0.3) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cos = Math.cos(rotation);
|
|
118
|
+
const sin = Math.sin(rotation);
|
|
119
|
+
|
|
120
|
+
const points = [
|
|
121
|
+
{x: 0, y: -halfH},
|
|
122
|
+
{x: halfW, y: 0},
|
|
123
|
+
{x: 0, y: halfH},
|
|
124
|
+
{x: -halfW, y: 0}
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.moveTo(cx + points[0].x * cos - points[0].y * sin, cy + points[0].x * sin + points[0].y * cos);
|
|
129
|
+
|
|
130
|
+
for (let p = 1; p < points.length; p++) {
|
|
131
|
+
ctx.lineTo(cx + points[p].x * cos - points[p].y * sin, cy + points[p].x * sin + points[p].y * cos);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
ctx.closePath();
|
|
135
|
+
|
|
136
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.8})`;
|
|
137
|
+
ctx.fill();
|
|
138
|
+
|
|
139
|
+
if (sparkle > 0.5) {
|
|
140
|
+
const highlightAlpha = (sparkle - 0.5) * 2 * alpha;
|
|
141
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${highlightAlpha * 0.6})`;
|
|
142
|
+
ctx.fill();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#settleGlitter(piece: FallingGlitter): void {
|
|
147
|
+
this.#settled.push({
|
|
148
|
+
x: piece.x,
|
|
149
|
+
y: this.#groundLevel + MULBERRY.next() * 0.05,
|
|
150
|
+
size: piece.size * 0.8,
|
|
151
|
+
rotation: piece.rotation,
|
|
152
|
+
sparklePhase: MULBERRY.next() * Math.PI * 2,
|
|
153
|
+
sparkleSpeed: 0.5 + MULBERRY.next() * 2,
|
|
154
|
+
colorIndex: piece.colorIndex
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#createFallingPiece(initialSpread: boolean): FallingGlitter {
|
|
159
|
+
return {
|
|
160
|
+
x: MULBERRY.next(),
|
|
161
|
+
y: initialSpread ? MULBERRY.next() * this.#groundLevel : -0.05 - MULBERRY.next() * 0.1,
|
|
162
|
+
vy: (0.0008 + MULBERRY.next() * 0.0015) * this.#scale,
|
|
163
|
+
size: (0.5 + MULBERRY.next() * 1) * this.#size,
|
|
164
|
+
rotation: MULBERRY.next() * Math.PI * 2,
|
|
165
|
+
rotationSpeed: (MULBERRY.next() - 0.5) * 0.08,
|
|
166
|
+
flipAngle: MULBERRY.next() * Math.PI * 2,
|
|
167
|
+
flipSpeed: 0.03 + MULBERRY.next() * 0.07,
|
|
168
|
+
sparkle: MULBERRY.next(),
|
|
169
|
+
colorIndex: Math.floor(MULBERRY.next() * this.#colorRGBs.length),
|
|
170
|
+
settled: false
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SimulationCanvas } from '../simulation-canvas';
|
|
2
|
+
import { GlitterLayer } from './layer';
|
|
3
|
+
|
|
4
|
+
export interface GlitterSimulationConfig {
|
|
5
|
+
readonly count?: number;
|
|
6
|
+
readonly colors?: string[];
|
|
7
|
+
readonly size?: number;
|
|
8
|
+
readonly speed?: number;
|
|
9
|
+
readonly groundLevel?: number;
|
|
10
|
+
readonly maxSettled?: number;
|
|
11
|
+
readonly scale?: number;
|
|
12
|
+
readonly canvasOptions?: CanvasRenderingContext2DSettings;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class GlitterSimulation extends SimulationCanvas {
|
|
16
|
+
constructor(canvas: HTMLCanvasElement, config: GlitterSimulationConfig = {}) {
|
|
17
|
+
super(canvas, new GlitterLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type FallingGlitter = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
vy: number;
|
|
5
|
+
size: number;
|
|
6
|
+
rotation: number;
|
|
7
|
+
rotationSpeed: number;
|
|
8
|
+
flipAngle: number;
|
|
9
|
+
flipSpeed: number;
|
|
10
|
+
sparkle: number;
|
|
11
|
+
colorIndex: number;
|
|
12
|
+
settled: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SettledGlitter = {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
size: number;
|
|
19
|
+
rotation: number;
|
|
20
|
+
sparklePhase: number;
|
|
21
|
+
sparkleSpeed: number;
|
|
22
|
+
colorIndex: number;
|
|
23
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
|
+
export * from './aurora';
|
|
2
|
+
export * from './color';
|
|
3
|
+
export * from './balloons';
|
|
4
|
+
export * from './bubbles';
|
|
1
5
|
export * from './canvas';
|
|
2
6
|
export * from './confetti';
|
|
7
|
+
export * from './donuts';
|
|
8
|
+
export * from './fireflies';
|
|
9
|
+
export * from './firepit';
|
|
3
10
|
export * from './fireworks';
|
|
11
|
+
export * from './glitter';
|
|
12
|
+
export * from './lanterns';
|
|
13
|
+
export * from './layer';
|
|
14
|
+
export * from './layered';
|
|
15
|
+
export * from './simulation-canvas';
|
|
16
|
+
export * from './leaves';
|
|
17
|
+
export * from './lightning';
|
|
18
|
+
export * from './matrix';
|
|
19
|
+
export * from './orbits';
|
|
20
|
+
export * from './particles';
|
|
21
|
+
export * from './petals';
|
|
22
|
+
export * from './plasma';
|
|
23
|
+
export * from './rain';
|
|
24
|
+
export * from './sandstorm';
|
|
25
|
+
export * from './shooting-stars';
|
|
4
26
|
export * from './snow';
|
|
27
|
+
export * from './sparklers';
|
|
28
|
+
export * from './stars';
|
|
29
|
+
export * from './streamers';
|
|
30
|
+
export * from './trail';
|
|
31
|
+
export * from './waves';
|
|
32
|
+
export * from './wormhole';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Mulberry32, mulberry32 } from '@basmilius/utils';
|
|
2
|
+
|
|
3
|
+
export const MULBERRY: Mulberry32 = mulberry32(13);
|
|
4
|
+
|
|
5
|
+
export const LANTERN_COLORS: string[] = [
|
|
6
|
+
'#ff6b35', // warm orange
|
|
7
|
+
'#ff8c42', // light orange
|
|
8
|
+
'#ffd166', // golden
|
|
9
|
+
'#ffb347', // amber
|
|
10
|
+
'#e85d04', // deep orange
|
|
11
|
+
'#f4845f', // coral
|
|
12
|
+
'#c1121f' // red
|
|
13
|
+
];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { SimulationLayer } from '../layer';
|
|
3
|
+
import { LANTERN_COLORS, MULBERRY } from './consts';
|
|
4
|
+
import type { LanternSimulationConfig } from './simulation';
|
|
5
|
+
import type { Lantern } from './types';
|
|
6
|
+
|
|
7
|
+
export class LanternLayer extends SimulationLayer {
|
|
8
|
+
readonly #scale: number;
|
|
9
|
+
readonly #speed: number;
|
|
10
|
+
readonly #size: number;
|
|
11
|
+
readonly #colorRGBs: [number, number, number][];
|
|
12
|
+
#maxCount: number;
|
|
13
|
+
#time: number = 0;
|
|
14
|
+
#lanterns: Lantern[] = [];
|
|
15
|
+
|
|
16
|
+
constructor(config: LanternSimulationConfig = {}) {
|
|
17
|
+
super();
|
|
18
|
+
|
|
19
|
+
this.#scale = config.scale ?? 1;
|
|
20
|
+
this.#maxCount = config.count ?? 25;
|
|
21
|
+
this.#size = (config.size ?? 20) * this.#scale;
|
|
22
|
+
this.#speed = config.speed ?? 0.5;
|
|
23
|
+
|
|
24
|
+
const colors = config.colors ?? LANTERN_COLORS;
|
|
25
|
+
this.#colorRGBs = colors.map(c => hexToRGB(c));
|
|
26
|
+
|
|
27
|
+
if (innerWidth < 991) {
|
|
28
|
+
this.#maxCount = Math.floor(this.#maxCount / 2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < this.#maxCount; ++i) {
|
|
32
|
+
this.#lanterns.push(this.#createLantern(true));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
tick(dt: number, width: number, height: number): void {
|
|
37
|
+
this.#time += 0.02 * dt * this.#speed;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < this.#lanterns.length; i++) {
|
|
40
|
+
const lantern = this.#lanterns[i];
|
|
41
|
+
|
|
42
|
+
lantern.y -= (lantern.vy * this.#speed * dt) / (height * 1.5);
|
|
43
|
+
|
|
44
|
+
const sway = Math.sin(this.#time * lantern.swaySpeed + lantern.swayPhase) * lantern.swayAmplitude;
|
|
45
|
+
lantern.x += sway * dt / (width * 8);
|
|
46
|
+
|
|
47
|
+
if (lantern.y < -0.15) {
|
|
48
|
+
this.#lanterns[i] = this.#createLantern(false);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
54
|
+
|
|
55
|
+
const sorted = [...this.#lanterns].sort((a, b) => a.size - b.size);
|
|
56
|
+
|
|
57
|
+
for (const lantern of sorted) {
|
|
58
|
+
const px = lantern.x * width;
|
|
59
|
+
const py = lantern.y * height;
|
|
60
|
+
const size = lantern.size;
|
|
61
|
+
const [r, g, b] = this.#colorRGBs[lantern.colorIndex];
|
|
62
|
+
|
|
63
|
+
const glowPulse = 0.6 + 0.4 * Math.sin(this.#time * lantern.glowSpeed + lantern.glowPhase);
|
|
64
|
+
const alpha = lantern.opacity * glowPulse;
|
|
65
|
+
|
|
66
|
+
const glowRadius = size * 3;
|
|
67
|
+
const glowGradient = ctx.createRadialGradient(px, py, 0, px, py, glowRadius);
|
|
68
|
+
glowGradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha * 0.35})`);
|
|
69
|
+
glowGradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, ${alpha * 0.15})`);
|
|
70
|
+
glowGradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, ${alpha * 0.05})`);
|
|
71
|
+
glowGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
72
|
+
|
|
73
|
+
ctx.fillStyle = glowGradient;
|
|
74
|
+
ctx.beginPath();
|
|
75
|
+
ctx.arc(px, py, glowRadius, 0, Math.PI * 2);
|
|
76
|
+
ctx.fill();
|
|
77
|
+
|
|
78
|
+
ctx.save();
|
|
79
|
+
ctx.translate(px, py);
|
|
80
|
+
|
|
81
|
+
const bodyW = size * 0.8;
|
|
82
|
+
const bodyH = size;
|
|
83
|
+
const topW = bodyW * 0.6;
|
|
84
|
+
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
ctx.moveTo(-topW, -bodyH * 0.5);
|
|
87
|
+
ctx.quadraticCurveTo(-bodyW, 0, -bodyW * 0.7, bodyH * 0.5);
|
|
88
|
+
ctx.lineTo(bodyW * 0.7, bodyH * 0.5);
|
|
89
|
+
ctx.quadraticCurveTo(bodyW, 0, topW, -bodyH * 0.5);
|
|
90
|
+
ctx.closePath();
|
|
91
|
+
|
|
92
|
+
const bodyGradient = ctx.createLinearGradient(0, -bodyH * 0.5, 0, bodyH * 0.5);
|
|
93
|
+
bodyGradient.addColorStop(0, `rgba(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 30)}, ${alpha * 0.9})`);
|
|
94
|
+
bodyGradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, ${alpha * 0.85})`);
|
|
95
|
+
bodyGradient.addColorStop(1, `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 20)}, ${alpha * 0.8})`);
|
|
96
|
+
|
|
97
|
+
ctx.fillStyle = bodyGradient;
|
|
98
|
+
ctx.fill();
|
|
99
|
+
|
|
100
|
+
ctx.beginPath();
|
|
101
|
+
ctx.moveTo(-topW * 0.7, -bodyH * 0.55);
|
|
102
|
+
ctx.lineTo(topW * 0.7, -bodyH * 0.55);
|
|
103
|
+
ctx.lineWidth = size * 0.06;
|
|
104
|
+
ctx.strokeStyle = `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, ${alpha * 0.7})`;
|
|
105
|
+
ctx.stroke();
|
|
106
|
+
|
|
107
|
+
const flameH = bodyH * 0.3;
|
|
108
|
+
const flameW = bodyW * 0.15;
|
|
109
|
+
const flameFlicker = Math.sin(this.#time * 8 + lantern.glowPhase) * flameW * 0.3;
|
|
110
|
+
|
|
111
|
+
const flameGradient = ctx.createRadialGradient(
|
|
112
|
+
flameFlicker, -flameH * 0.1, 0,
|
|
113
|
+
flameFlicker, -flameH * 0.1, flameH
|
|
114
|
+
);
|
|
115
|
+
flameGradient.addColorStop(0, `rgba(255, 255, 200, ${alpha * 0.95})`);
|
|
116
|
+
flameGradient.addColorStop(0.3, `rgba(255, 200, 80, ${alpha * 0.7})`);
|
|
117
|
+
flameGradient.addColorStop(0.7, `rgba(255, 140, 40, ${alpha * 0.3})`);
|
|
118
|
+
flameGradient.addColorStop(1, `rgba(255, 100, 20, 0)`);
|
|
119
|
+
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.moveTo(-flameW + flameFlicker, flameH * 0.2);
|
|
122
|
+
ctx.quadraticCurveTo(-flameW * 0.5 + flameFlicker, -flameH * 0.3, flameFlicker, -flameH);
|
|
123
|
+
ctx.quadraticCurveTo(flameW * 0.5 + flameFlicker, -flameH * 0.3, flameW + flameFlicker, flameH * 0.2);
|
|
124
|
+
ctx.closePath();
|
|
125
|
+
ctx.fillStyle = flameGradient;
|
|
126
|
+
ctx.fill();
|
|
127
|
+
|
|
128
|
+
const stringLen = size * 0.6;
|
|
129
|
+
const stringDrift = Math.sin(this.#time * 1.5 + lantern.swayPhase) * size * 0.1;
|
|
130
|
+
|
|
131
|
+
ctx.beginPath();
|
|
132
|
+
ctx.moveTo(0, bodyH * 0.5);
|
|
133
|
+
ctx.quadraticCurveTo(
|
|
134
|
+
stringDrift,
|
|
135
|
+
bodyH * 0.5 + stringLen * 0.5,
|
|
136
|
+
-stringDrift * 0.5,
|
|
137
|
+
bodyH * 0.5 + stringLen
|
|
138
|
+
);
|
|
139
|
+
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.4})`;
|
|
140
|
+
ctx.lineWidth = size * 0.04;
|
|
141
|
+
ctx.stroke();
|
|
142
|
+
|
|
143
|
+
ctx.restore();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#createLantern(initialSpread: boolean): Lantern {
|
|
148
|
+
const colorIndex = Math.floor(MULBERRY.next() * this.#colorRGBs.length);
|
|
149
|
+
const sizeVariation = 0.6 + MULBERRY.next() * 0.8;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
x: 0.05 + MULBERRY.next() * 0.9,
|
|
153
|
+
y: initialSpread ? MULBERRY.next() * 1.3 : 1.15 + MULBERRY.next() * 0.2,
|
|
154
|
+
vx: 0,
|
|
155
|
+
vy: 0.2 + MULBERRY.next() * 0.6,
|
|
156
|
+
size: this.#size * sizeVariation,
|
|
157
|
+
glowPhase: MULBERRY.next() * Math.PI * 2,
|
|
158
|
+
glowSpeed: 0.8 + MULBERRY.next() * 1.2,
|
|
159
|
+
swayPhase: MULBERRY.next() * Math.PI * 2,
|
|
160
|
+
swaySpeed: 0.4 + MULBERRY.next() * 0.8,
|
|
161
|
+
swayAmplitude: 0.3 + MULBERRY.next() * 0.7,
|
|
162
|
+
colorIndex,
|
|
163
|
+
opacity: 0.7 + MULBERRY.next() * 0.3
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SimulationCanvas } from '../simulation-canvas';
|
|
2
|
+
import { LanternLayer } from './layer';
|
|
3
|
+
|
|
4
|
+
export interface LanternSimulationConfig {
|
|
5
|
+
readonly count?: number;
|
|
6
|
+
readonly colors?: string[];
|
|
7
|
+
readonly size?: number;
|
|
8
|
+
readonly speed?: number;
|
|
9
|
+
readonly scale?: number;
|
|
10
|
+
readonly canvasOptions?: CanvasRenderingContext2DSettings;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class LanternSimulation extends SimulationCanvas {
|
|
14
|
+
constructor(canvas: HTMLCanvasElement, config: LanternSimulationConfig = {}) {
|
|
15
|
+
super(canvas, new LanternLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Lantern = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
vx: number;
|
|
5
|
+
vy: number;
|
|
6
|
+
size: number;
|
|
7
|
+
glowPhase: number;
|
|
8
|
+
glowSpeed: number;
|
|
9
|
+
swayPhase: number;
|
|
10
|
+
swaySpeed: number;
|
|
11
|
+
swayAmplitude: number;
|
|
12
|
+
colorIndex: number;
|
|
13
|
+
opacity: number;
|
|
14
|
+
};
|
package/src/layer.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type EdgeFadeSide = number | [number, number];
|
|
2
|
+
|
|
3
|
+
export type EdgeFade = {
|
|
4
|
+
readonly top?: EdgeFadeSide;
|
|
5
|
+
readonly bottom?: EdgeFadeSide;
|
|
6
|
+
readonly left?: EdgeFadeSide;
|
|
7
|
+
readonly right?: EdgeFadeSide;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export abstract class SimulationLayer {
|
|
11
|
+
fade: EdgeFade | null = null;
|
|
12
|
+
|
|
13
|
+
abstract tick(dt: number, width: number, height: number): void;
|
|
14
|
+
abstract draw(ctx: CanvasRenderingContext2D, width: number, height: number): void;
|
|
15
|
+
|
|
16
|
+
onResize(_width: number, _height: number): void {}
|
|
17
|
+
onMount(_canvas: HTMLCanvasElement): void {}
|
|
18
|
+
onUnmount(_canvas: HTMLCanvasElement): void {}
|
|
19
|
+
|
|
20
|
+
withFade(fade: EdgeFade): this {
|
|
21
|
+
this.fade = fade;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/layered.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { LimitedFrameRateCanvas } from './canvas';
|
|
2
|
+
import type { EdgeFade, EdgeFadeSide, SimulationLayer } from './layer';
|
|
3
|
+
|
|
4
|
+
function parseSide(side: EdgeFadeSide): [number, number] {
|
|
5
|
+
return typeof side === 'number' ? [0, side] : side;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function applyEdgeFade(ctx: CanvasRenderingContext2D, width: number, height: number, fade: EdgeFade): void {
|
|
10
|
+
ctx.globalCompositeOperation = 'destination-out';
|
|
11
|
+
|
|
12
|
+
if (fade.top !== undefined) {
|
|
13
|
+
const [near, far] = parseSide(fade.top);
|
|
14
|
+
const nearPx = near * height;
|
|
15
|
+
const farPx = far * height;
|
|
16
|
+
|
|
17
|
+
if (nearPx > 0) {
|
|
18
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
19
|
+
ctx.fillRect(0, 0, width, nearPx);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (farPx > nearPx) {
|
|
23
|
+
const gradient = ctx.createLinearGradient(0, nearPx, 0, farPx);
|
|
24
|
+
gradient.addColorStop(0, 'rgba(0,0,0,1)');
|
|
25
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
|
26
|
+
ctx.fillStyle = gradient;
|
|
27
|
+
ctx.fillRect(0, nearPx, width, farPx - nearPx);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (fade.bottom !== undefined) {
|
|
32
|
+
const [near, far] = parseSide(fade.bottom);
|
|
33
|
+
const nearPx = near * height;
|
|
34
|
+
const farPx = far * height;
|
|
35
|
+
|
|
36
|
+
if (nearPx > 0) {
|
|
37
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
38
|
+
ctx.fillRect(0, height - nearPx, width, nearPx);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (farPx > nearPx) {
|
|
42
|
+
const gradient = ctx.createLinearGradient(0, height - farPx, 0, height - nearPx);
|
|
43
|
+
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
|
44
|
+
gradient.addColorStop(1, 'rgba(0,0,0,1)');
|
|
45
|
+
ctx.fillStyle = gradient;
|
|
46
|
+
ctx.fillRect(0, height - farPx, width, farPx - nearPx);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (fade.left !== undefined) {
|
|
51
|
+
const [near, far] = parseSide(fade.left);
|
|
52
|
+
const nearPx = near * width;
|
|
53
|
+
const farPx = far * width;
|
|
54
|
+
|
|
55
|
+
if (nearPx > 0) {
|
|
56
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
57
|
+
ctx.fillRect(0, 0, nearPx, height);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (farPx > nearPx) {
|
|
61
|
+
const gradient = ctx.createLinearGradient(nearPx, 0, farPx, 0);
|
|
62
|
+
gradient.addColorStop(0, 'rgba(0,0,0,1)');
|
|
63
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
|
64
|
+
ctx.fillStyle = gradient;
|
|
65
|
+
ctx.fillRect(nearPx, 0, farPx - nearPx, height);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (fade.right !== undefined) {
|
|
70
|
+
const [near, far] = parseSide(fade.right);
|
|
71
|
+
const nearPx = near * width;
|
|
72
|
+
const farPx = far * width;
|
|
73
|
+
|
|
74
|
+
if (nearPx > 0) {
|
|
75
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
76
|
+
ctx.fillRect(width - nearPx, 0, nearPx, height);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (farPx > nearPx) {
|
|
80
|
+
const gradient = ctx.createLinearGradient(width - farPx, 0, width - nearPx, 0);
|
|
81
|
+
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
|
82
|
+
gradient.addColorStop(1, 'rgba(0,0,0,1)');
|
|
83
|
+
ctx.fillStyle = gradient;
|
|
84
|
+
ctx.fillRect(width - farPx, 0, farPx - nearPx, height);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class LayeredSimulation extends LimitedFrameRateCanvas {
|
|
92
|
+
readonly #layers: SimulationLayer[] = [];
|
|
93
|
+
readonly #contextOptions: CanvasRenderingContext2DSettings;
|
|
94
|
+
#offscreen: HTMLCanvasElement | null = null;
|
|
95
|
+
#offscreenCtx: CanvasRenderingContext2D | null = null;
|
|
96
|
+
|
|
97
|
+
constructor(canvas: HTMLCanvasElement, frameRate: number = 60, options: CanvasRenderingContext2DSettings = {colorSpace: 'display-p3'}) {
|
|
98
|
+
super(canvas, frameRate, options);
|
|
99
|
+
this.#contextOptions = options;
|
|
100
|
+
|
|
101
|
+
canvas.style.position = 'absolute';
|
|
102
|
+
canvas.style.top = '0';
|
|
103
|
+
canvas.style.left = '0';
|
|
104
|
+
canvas.style.height = '100%';
|
|
105
|
+
canvas.style.width = '100%';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
add(layer: SimulationLayer): this {
|
|
109
|
+
this.#layers.push(layer);
|
|
110
|
+
|
|
111
|
+
if (this.isTicking) {
|
|
112
|
+
layer.onMount(this.canvas);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
start(): void {
|
|
119
|
+
for (const layer of this.#layers) {
|
|
120
|
+
layer.onMount(this.canvas);
|
|
121
|
+
}
|
|
122
|
+
super.start();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
destroy(): void {
|
|
126
|
+
for (const layer of this.#layers) {
|
|
127
|
+
layer.onUnmount(this.canvas);
|
|
128
|
+
}
|
|
129
|
+
super.destroy();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
draw(): void {
|
|
133
|
+
this.canvas.height = this.height;
|
|
134
|
+
this.canvas.width = this.width;
|
|
135
|
+
|
|
136
|
+
const ctx = this.context;
|
|
137
|
+
ctx.clearRect(0, 0, this.width, this.height);
|
|
138
|
+
|
|
139
|
+
for (const layer of this.#layers) {
|
|
140
|
+
if (layer.fade) {
|
|
141
|
+
const offCtx = this.#getOffscreenCtx(this.width, this.height);
|
|
142
|
+
offCtx.clearRect(0, 0, this.width, this.height);
|
|
143
|
+
layer.draw(offCtx, this.width, this.height);
|
|
144
|
+
applyEdgeFade(offCtx, this.width, this.height, layer.fade);
|
|
145
|
+
ctx.drawImage(this.#offscreen!, 0, 0);
|
|
146
|
+
} else {
|
|
147
|
+
ctx.save();
|
|
148
|
+
layer.draw(ctx, this.width, this.height);
|
|
149
|
+
ctx.restore();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
tick(): void {
|
|
155
|
+
const dt = (this.delta > 0 && this.delta < 200 ? this.delta / (1000 / 60) : 1) * this.speed * LimitedFrameRateCanvas.globalSpeed;
|
|
156
|
+
|
|
157
|
+
for (const layer of this.#layers) {
|
|
158
|
+
layer.tick(dt, this.width, this.height);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onResize(): void {
|
|
163
|
+
super.onResize();
|
|
164
|
+
|
|
165
|
+
if (this.#offscreen) {
|
|
166
|
+
this.#offscreen.width = this.width;
|
|
167
|
+
this.#offscreen.height = this.height;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const layer of this.#layers) {
|
|
171
|
+
layer.onResize(this.width, this.height);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#getOffscreenCtx(width: number, height: number): CanvasRenderingContext2D {
|
|
176
|
+
if (!this.#offscreen) {
|
|
177
|
+
this.#offscreen = document.createElement('canvas');
|
|
178
|
+
this.#offscreen.width = width;
|
|
179
|
+
this.#offscreen.height = height;
|
|
180
|
+
this.#offscreenCtx = this.#offscreen.getContext('2d', this.#contextOptions)!;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return this.#offscreenCtx!;
|
|
184
|
+
}
|
|
185
|
+
}
|