@basmilius/sparkle 2.4.0 → 2.5.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 +637 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6964 -2577
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/balloons/layer.ts +4 -3
- package/src/black-hole/consts.ts +3 -0
- package/src/black-hole/index.ts +10 -0
- package/src/black-hole/layer.ts +193 -0
- package/src/black-hole/types.ts +8 -0
- package/src/boids/consts.ts +8 -0
- package/src/boids/index.ts +9 -0
- package/src/boids/layer.ts +245 -0
- package/src/boids/types.ts +7 -0
- package/src/butterflies/consts.ts +3 -0
- package/src/butterflies/index.ts +9 -0
- package/src/butterflies/layer.ts +246 -0
- package/src/butterflies/types.ts +23 -0
- package/src/caustics/consts.ts +3 -0
- package/src/caustics/index.ts +9 -0
- package/src/caustics/layer.ts +107 -0
- package/src/clouds/consts.ts +3 -0
- package/src/clouds/index.ts +9 -0
- package/src/clouds/layer.ts +167 -0
- package/src/clouds/types.ts +9 -0
- package/src/confetti/layer.ts +3 -2
- package/src/constellation/consts.ts +3 -0
- package/src/constellation/index.ts +10 -0
- package/src/constellation/layer.ts +256 -0
- package/src/constellation/types.ts +11 -0
- package/src/coral-reef/consts.ts +3 -0
- package/src/coral-reef/index.ts +10 -0
- package/src/coral-reef/layer.ts +276 -0
- package/src/coral-reef/types.ts +31 -0
- package/src/crystallization/consts.ts +3 -0
- package/src/crystallization/index.ts +10 -0
- package/src/crystallization/layer.ts +318 -0
- package/src/crystallization/types.ts +25 -0
- package/src/digital-rain/consts.ts +7 -0
- package/src/digital-rain/index.ts +10 -0
- package/src/digital-rain/layer.ts +195 -0
- package/src/digital-rain/types.ts +10 -0
- package/src/donuts/layer.ts +5 -3
- package/src/glitch/consts.ts +3 -0
- package/src/glitch/index.ts +9 -0
- package/src/glitch/layer.ts +231 -0
- package/src/glitch/types.ts +28 -0
- package/src/gradient-flow/consts.ts +3 -0
- package/src/gradient-flow/index.ts +9 -0
- package/src/gradient-flow/layer.ts +134 -0
- package/src/gradient-flow/types.ts +8 -0
- package/src/hologram/consts.ts +5 -0
- package/src/hologram/index.ts +9 -0
- package/src/hologram/layer.ts +205 -0
- package/src/hologram/types.ts +20 -0
- package/src/hyper-space/consts.ts +3 -0
- package/src/hyper-space/index.ts +10 -0
- package/src/hyper-space/layer.ts +167 -0
- package/src/hyper-space/types.ts +8 -0
- package/src/index.ts +29 -0
- package/src/interference/consts.ts +9 -0
- package/src/interference/index.ts +9 -0
- package/src/interference/layer.ts +129 -0
- package/src/kaleidoscope/consts.ts +12 -0
- package/src/kaleidoscope/index.ts +9 -0
- package/src/kaleidoscope/layer.ts +213 -0
- package/src/kaleidoscope/types.ts +19 -0
- package/src/lanterns/layer.ts +3 -2
- package/src/lava/consts.ts +3 -0
- package/src/lava/index.ts +9 -0
- package/src/lava/layer.ts +152 -0
- package/src/lava/types.ts +13 -0
- package/src/leaves/layer.ts +3 -2
- package/src/murmuration/consts.ts +3 -0
- package/src/murmuration/index.ts +10 -0
- package/src/murmuration/layer.ts +279 -0
- package/src/murmuration/types.ts +7 -0
- package/src/nebula/consts.ts +3 -0
- package/src/nebula/index.ts +10 -0
- package/src/nebula/layer.ts +150 -0
- package/src/nebula/types.ts +20 -0
- package/src/neon/consts.ts +5 -0
- package/src/neon/index.ts +9 -0
- package/src/neon/layer.ts +213 -0
- package/src/neon/types.ts +18 -0
- package/src/petals/layer.ts +3 -2
- package/src/pollen/consts.ts +3 -0
- package/src/pollen/index.ts +10 -0
- package/src/pollen/layer.ts +181 -0
- package/src/pollen/types.ts +10 -0
- package/src/popcorn/consts.ts +3 -0
- package/src/popcorn/index.ts +10 -0
- package/src/popcorn/layer.ts +218 -0
- package/src/popcorn/types.ts +13 -0
- package/src/portal/consts.ts +3 -0
- package/src/portal/index.ts +10 -0
- package/src/portal/layer.ts +251 -0
- package/src/portal/types.ts +10 -0
- package/src/pulse-grid/consts.ts +3 -0
- package/src/pulse-grid/index.ts +10 -0
- package/src/pulse-grid/layer.ts +185 -0
- package/src/pulse-grid/types.ts +8 -0
- package/src/roots/consts.ts +3 -0
- package/src/roots/index.ts +9 -0
- package/src/roots/layer.ts +218 -0
- package/src/roots/types.ts +23 -0
- package/src/smoke/consts.ts +3 -0
- package/src/smoke/index.ts +9 -0
- package/src/smoke/layer.ts +182 -0
- package/src/smoke/types.ts +14 -0
- package/src/snow/layer.ts +3 -2
- package/src/topography/consts.ts +3 -0
- package/src/topography/index.ts +9 -0
- package/src/topography/layer.ts +141 -0
- package/src/tornado/consts.ts +3 -0
- package/src/tornado/index.ts +10 -0
- package/src/tornado/layer.ts +271 -0
- package/src/tornado/types.ts +22 -0
- package/src/volcano/consts.ts +3 -0
- package/src/volcano/index.ts +10 -0
- package/src/volcano/layer.ts +261 -0
- package/src/volcano/types.ts +10 -0
- package/src/voronoi/consts.ts +3 -0
- package/src/voronoi/index.ts +10 -0
- package/src/voronoi/layer.ts +197 -0
- package/src/voronoi/types.ts +7 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { parseColor } from '../color';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { MULBERRY } from './consts';
|
|
4
|
+
import type { RootSystem, RootTip } from './types';
|
|
5
|
+
|
|
6
|
+
export interface RootsConfig {
|
|
7
|
+
readonly count?: number;
|
|
8
|
+
readonly speed?: number;
|
|
9
|
+
readonly color?: string;
|
|
10
|
+
readonly branchProbability?: number;
|
|
11
|
+
readonly maxSegments?: number;
|
|
12
|
+
readonly scale?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Roots extends Effect<RootsConfig> {
|
|
16
|
+
readonly #scale: number;
|
|
17
|
+
#speed: number;
|
|
18
|
+
readonly #colorR: number;
|
|
19
|
+
readonly #colorG: number;
|
|
20
|
+
readonly #colorB: number;
|
|
21
|
+
readonly #branchProbability: number;
|
|
22
|
+
readonly #maxSegments: number;
|
|
23
|
+
readonly #count: number;
|
|
24
|
+
#systems: RootSystem[] = [];
|
|
25
|
+
#initialized: boolean = false;
|
|
26
|
+
#width: number = 800;
|
|
27
|
+
#height: number = 600;
|
|
28
|
+
|
|
29
|
+
constructor(config: RootsConfig = {}) {
|
|
30
|
+
super();
|
|
31
|
+
|
|
32
|
+
this.#scale = config.scale ?? 1;
|
|
33
|
+
this.#speed = config.speed ?? 1;
|
|
34
|
+
this.#branchProbability = config.branchProbability ?? 0.3;
|
|
35
|
+
this.#maxSegments = config.maxSegments ?? 200;
|
|
36
|
+
this.#count = config.count ?? 5;
|
|
37
|
+
|
|
38
|
+
const parsed = parseColor(config.color ?? '#4a3728');
|
|
39
|
+
this.#colorR = parsed.r;
|
|
40
|
+
this.#colorG = parsed.g;
|
|
41
|
+
this.#colorB = parsed.b;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
configure(config: Partial<RootsConfig>): void {
|
|
45
|
+
if (config.speed !== undefined) {
|
|
46
|
+
this.#speed = config.speed;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onResize(width: number, height: number): void {
|
|
51
|
+
this.#width = width;
|
|
52
|
+
this.#height = height;
|
|
53
|
+
|
|
54
|
+
if (!this.#initialized && width > 0 && height > 0) {
|
|
55
|
+
this.#initialized = true;
|
|
56
|
+
this.#systems = [];
|
|
57
|
+
for (let i = 0; i < this.#count; i++) {
|
|
58
|
+
this.#systems.push(this.#createSystem(width, height));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
tick(dt: number, width: number, height: number): void {
|
|
64
|
+
this.#width = width;
|
|
65
|
+
this.#height = height;
|
|
66
|
+
|
|
67
|
+
const growSteps = Math.ceil(this.#speed * dt / 16 * 2);
|
|
68
|
+
|
|
69
|
+
for (const system of this.#systems) {
|
|
70
|
+
if (system.phase === 'growing') {
|
|
71
|
+
for (let step = 0; step < growSteps; step++) {
|
|
72
|
+
this.#growSystem(system);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
system.opacity -= 0.04 * this.#speed * dt / 16;
|
|
76
|
+
if (system.opacity <= 0) {
|
|
77
|
+
this.#resetSystem(system, width, height);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
draw(ctx: CanvasRenderingContext2D, _width: number, _height: number): void {
|
|
84
|
+
for (const system of this.#systems) {
|
|
85
|
+
if (system.opacity <= 0) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ctx.globalAlpha = system.opacity;
|
|
90
|
+
this.#drawSystem(ctx, system);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ctx.globalAlpha = 1;
|
|
94
|
+
ctx.resetTransform();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#growSystem(system: RootSystem): void {
|
|
98
|
+
if (system.segmentCount >= this.#maxSegments) {
|
|
99
|
+
system.phase = 'fading';
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const activeTips = system.tips.filter(tip => tip.alive);
|
|
104
|
+
if (activeTips.length === 0) {
|
|
105
|
+
system.phase = 'fading';
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const tip of activeTips) {
|
|
110
|
+
// Grow upward with slight deviation
|
|
111
|
+
const angleDeviation = (MULBERRY.next() - 0.5) * 0.6;
|
|
112
|
+
tip.angle += angleDeviation;
|
|
113
|
+
|
|
114
|
+
// Keep growing upward (bias)
|
|
115
|
+
tip.angle = tip.angle * 0.85 + (-Math.PI / 2) * 0.15;
|
|
116
|
+
|
|
117
|
+
const stepSize = (3 + MULBERRY.next() * 4) * this.#scale;
|
|
118
|
+
const newX = tip.x + Math.cos(tip.angle) * stepSize;
|
|
119
|
+
const newY = tip.y + Math.sin(tip.angle) * stepSize;
|
|
120
|
+
|
|
121
|
+
tip.points.push({ x: newX, y: newY });
|
|
122
|
+
tip.x = newX;
|
|
123
|
+
tip.y = newY;
|
|
124
|
+
system.segmentCount++;
|
|
125
|
+
|
|
126
|
+
// Branch
|
|
127
|
+
if (tip.depth < 6 && MULBERRY.next() < this.#branchProbability * 0.04 && system.segmentCount < this.#maxSegments * 0.8) {
|
|
128
|
+
const branchAngle = tip.angle + (MULBERRY.next() > 0.5 ? 1 : -1) * (0.3 + MULBERRY.next() * 0.5);
|
|
129
|
+
const newTip: RootTip = {
|
|
130
|
+
x: tip.x,
|
|
131
|
+
y: tip.y,
|
|
132
|
+
angle: branchAngle,
|
|
133
|
+
depth: tip.depth + 1,
|
|
134
|
+
points: [{ x: tip.x, y: tip.y }],
|
|
135
|
+
alive: true,
|
|
136
|
+
lineWidth: Math.max(0.5, tip.lineWidth * 0.7),
|
|
137
|
+
colorVariant: MULBERRY.next() * 0.3
|
|
138
|
+
};
|
|
139
|
+
system.tips.push(newTip);
|
|
140
|
+
system.allTips.push(newTip);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Kill tips that go off screen
|
|
144
|
+
if (newX < -50 || newX > this.#width + 50 || newY < -50) {
|
|
145
|
+
tip.alive = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#drawSystem(ctx: CanvasRenderingContext2D, system: RootSystem): void {
|
|
151
|
+
const r = this.#colorR;
|
|
152
|
+
const g = this.#colorG;
|
|
153
|
+
const b = this.#colorB;
|
|
154
|
+
|
|
155
|
+
for (const tip of system.allTips) {
|
|
156
|
+
if (tip.points.length < 2) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const darkness = tip.colorVariant;
|
|
161
|
+
const cr = Math.max(0, r - darkness * 40);
|
|
162
|
+
const cg = Math.max(0, g - darkness * 30);
|
|
163
|
+
const cb = Math.max(0, b - darkness * 20);
|
|
164
|
+
|
|
165
|
+
ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`;
|
|
166
|
+
ctx.lineWidth = tip.lineWidth * this.#scale;
|
|
167
|
+
ctx.lineCap = 'round';
|
|
168
|
+
ctx.lineJoin = 'round';
|
|
169
|
+
|
|
170
|
+
ctx.beginPath();
|
|
171
|
+
ctx.moveTo(tip.points[0].x, tip.points[0].y);
|
|
172
|
+
|
|
173
|
+
for (let i = 1; i < tip.points.length - 1; i++) {
|
|
174
|
+
const mx = (tip.points[i].x + tip.points[i + 1].x) / 2;
|
|
175
|
+
const my = (tip.points[i].y + tip.points[i + 1].y) / 2;
|
|
176
|
+
ctx.quadraticCurveTo(tip.points[i].x, tip.points[i].y, mx, my);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const last = tip.points[tip.points.length - 1];
|
|
180
|
+
ctx.lineTo(last.x, last.y);
|
|
181
|
+
ctx.stroke();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#createSystem(width: number, height: number): RootSystem {
|
|
186
|
+
const startX = width * (0.2 + MULBERRY.next() * 0.6);
|
|
187
|
+
const startY = height + 10;
|
|
188
|
+
const baseWidth = 3 + MULBERRY.next() * 3;
|
|
189
|
+
|
|
190
|
+
const rootTip: RootTip = {
|
|
191
|
+
x: startX,
|
|
192
|
+
y: startY,
|
|
193
|
+
angle: -Math.PI / 2 + (MULBERRY.next() - 0.5) * 0.3,
|
|
194
|
+
depth: 0,
|
|
195
|
+
points: [{ x: startX, y: startY }],
|
|
196
|
+
alive: true,
|
|
197
|
+
lineWidth: baseWidth,
|
|
198
|
+
colorVariant: MULBERRY.next() * 0.2
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
tips: [rootTip],
|
|
203
|
+
allTips: [rootTip],
|
|
204
|
+
segmentCount: 0,
|
|
205
|
+
phase: 'growing',
|
|
206
|
+
opacity: 0.8 + MULBERRY.next() * 0.2
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#resetSystem(system: RootSystem, width: number, height: number): void {
|
|
211
|
+
const newSystem = this.#createSystem(width, height);
|
|
212
|
+
system.tips = newSystem.tips;
|
|
213
|
+
system.allTips = newSystem.allTips;
|
|
214
|
+
system.segmentCount = 0;
|
|
215
|
+
system.phase = 'growing';
|
|
216
|
+
system.opacity = 0.8 + MULBERRY.next() * 0.2;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RootPoint = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type RootTip = {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
angle: number;
|
|
10
|
+
depth: number;
|
|
11
|
+
points: RootPoint[];
|
|
12
|
+
alive: boolean;
|
|
13
|
+
lineWidth: number;
|
|
14
|
+
colorVariant: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type RootSystem = {
|
|
18
|
+
tips: RootTip[];
|
|
19
|
+
allTips: RootTip[];
|
|
20
|
+
segmentCount: number;
|
|
21
|
+
phase: 'growing' | 'fading';
|
|
22
|
+
opacity: number;
|
|
23
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { parseColor } from '../color';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { MULBERRY } from './consts';
|
|
4
|
+
import type { SmokeParticle } from './types';
|
|
5
|
+
|
|
6
|
+
export interface SmokeConfig {
|
|
7
|
+
readonly color?: string;
|
|
8
|
+
readonly count?: number;
|
|
9
|
+
readonly scale?: number;
|
|
10
|
+
readonly speed?: number;
|
|
11
|
+
readonly spread?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SPRITE_SIZE = 128;
|
|
15
|
+
const SPRITE_CENTER = SPRITE_SIZE / 2;
|
|
16
|
+
const SPRITE_RADIUS = SPRITE_SIZE / 2;
|
|
17
|
+
const SPRITE_VARIANTS = 4;
|
|
18
|
+
|
|
19
|
+
export class Smoke extends Effect<SmokeConfig> {
|
|
20
|
+
readonly #scale: number;
|
|
21
|
+
#speed: number;
|
|
22
|
+
#count: number;
|
|
23
|
+
#spread: number;
|
|
24
|
+
#time: number = 0;
|
|
25
|
+
#particles: SmokeParticle[] = [];
|
|
26
|
+
#sprites: HTMLCanvasElement[] = [];
|
|
27
|
+
#colorR: number = 136;
|
|
28
|
+
#colorG: number = 136;
|
|
29
|
+
#colorB: number = 136;
|
|
30
|
+
|
|
31
|
+
constructor(config: SmokeConfig = {}) {
|
|
32
|
+
super();
|
|
33
|
+
|
|
34
|
+
this.#scale = config.scale ?? 1;
|
|
35
|
+
this.#speed = config.speed ?? 1;
|
|
36
|
+
this.#count = config.count ?? 40;
|
|
37
|
+
this.#spread = config.spread ?? 0.3;
|
|
38
|
+
|
|
39
|
+
const {r, g, b} = parseColor(config.color ?? '#888888');
|
|
40
|
+
this.#colorR = r;
|
|
41
|
+
this.#colorG = g;
|
|
42
|
+
this.#colorB = b;
|
|
43
|
+
|
|
44
|
+
if (innerWidth < 991) {
|
|
45
|
+
this.#count = Math.floor(this.#count / 2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.#sprites = this.#createSprites(r, g, b);
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < this.#count; ++i) {
|
|
51
|
+
this.#particles.push(this.#createParticle(true));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
configure(config: Partial<SmokeConfig>): void {
|
|
56
|
+
if (config.speed !== undefined) {
|
|
57
|
+
this.#speed = config.speed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.spread !== undefined) {
|
|
61
|
+
this.#spread = config.spread;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config.color !== undefined) {
|
|
65
|
+
const {r, g, b} = parseColor(config.color);
|
|
66
|
+
this.#colorR = r;
|
|
67
|
+
this.#colorG = g;
|
|
68
|
+
this.#colorB = b;
|
|
69
|
+
this.#sprites = this.#createSprites(r, g, b);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
tick(dt: number, _width: number, _height: number): void {
|
|
74
|
+
this.#time += 0.008 * dt;
|
|
75
|
+
|
|
76
|
+
for (let index = 0; index < this.#particles.length; index++) {
|
|
77
|
+
const particle = this.#particles[index];
|
|
78
|
+
|
|
79
|
+
particle.age += dt;
|
|
80
|
+
|
|
81
|
+
const progress = particle.age / particle.lifetime;
|
|
82
|
+
|
|
83
|
+
const turbulence = Math.sin(this.#time * particle.turbulenceSpeed + particle.turbulenceOffset) * 0.0002
|
|
84
|
+
+ Math.sin(this.#time * particle.turbulenceSpeed * 1.7 + particle.turbulenceOffset + 2.1) * 0.0001;
|
|
85
|
+
|
|
86
|
+
particle.x += (particle.vx + turbulence) * dt;
|
|
87
|
+
particle.y += particle.vy * dt;
|
|
88
|
+
particle.radius = particle.maxRadius * Math.min(1, progress * 3);
|
|
89
|
+
particle.opacity = progress < 0.15
|
|
90
|
+
? progress / 0.15 * 0.35
|
|
91
|
+
: (1 - progress) * 0.35;
|
|
92
|
+
|
|
93
|
+
if (particle.age >= particle.lifetime || particle.y < -0.3) {
|
|
94
|
+
this.#particles[index] = this.#createParticle(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
100
|
+
ctx.globalCompositeOperation = 'screen';
|
|
101
|
+
|
|
102
|
+
for (const particle of this.#particles) {
|
|
103
|
+
const px = particle.x * width;
|
|
104
|
+
const py = particle.y * height;
|
|
105
|
+
const displayRadius = particle.radius * this.#scale * Math.min(width, height) * 0.5;
|
|
106
|
+
|
|
107
|
+
if (displayRadius < 1) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ctx.globalAlpha = particle.opacity;
|
|
112
|
+
ctx.drawImage(
|
|
113
|
+
this.#sprites[particle.spriteIndex],
|
|
114
|
+
px - displayRadius,
|
|
115
|
+
py - displayRadius,
|
|
116
|
+
displayRadius * 2,
|
|
117
|
+
displayRadius * 2
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
122
|
+
ctx.globalAlpha = 1;
|
|
123
|
+
ctx.resetTransform();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#createSprites(r: number, g: number, b: number): HTMLCanvasElement[] {
|
|
127
|
+
const sprites: HTMLCanvasElement[] = [];
|
|
128
|
+
|
|
129
|
+
for (let variant = 0; variant < SPRITE_VARIANTS; variant++) {
|
|
130
|
+
const canvas = document.createElement('canvas');
|
|
131
|
+
canvas.width = SPRITE_SIZE;
|
|
132
|
+
canvas.height = SPRITE_SIZE;
|
|
133
|
+
const spriteCtx = canvas.getContext('2d')!;
|
|
134
|
+
|
|
135
|
+
const offsets = [
|
|
136
|
+
{dx: 0, dy: 0, r: SPRITE_RADIUS},
|
|
137
|
+
{dx: SPRITE_RADIUS * 0.15, dy: -SPRITE_RADIUS * 0.1, r: SPRITE_RADIUS * 0.85},
|
|
138
|
+
{dx: -SPRITE_RADIUS * 0.1, dy: SPRITE_RADIUS * 0.05, r: SPRITE_RADIUS * 0.7}
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (const offset of offsets) {
|
|
142
|
+
const cx = SPRITE_CENTER + offset.dx;
|
|
143
|
+
const cy = SPRITE_CENTER + offset.dy;
|
|
144
|
+
const gradient = spriteCtx.createRadialGradient(cx, cy, 0, cx, cy, offset.r);
|
|
145
|
+
gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.25)`);
|
|
146
|
+
gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.12)`);
|
|
147
|
+
gradient.addColorStop(0.75, `rgba(${r}, ${g}, ${b}, 0.03)`);
|
|
148
|
+
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
149
|
+
|
|
150
|
+
spriteCtx.fillStyle = gradient;
|
|
151
|
+
spriteCtx.beginPath();
|
|
152
|
+
spriteCtx.arc(cx, cy, offset.r, 0, Math.PI * 2);
|
|
153
|
+
spriteCtx.fill();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
sprites.push(canvas);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return sprites;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#createParticle(initialSpread: boolean): SmokeParticle {
|
|
163
|
+
const lifetime = (4 + MULBERRY.next() * 6) * (1 / this.#speed) * 60;
|
|
164
|
+
const startX = 0.5 + (MULBERRY.next() - 0.5) * this.#spread;
|
|
165
|
+
const startY = initialSpread ? 0.6 + MULBERRY.next() * 0.5 : 1.05;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
x: startX,
|
|
169
|
+
y: startY,
|
|
170
|
+
vx: (MULBERRY.next() - 0.5) * 0.0002,
|
|
171
|
+
vy: -(0.0008 + MULBERRY.next() * 0.001) * this.#speed,
|
|
172
|
+
age: initialSpread ? MULBERRY.next() * lifetime : 0,
|
|
173
|
+
lifetime,
|
|
174
|
+
radius: 0,
|
|
175
|
+
maxRadius: 0.15 + MULBERRY.next() * 0.25,
|
|
176
|
+
opacity: 0,
|
|
177
|
+
turbulenceOffset: MULBERRY.next() * Math.PI * 2,
|
|
178
|
+
turbulenceSpeed: 0.5 + MULBERRY.next() * 1.5,
|
|
179
|
+
spriteIndex: Math.floor(MULBERRY.next() * SPRITE_VARIANTS)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SmokeParticle {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
vx: number;
|
|
5
|
+
vy: number;
|
|
6
|
+
age: number;
|
|
7
|
+
lifetime: number;
|
|
8
|
+
radius: number;
|
|
9
|
+
maxRadius: number;
|
|
10
|
+
opacity: number;
|
|
11
|
+
turbulenceOffset: number;
|
|
12
|
+
turbulenceSpeed: number;
|
|
13
|
+
spriteIndex: number;
|
|
14
|
+
}
|
package/src/snow/layer.ts
CHANGED
|
@@ -119,7 +119,8 @@ export class Snow extends Effect<SnowConfig> {
|
|
|
119
119
|
if (snowflake.spriteIndex === 3) {
|
|
120
120
|
const cos = Math.cos(snowflake.rotation);
|
|
121
121
|
const sin = Math.sin(snowflake.rotation);
|
|
122
|
-
ctx.
|
|
122
|
+
ctx.save();
|
|
123
|
+
ctx.transform(cos, sin, -sin, cos, px, py);
|
|
123
124
|
ctx.drawImage(
|
|
124
125
|
this.#sprites[snowflake.spriteIndex],
|
|
125
126
|
-displayRadius,
|
|
@@ -127,7 +128,7 @@ export class Snow extends Effect<SnowConfig> {
|
|
|
127
128
|
displaySize,
|
|
128
129
|
displaySize
|
|
129
130
|
);
|
|
130
|
-
ctx.
|
|
131
|
+
ctx.restore();
|
|
131
132
|
} else {
|
|
132
133
|
ctx.drawImage(
|
|
133
134
|
this.#sprites[snowflake.spriteIndex],
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Topography } from './layer';
|
|
2
|
+
import type { TopographyConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createTopography(config?: TopographyConfig): Effect<TopographyConfig> {
|
|
6
|
+
return new Topography(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { TopographyConfig };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
|
|
4
|
+
export interface TopographyConfig {
|
|
5
|
+
readonly speed?: number;
|
|
6
|
+
readonly scale?: number;
|
|
7
|
+
readonly resolution?: number;
|
|
8
|
+
readonly contourSpacing?: number;
|
|
9
|
+
readonly lineWidth?: number;
|
|
10
|
+
readonly color?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Topography extends Effect<TopographyConfig> {
|
|
14
|
+
#speed: number;
|
|
15
|
+
#scale: number;
|
|
16
|
+
readonly #resolution: number;
|
|
17
|
+
readonly #contourSpacing: number;
|
|
18
|
+
readonly #lineWidth: number;
|
|
19
|
+
readonly #colorR: number;
|
|
20
|
+
readonly #colorG: number;
|
|
21
|
+
readonly #colorB: number;
|
|
22
|
+
#time: number = 0;
|
|
23
|
+
#offscreen: HTMLCanvasElement | null = null;
|
|
24
|
+
#offscreenCtx: CanvasRenderingContext2D | null = null;
|
|
25
|
+
#imageData: ImageData | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(config: TopographyConfig = {}) {
|
|
28
|
+
super();
|
|
29
|
+
|
|
30
|
+
this.#speed = config.speed ?? 0.5;
|
|
31
|
+
this.#scale = config.scale ?? 1;
|
|
32
|
+
this.#resolution = config.resolution ?? 2;
|
|
33
|
+
this.#contourSpacing = config.contourSpacing ?? 0.1;
|
|
34
|
+
this.#lineWidth = config.lineWidth ?? 1.5;
|
|
35
|
+
|
|
36
|
+
const [cr, cg, cb] = hexToRGB(config.color ?? '#2d5016');
|
|
37
|
+
this.#colorR = cr;
|
|
38
|
+
this.#colorG = cg;
|
|
39
|
+
this.#colorB = cb;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
configure(config: Partial<TopographyConfig>): void {
|
|
43
|
+
if (config.speed !== undefined) {
|
|
44
|
+
this.#speed = config.speed;
|
|
45
|
+
}
|
|
46
|
+
if (config.scale !== undefined) {
|
|
47
|
+
this.#scale = config.scale;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
tick(dt: number, _width: number, _height: number): void {
|
|
52
|
+
this.#time += 0.02 * dt * this.#speed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
56
|
+
const resolution = this.#resolution;
|
|
57
|
+
const offWidth = Math.ceil(width / resolution);
|
|
58
|
+
const offHeight = Math.ceil(height / resolution);
|
|
59
|
+
|
|
60
|
+
if (!this.#offscreen || this.#offscreen.width !== offWidth || this.#offscreen.height !== offHeight) {
|
|
61
|
+
this.#offscreen = document.createElement('canvas');
|
|
62
|
+
this.#offscreen.width = offWidth;
|
|
63
|
+
this.#offscreen.height = offHeight;
|
|
64
|
+
this.#offscreenCtx = this.#offscreen.getContext('2d');
|
|
65
|
+
this.#imageData = this.#offscreenCtx!.createImageData(offWidth, offHeight);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const data = this.#imageData!.data;
|
|
69
|
+
const time = this.#time;
|
|
70
|
+
const scale = this.#scale;
|
|
71
|
+
const spacing = this.#contourSpacing;
|
|
72
|
+
const lineWidth = this.#lineWidth;
|
|
73
|
+
const colorR = this.#colorR;
|
|
74
|
+
const colorG = this.#colorG;
|
|
75
|
+
const colorB = this.#colorB;
|
|
76
|
+
|
|
77
|
+
const freq1 = 80 * scale;
|
|
78
|
+
const freq2 = 50 * scale;
|
|
79
|
+
const freq3 = 30 * scale;
|
|
80
|
+
|
|
81
|
+
const heightField = new Float32Array(offWidth * offHeight);
|
|
82
|
+
|
|
83
|
+
for (let py = 0; py < offHeight; py++) {
|
|
84
|
+
const worldY = py * resolution;
|
|
85
|
+
|
|
86
|
+
for (let px = 0; px < offWidth; px++) {
|
|
87
|
+
const worldX = px * resolution;
|
|
88
|
+
|
|
89
|
+
const value = Math.sin(worldX / freq1 + time) * Math.sin(worldY / freq1 + time * 0.7)
|
|
90
|
+
+ 0.5 * Math.sin(worldX / freq2 + worldY / freq2 + time * 1.3)
|
|
91
|
+
+ 0.25 * Math.sin(worldX / freq3 - time * 0.5) * Math.sin(worldY / freq3 + time * 0.9);
|
|
92
|
+
|
|
93
|
+
heightField[py * offWidth + px] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (let py = 0; py < offHeight; py++) {
|
|
98
|
+
for (let px = 0; px < offWidth; px++) {
|
|
99
|
+
const index = py * offWidth + px;
|
|
100
|
+
const value = heightField[index];
|
|
101
|
+
const contourLevel = Math.floor(value / spacing);
|
|
102
|
+
|
|
103
|
+
let isContour = false;
|
|
104
|
+
|
|
105
|
+
if (px < offWidth - 1) {
|
|
106
|
+
const rightLevel = Math.floor(heightField[index + 1] / spacing);
|
|
107
|
+
if (contourLevel !== rightLevel) {
|
|
108
|
+
isContour = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!isContour && py < offHeight - 1) {
|
|
113
|
+
const belowLevel = Math.floor(heightField[index + offWidth] / spacing);
|
|
114
|
+
if (contourLevel !== belowLevel) {
|
|
115
|
+
isContour = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const offset = index * 4;
|
|
120
|
+
|
|
121
|
+
if (isContour) {
|
|
122
|
+
const alpha = Math.min(255, 255 * lineWidth);
|
|
123
|
+
data[offset] = colorR;
|
|
124
|
+
data[offset + 1] = colorG;
|
|
125
|
+
data[offset + 2] = colorB;
|
|
126
|
+
data[offset + 3] = alpha;
|
|
127
|
+
} else {
|
|
128
|
+
data[offset] = colorR;
|
|
129
|
+
data[offset + 1] = colorG;
|
|
130
|
+
data[offset + 2] = colorB;
|
|
131
|
+
data[offset + 3] = 10;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.#offscreenCtx!.putImageData(this.#imageData!, 0, 0);
|
|
137
|
+
|
|
138
|
+
ctx.imageSmoothingEnabled = true;
|
|
139
|
+
ctx.drawImage(this.#offscreen!, 0, 0, width, height);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Tornado } from './layer';
|
|
2
|
+
import type { TornadoConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createTornado(config?: TornadoConfig): Effect<TornadoConfig> {
|
|
6
|
+
return new Tornado(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { TornadoConfig };
|
|
10
|
+
export type { TornadoDebris, TornadoParticle } from './types';
|