@basmilius/sparkle 2.0.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 +1053 -28
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4840 -400
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
- package/src/aurora/consts.ts +3 -0
- package/src/aurora/index.ts +10 -0
- package/src/aurora/layer.ts +180 -0
- package/src/aurora/types.ts +13 -0
- package/src/balloons/consts.ts +3 -0
- package/src/balloons/index.ts +12 -0
- package/src/balloons/layer.ts +169 -0
- package/src/balloons/particle.ts +110 -0
- package/src/balloons/types.ts +14 -0
- package/src/bubbles/consts.ts +3 -0
- package/src/bubbles/index.ts +10 -0
- package/src/bubbles/layer.ts +246 -0
- package/src/bubbles/types.ts +21 -0
- package/src/canvas.ts +32 -1
- package/src/color.ts +19 -0
- package/src/confetti/consts.ts +13 -13
- package/src/confetti/index.ts +20 -2
- package/src/confetti/layer.ts +155 -0
- package/src/confetti/particle.ts +106 -0
- package/src/confetti/shapes.ts +104 -0
- 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 +9 -0
- package/src/donuts/layer.ts +301 -0
- package/src/effect.ts +107 -0
- package/src/fade.ts +87 -0
- package/src/fireflies/consts.ts +3 -0
- package/src/fireflies/index.ts +12 -0
- package/src/fireflies/layer.ts +169 -0
- package/src/fireflies/particle.ts +124 -0
- package/src/fireflies/types.ts +17 -0
- package/src/firepit/consts.ts +3 -0
- package/src/firepit/index.ts +10 -0
- package/src/firepit/layer.ts +193 -0
- package/src/firepit/types.ts +20 -0
- package/src/fireworks/create-explosion.ts +237 -0
- package/src/fireworks/explosion.ts +9 -9
- package/src/fireworks/firework.ts +9 -8
- package/src/fireworks/index.ts +19 -3
- package/src/fireworks/layer.ts +203 -0
- package/src/fireworks/spark.ts +9 -9
- package/src/fireworks/types.ts +2 -2
- package/src/glitter/consts.ts +13 -0
- package/src/glitter/index.ts +9 -0
- package/src/glitter/layer.ts +181 -0
- package/src/glitter/types.ts +33 -0
- package/src/index.ts +27 -0
- package/src/lanterns/consts.ts +13 -0
- package/src/lanterns/index.ts +9 -0
- package/src/lanterns/layer.ts +178 -0
- package/src/lanterns/types.ts +22 -0
- package/src/layer.ts +26 -0
- package/src/leaves/consts.ts +16 -0
- package/src/leaves/index.ts +9 -0
- package/src/leaves/layer.ts +258 -0
- package/src/leaves/types.ts +25 -0
- package/src/lightning/consts.ts +3 -0
- package/src/lightning/index.ts +11 -0
- package/src/lightning/layer.ts +41 -0
- package/src/lightning/system.ts +196 -0
- package/src/lightning/types.ts +20 -0
- package/src/matrix/consts.ts +5 -0
- package/src/matrix/index.ts +9 -0
- package/src/matrix/layer.ts +154 -0
- package/src/matrix/types.ts +17 -0
- package/src/orbits/consts.ts +13 -0
- package/src/orbits/index.ts +9 -0
- package/src/orbits/layer.ts +213 -0
- package/src/orbits/types.ts +27 -0
- package/src/particles/consts.ts +3 -0
- package/src/particles/index.ts +10 -0
- package/src/particles/layer.ts +360 -0
- package/src/particles/types.ts +10 -0
- package/src/petals/consts.ts +13 -0
- package/src/petals/index.ts +10 -0
- package/src/petals/layer.ts +174 -0
- package/src/petals/types.ts +15 -0
- package/src/plasma/consts.ts +3 -0
- package/src/plasma/index.ts +10 -0
- package/src/plasma/layer.ts +107 -0
- package/src/plasma/types.ts +5 -0
- package/src/rain/consts.ts +3 -0
- package/src/rain/index.ts +12 -0
- package/src/rain/layer.ts +194 -0
- package/src/rain/particle.ts +132 -0
- package/src/rain/types.ts +22 -0
- package/src/sandstorm/consts.ts +3 -0
- package/src/sandstorm/index.ts +10 -0
- package/src/sandstorm/layer.ts +152 -0
- package/src/sandstorm/types.ts +10 -0
- package/src/scene.ts +201 -0
- package/src/shooting-stars/index.ts +3 -0
- package/src/shooting-stars/system.ts +151 -0
- package/src/shooting-stars/types.ts +11 -0
- package/src/simulation-canvas.ts +83 -0
- package/src/snow/consts.ts +2 -2
- package/src/snow/index.ts +9 -2
- package/src/snow/{simulation.ts → layer.ts} +64 -89
- package/src/sparklers/consts.ts +3 -0
- package/src/sparklers/index.ts +16 -0
- package/src/sparklers/layer.ts +220 -0
- package/src/sparklers/particle.ts +89 -0
- package/src/sparklers/types.ts +13 -0
- package/src/stars/consts.ts +3 -0
- package/src/stars/index.ts +10 -0
- package/src/stars/layer.ts +139 -0
- package/src/stars/types.ts +12 -0
- package/src/streamers/consts.ts +14 -0
- package/src/streamers/index.ts +10 -0
- package/src/streamers/layer.ts +223 -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 +10 -0
- package/src/waves/layer.ts +164 -0
- package/src/waves/types.ts +10 -0
- package/src/wormhole/consts.ts +3 -0
- package/src/wormhole/index.ts +10 -0
- package/src/wormhole/layer.ts +197 -0
- package/src/wormhole/types.ts +10 -0
- package/src/confetti/simulation.ts +0 -221
- package/src/fireworks/simulation.ts +0 -493
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { GLITTER_COLORS, MULBERRY } from './consts';
|
|
4
|
+
import type { FallingGlitter, GlitterConfig, SettledGlitter } from './types';
|
|
5
|
+
|
|
6
|
+
export class Glitter extends Effect<GlitterConfig> {
|
|
7
|
+
readonly #scale: number;
|
|
8
|
+
readonly #size: number;
|
|
9
|
+
#speed: number;
|
|
10
|
+
#groundLevel: number;
|
|
11
|
+
readonly #maxSettled: number;
|
|
12
|
+
readonly #colorRGBs: [number, number, number][];
|
|
13
|
+
#maxCount: number;
|
|
14
|
+
#time: number = 0;
|
|
15
|
+
#falling: FallingGlitter[] = [];
|
|
16
|
+
#settled: SettledGlitter[] = [];
|
|
17
|
+
|
|
18
|
+
constructor(config: GlitterConfig = {}) {
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
this.#scale = config.scale ?? 1;
|
|
22
|
+
this.#maxCount = config.count ?? 80;
|
|
23
|
+
this.#size = (config.size ?? 4) * this.#scale;
|
|
24
|
+
this.#speed = config.speed ?? 1;
|
|
25
|
+
this.#groundLevel = config.groundLevel ?? 0.85;
|
|
26
|
+
this.#maxSettled = config.maxSettled ?? 200;
|
|
27
|
+
|
|
28
|
+
const colors = config.colors ?? GLITTER_COLORS;
|
|
29
|
+
this.#colorRGBs = colors.map(c => hexToRGB(c));
|
|
30
|
+
|
|
31
|
+
if (innerWidth < 991) {
|
|
32
|
+
this.#maxCount = Math.floor(this.#maxCount / 2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < this.#maxCount; ++i) {
|
|
36
|
+
this.#falling.push(this.#createFallingPiece(true));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
configure(config: Partial<GlitterConfig>): void {
|
|
41
|
+
if (config.speed !== undefined) {
|
|
42
|
+
this.#speed = config.speed;
|
|
43
|
+
}
|
|
44
|
+
if (config.groundLevel !== undefined) {
|
|
45
|
+
this.#groundLevel = config.groundLevel;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
tick(dt: number, _width: number, _height: number): void {
|
|
50
|
+
this.#time += 0.03 * dt;
|
|
51
|
+
|
|
52
|
+
let alive = 0;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < this.#falling.length; i++) {
|
|
55
|
+
const piece = this.#falling[i];
|
|
56
|
+
|
|
57
|
+
piece.y += piece.vy * this.#speed * dt;
|
|
58
|
+
piece.rotation += piece.rotationSpeed * dt;
|
|
59
|
+
piece.flipAngle += piece.flipSpeed * dt;
|
|
60
|
+
|
|
61
|
+
piece.sparkle = 0.3 + 0.7 * Math.max(0, Math.sin(this.#time * 3 + piece.flipAngle * 2));
|
|
62
|
+
|
|
63
|
+
if (piece.y >= this.#groundLevel) {
|
|
64
|
+
this.#settleGlitter(piece);
|
|
65
|
+
this.#falling[alive++] = this.#createFallingPiece(false);
|
|
66
|
+
} else if (piece.y > 1.1) {
|
|
67
|
+
this.#falling[alive++] = this.#createFallingPiece(false);
|
|
68
|
+
} else {
|
|
69
|
+
this.#falling[alive++] = piece;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.#falling.length = alive;
|
|
74
|
+
|
|
75
|
+
while (this.#settled.length > this.#maxSettled) {
|
|
76
|
+
this.#settled.shift();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
81
|
+
|
|
82
|
+
for (const piece of this.#settled) {
|
|
83
|
+
const px = piece.x * width;
|
|
84
|
+
const py = piece.y * height;
|
|
85
|
+
const [r, g, b] = this.#colorRGBs[piece.colorIndex % this.#colorRGBs.length];
|
|
86
|
+
|
|
87
|
+
const sparkle = 0.3 + 0.7 * Math.max(0, Math.sin(this.#time * piece.sparkleSpeed + piece.sparklePhase));
|
|
88
|
+
const alpha = 0.4 + 0.6 * sparkle;
|
|
89
|
+
|
|
90
|
+
this.#drawDiamond(ctx, px, py, piece.size, piece.rotation, r, g, b, alpha, sparkle);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const piece of this.#falling) {
|
|
94
|
+
const px = piece.x * width;
|
|
95
|
+
const py = piece.y * height;
|
|
96
|
+
const [r, g, b] = this.#colorRGBs[piece.colorIndex % this.#colorRGBs.length];
|
|
97
|
+
|
|
98
|
+
const flipFactor = Math.abs(Math.cos(piece.flipAngle));
|
|
99
|
+
const alpha = 0.5 + 0.5 * piece.sparkle;
|
|
100
|
+
|
|
101
|
+
this.#drawDiamond(ctx, px, py, piece.size, piece.rotation, r, g, b, alpha, piece.sparkle, flipFactor);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#drawDiamond(
|
|
106
|
+
ctx: CanvasRenderingContext2D,
|
|
107
|
+
cx: number,
|
|
108
|
+
cy: number,
|
|
109
|
+
size: number,
|
|
110
|
+
rotation: number,
|
|
111
|
+
r: number,
|
|
112
|
+
g: number,
|
|
113
|
+
b: number,
|
|
114
|
+
alpha: number,
|
|
115
|
+
sparkle: number,
|
|
116
|
+
flipFactor: number = 1
|
|
117
|
+
): void {
|
|
118
|
+
const halfW = size * flipFactor;
|
|
119
|
+
const halfH = size;
|
|
120
|
+
|
|
121
|
+
if (halfW < 0.3) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cos = Math.cos(rotation);
|
|
126
|
+
const sin = Math.sin(rotation);
|
|
127
|
+
|
|
128
|
+
const points = [
|
|
129
|
+
{x: 0, y: -halfH},
|
|
130
|
+
{x: halfW, y: 0},
|
|
131
|
+
{x: 0, y: halfH},
|
|
132
|
+
{x: -halfW, y: 0}
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
ctx.beginPath();
|
|
136
|
+
ctx.moveTo(cx + points[0].x * cos - points[0].y * sin, cy + points[0].x * sin + points[0].y * cos);
|
|
137
|
+
|
|
138
|
+
for (let p = 1; p < points.length; p++) {
|
|
139
|
+
ctx.lineTo(cx + points[p].x * cos - points[p].y * sin, cy + points[p].x * sin + points[p].y * cos);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ctx.closePath();
|
|
143
|
+
|
|
144
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.8})`;
|
|
145
|
+
ctx.fill();
|
|
146
|
+
|
|
147
|
+
if (sparkle > 0.5) {
|
|
148
|
+
const highlightAlpha = (sparkle - 0.5) * 2 * alpha;
|
|
149
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${highlightAlpha * 0.6})`;
|
|
150
|
+
ctx.fill();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#settleGlitter(piece: FallingGlitter): void {
|
|
155
|
+
this.#settled.push({
|
|
156
|
+
x: piece.x,
|
|
157
|
+
y: this.#groundLevel + MULBERRY.next() * 0.05,
|
|
158
|
+
size: piece.size * 0.8,
|
|
159
|
+
rotation: piece.rotation,
|
|
160
|
+
sparklePhase: MULBERRY.next() * Math.PI * 2,
|
|
161
|
+
sparkleSpeed: 0.5 + MULBERRY.next() * 2,
|
|
162
|
+
colorIndex: piece.colorIndex
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#createFallingPiece(initialSpread: boolean): FallingGlitter {
|
|
167
|
+
return {
|
|
168
|
+
x: MULBERRY.next(),
|
|
169
|
+
y: initialSpread ? MULBERRY.next() * this.#groundLevel : -0.05 - MULBERRY.next() * 0.1,
|
|
170
|
+
vy: (0.0008 + MULBERRY.next() * 0.0015) * this.#scale,
|
|
171
|
+
size: (0.5 + MULBERRY.next() * 1) * this.#size,
|
|
172
|
+
rotation: MULBERRY.next() * Math.PI * 2,
|
|
173
|
+
rotationSpeed: (MULBERRY.next() - 0.5) * 0.08,
|
|
174
|
+
flipAngle: MULBERRY.next() * Math.PI * 2,
|
|
175
|
+
flipSpeed: 0.03 + MULBERRY.next() * 0.07,
|
|
176
|
+
sparkle: MULBERRY.next(),
|
|
177
|
+
colorIndex: Math.floor(MULBERRY.next() * this.#colorRGBs.length),
|
|
178
|
+
settled: false
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface GlitterConfig {
|
|
2
|
+
readonly count?: number;
|
|
3
|
+
readonly colors?: string[];
|
|
4
|
+
readonly size?: number;
|
|
5
|
+
readonly speed?: number;
|
|
6
|
+
readonly groundLevel?: number;
|
|
7
|
+
readonly maxSettled?: number;
|
|
8
|
+
readonly scale?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type FallingGlitter = {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
vy: number;
|
|
15
|
+
size: number;
|
|
16
|
+
rotation: number;
|
|
17
|
+
rotationSpeed: number;
|
|
18
|
+
flipAngle: number;
|
|
19
|
+
flipSpeed: number;
|
|
20
|
+
sparkle: number;
|
|
21
|
+
colorIndex: number;
|
|
22
|
+
settled: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SettledGlitter = {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
size: number;
|
|
29
|
+
rotation: number;
|
|
30
|
+
sparklePhase: number;
|
|
31
|
+
sparkleSpeed: number;
|
|
32
|
+
colorIndex: number;
|
|
33
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
export * from './aurora';
|
|
2
|
+
export * from './balloons';
|
|
3
|
+
export * from './bubbles';
|
|
1
4
|
export * from './canvas';
|
|
5
|
+
export * from './color';
|
|
2
6
|
export * from './confetti';
|
|
7
|
+
export * from './donuts';
|
|
8
|
+
export * from './effect';
|
|
9
|
+
export * from './fireflies';
|
|
10
|
+
export * from './firepit';
|
|
3
11
|
export * from './fireworks';
|
|
12
|
+
export * from './glitter';
|
|
13
|
+
export * from './lanterns';
|
|
14
|
+
export * from './leaves';
|
|
15
|
+
export * from './lightning';
|
|
16
|
+
export * from './matrix';
|
|
17
|
+
export * from './orbits';
|
|
18
|
+
export * from './particles';
|
|
19
|
+
export * from './petals';
|
|
20
|
+
export * from './plasma';
|
|
21
|
+
export * from './rain';
|
|
22
|
+
export * from './sandstorm';
|
|
23
|
+
export * from './scene';
|
|
24
|
+
export * from './shooting-stars';
|
|
4
25
|
export * from './snow';
|
|
26
|
+
export * from './sparklers';
|
|
27
|
+
export * from './stars';
|
|
28
|
+
export * from './streamers';
|
|
29
|
+
export * from './trail';
|
|
30
|
+
export * from './waves';
|
|
31
|
+
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,9 @@
|
|
|
1
|
+
import { Lanterns } from './layer';
|
|
2
|
+
import type { LanternsConfig } from './types';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createLanterns(config?: LanternsConfig): Effect<LanternsConfig> {
|
|
6
|
+
return new Lanterns(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { LanternsConfig, Lantern } from './types';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { LANTERN_COLORS, MULBERRY } from './consts';
|
|
4
|
+
import type { Lantern, LanternsConfig } from './types';
|
|
5
|
+
|
|
6
|
+
export class Lanterns extends Effect<LanternsConfig> {
|
|
7
|
+
readonly #scale: number;
|
|
8
|
+
#speed: number;
|
|
9
|
+
readonly #size: number;
|
|
10
|
+
readonly #colorRGBs: [number, number, number][];
|
|
11
|
+
#maxCount: number;
|
|
12
|
+
#time: number = 0;
|
|
13
|
+
#lanterns: Lantern[] = [];
|
|
14
|
+
#sortedLanterns: Lantern[] = [];
|
|
15
|
+
#sortDirty: boolean = true;
|
|
16
|
+
|
|
17
|
+
constructor(config: LanternsConfig = {}) {
|
|
18
|
+
super();
|
|
19
|
+
|
|
20
|
+
this.#scale = config.scale ?? 1;
|
|
21
|
+
this.#maxCount = config.count ?? 25;
|
|
22
|
+
this.#size = (config.size ?? 20) * this.#scale;
|
|
23
|
+
this.#speed = config.speed ?? 0.5;
|
|
24
|
+
|
|
25
|
+
const colors = config.colors ?? LANTERN_COLORS;
|
|
26
|
+
this.#colorRGBs = colors.map(c => hexToRGB(c));
|
|
27
|
+
|
|
28
|
+
if (innerWidth < 991) {
|
|
29
|
+
this.#maxCount = Math.floor(this.#maxCount / 2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < this.#maxCount; ++i) {
|
|
33
|
+
this.#lanterns.push(this.#createLantern(true));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
configure(config: Partial<LanternsConfig>): void {
|
|
38
|
+
if (config.speed !== undefined) {
|
|
39
|
+
this.#speed = config.speed;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
tick(dt: number, width: number, height: number): void {
|
|
44
|
+
this.#time += 0.02 * dt * this.#speed;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < this.#lanterns.length; i++) {
|
|
47
|
+
const lantern = this.#lanterns[i];
|
|
48
|
+
|
|
49
|
+
lantern.y -= (lantern.vy * this.#speed * dt) / (height * 1.5);
|
|
50
|
+
|
|
51
|
+
const sway = Math.sin(this.#time * lantern.swaySpeed + lantern.swayPhase) * lantern.swayAmplitude;
|
|
52
|
+
lantern.x += sway * dt / (width * 8);
|
|
53
|
+
|
|
54
|
+
if (lantern.y < -0.15) {
|
|
55
|
+
this.#lanterns[i] = this.#createLantern(false);
|
|
56
|
+
this.#sortDirty = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
62
|
+
|
|
63
|
+
if (this.#sortDirty) {
|
|
64
|
+
this.#sortedLanterns = [...this.#lanterns].sort((a, b) => a.size - b.size);
|
|
65
|
+
this.#sortDirty = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sorted = this.#sortedLanterns;
|
|
69
|
+
|
|
70
|
+
for (const lantern of sorted) {
|
|
71
|
+
const px = lantern.x * width;
|
|
72
|
+
const py = lantern.y * height;
|
|
73
|
+
const size = lantern.size;
|
|
74
|
+
const [r, g, b] = this.#colorRGBs[lantern.colorIndex];
|
|
75
|
+
|
|
76
|
+
const glowPulse = 0.6 + 0.4 * Math.sin(this.#time * lantern.glowSpeed + lantern.glowPhase);
|
|
77
|
+
const alpha = lantern.opacity * glowPulse;
|
|
78
|
+
|
|
79
|
+
const glowRadius = size * 3;
|
|
80
|
+
const glowGradient = ctx.createRadialGradient(px, py, 0, px, py, glowRadius);
|
|
81
|
+
glowGradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha * 0.35})`);
|
|
82
|
+
glowGradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, ${alpha * 0.15})`);
|
|
83
|
+
glowGradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, ${alpha * 0.05})`);
|
|
84
|
+
glowGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
85
|
+
|
|
86
|
+
ctx.fillStyle = glowGradient;
|
|
87
|
+
ctx.beginPath();
|
|
88
|
+
ctx.arc(px, py, glowRadius, 0, Math.PI * 2);
|
|
89
|
+
ctx.fill();
|
|
90
|
+
|
|
91
|
+
ctx.setTransform(1, 0, 0, 1, px, py);
|
|
92
|
+
|
|
93
|
+
const bodyW = size * 0.8;
|
|
94
|
+
const bodyH = size;
|
|
95
|
+
const topW = bodyW * 0.6;
|
|
96
|
+
|
|
97
|
+
ctx.beginPath();
|
|
98
|
+
ctx.moveTo(-topW, -bodyH * 0.5);
|
|
99
|
+
ctx.quadraticCurveTo(-bodyW, 0, -bodyW * 0.7, bodyH * 0.5);
|
|
100
|
+
ctx.lineTo(bodyW * 0.7, bodyH * 0.5);
|
|
101
|
+
ctx.quadraticCurveTo(bodyW, 0, topW, -bodyH * 0.5);
|
|
102
|
+
ctx.closePath();
|
|
103
|
+
|
|
104
|
+
const bodyGradient = ctx.createLinearGradient(0, -bodyH * 0.5, 0, bodyH * 0.5);
|
|
105
|
+
bodyGradient.addColorStop(0, `rgba(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 30)}, ${alpha * 0.9})`);
|
|
106
|
+
bodyGradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, ${alpha * 0.85})`);
|
|
107
|
+
bodyGradient.addColorStop(1, `rgba(${Math.max(0, r - 30)}, ${Math.max(0, g - 30)}, ${Math.max(0, b - 20)}, ${alpha * 0.8})`);
|
|
108
|
+
|
|
109
|
+
ctx.fillStyle = bodyGradient;
|
|
110
|
+
ctx.fill();
|
|
111
|
+
|
|
112
|
+
ctx.beginPath();
|
|
113
|
+
ctx.moveTo(-topW * 0.7, -bodyH * 0.55);
|
|
114
|
+
ctx.lineTo(topW * 0.7, -bodyH * 0.55);
|
|
115
|
+
ctx.lineWidth = size * 0.06;
|
|
116
|
+
ctx.strokeStyle = `rgba(${Math.max(0, r - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)}, ${alpha * 0.7})`;
|
|
117
|
+
ctx.stroke();
|
|
118
|
+
|
|
119
|
+
const flameH = bodyH * 0.3;
|
|
120
|
+
const flameW = bodyW * 0.15;
|
|
121
|
+
const flameFlicker = Math.sin(this.#time * 8 + lantern.glowPhase) * flameW * 0.3;
|
|
122
|
+
|
|
123
|
+
const flameGradient = ctx.createRadialGradient(
|
|
124
|
+
flameFlicker, -flameH * 0.1, 0,
|
|
125
|
+
flameFlicker, -flameH * 0.1, flameH
|
|
126
|
+
);
|
|
127
|
+
flameGradient.addColorStop(0, `rgba(255, 255, 200, ${alpha * 0.95})`);
|
|
128
|
+
flameGradient.addColorStop(0.3, `rgba(255, 200, 80, ${alpha * 0.7})`);
|
|
129
|
+
flameGradient.addColorStop(0.7, `rgba(255, 140, 40, ${alpha * 0.3})`);
|
|
130
|
+
flameGradient.addColorStop(1, `rgba(255, 100, 20, 0)`);
|
|
131
|
+
|
|
132
|
+
ctx.beginPath();
|
|
133
|
+
ctx.moveTo(-flameW + flameFlicker, flameH * 0.2);
|
|
134
|
+
ctx.quadraticCurveTo(-flameW * 0.5 + flameFlicker, -flameH * 0.3, flameFlicker, -flameH);
|
|
135
|
+
ctx.quadraticCurveTo(flameW * 0.5 + flameFlicker, -flameH * 0.3, flameW + flameFlicker, flameH * 0.2);
|
|
136
|
+
ctx.closePath();
|
|
137
|
+
ctx.fillStyle = flameGradient;
|
|
138
|
+
ctx.fill();
|
|
139
|
+
|
|
140
|
+
const stringLen = size * 0.6;
|
|
141
|
+
const stringDrift = Math.sin(this.#time * 1.5 + lantern.swayPhase) * size * 0.1;
|
|
142
|
+
|
|
143
|
+
ctx.beginPath();
|
|
144
|
+
ctx.moveTo(0, bodyH * 0.5);
|
|
145
|
+
ctx.quadraticCurveTo(
|
|
146
|
+
stringDrift,
|
|
147
|
+
bodyH * 0.5 + stringLen * 0.5,
|
|
148
|
+
-stringDrift * 0.5,
|
|
149
|
+
bodyH * 0.5 + stringLen
|
|
150
|
+
);
|
|
151
|
+
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.4})`;
|
|
152
|
+
ctx.lineWidth = size * 0.04;
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
|
|
155
|
+
ctx.resetTransform();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#createLantern(initialSpread: boolean): Lantern {
|
|
160
|
+
const colorIndex = Math.floor(MULBERRY.next() * this.#colorRGBs.length);
|
|
161
|
+
const sizeVariation = 0.6 + MULBERRY.next() * 0.8;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
x: 0.05 + MULBERRY.next() * 0.9,
|
|
165
|
+
y: initialSpread ? MULBERRY.next() * 1.3 : 1.15 + MULBERRY.next() * 0.2,
|
|
166
|
+
vx: 0,
|
|
167
|
+
vy: 0.2 + MULBERRY.next() * 0.6,
|
|
168
|
+
size: this.#size * sizeVariation,
|
|
169
|
+
glowPhase: MULBERRY.next() * Math.PI * 2,
|
|
170
|
+
glowSpeed: 0.8 + MULBERRY.next() * 1.2,
|
|
171
|
+
swayPhase: MULBERRY.next() * Math.PI * 2,
|
|
172
|
+
swaySpeed: 0.4 + MULBERRY.next() * 0.8,
|
|
173
|
+
swayAmplitude: 0.3 + MULBERRY.next() * 0.7,
|
|
174
|
+
colorIndex,
|
|
175
|
+
opacity: 0.7 + MULBERRY.next() * 0.3
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface LanternsConfig {
|
|
2
|
+
readonly count?: number;
|
|
3
|
+
readonly colors?: string[];
|
|
4
|
+
readonly size?: number;
|
|
5
|
+
readonly speed?: number;
|
|
6
|
+
readonly scale?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type Lantern = {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
vx: number;
|
|
13
|
+
vy: number;
|
|
14
|
+
size: number;
|
|
15
|
+
glowPhase: number;
|
|
16
|
+
glowSpeed: number;
|
|
17
|
+
swayPhase: number;
|
|
18
|
+
swaySpeed: number;
|
|
19
|
+
swayAmplitude: number;
|
|
20
|
+
colorIndex: number;
|
|
21
|
+
opacity: number;
|
|
22
|
+
};
|
package/src/layer.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* Internal interface implemented by all Effect subclasses. Used by SimulationCanvas
|
|
12
|
+
* and Scene to drive rendering without depending on the generic Effect<TConfig> type.
|
|
13
|
+
*/
|
|
14
|
+
export interface SimulationLayer {
|
|
15
|
+
fade: EdgeFade | null;
|
|
16
|
+
|
|
17
|
+
tick(dt: number, width: number, height: number): void;
|
|
18
|
+
|
|
19
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void;
|
|
20
|
+
|
|
21
|
+
onResize(width: number, height: number): void;
|
|
22
|
+
|
|
23
|
+
onMount(canvas: HTMLCanvasElement): void;
|
|
24
|
+
|
|
25
|
+
onUnmount(canvas: HTMLCanvasElement): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Mulberry32, mulberry32 } from '@basmilius/utils';
|
|
2
|
+
|
|
3
|
+
export const MULBERRY: Mulberry32 = mulberry32(13);
|
|
4
|
+
|
|
5
|
+
export const LEAF_COLORS: string[] = [
|
|
6
|
+
'#c0392b', // deep red
|
|
7
|
+
'#e74c3c', // bright red
|
|
8
|
+
'#d35400', // burnt orange
|
|
9
|
+
'#e67e22', // orange
|
|
10
|
+
'#f39c12', // amber
|
|
11
|
+
'#f1c40f', // golden yellow
|
|
12
|
+
'#d4a017', // dark gold
|
|
13
|
+
'#8b4513', // saddle brown
|
|
14
|
+
'#a0522d', // sienna
|
|
15
|
+
'#6b8e23' // olive (some green still)
|
|
16
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Leaves } from './layer';
|
|
2
|
+
import type { LeavesConfig } from './types';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createLeaves(config?: LeavesConfig): Effect<LeavesConfig> {
|
|
6
|
+
return new Leaves(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { LeavesConfig, Leaf } from './types';
|