@basmilius/sparkle 2.3.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,318 @@
|
|
|
1
|
+
import { Effect } from '../effect';
|
|
2
|
+
import { MULBERRY } from './consts';
|
|
3
|
+
import type { CrystalBranch, CrystalSeed } from './types';
|
|
4
|
+
|
|
5
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
6
|
+
const TAU = Math.PI * 2;
|
|
7
|
+
const MAX_BRANCHES_PER_SEED = 80;
|
|
8
|
+
|
|
9
|
+
export interface CrystallizationConfig {
|
|
10
|
+
readonly seeds?: number;
|
|
11
|
+
readonly speed?: number;
|
|
12
|
+
readonly branchAngle?: number;
|
|
13
|
+
readonly maxDepth?: number;
|
|
14
|
+
readonly color?: string;
|
|
15
|
+
readonly scale?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const enum SeedPhase {
|
|
19
|
+
Growing,
|
|
20
|
+
Holding,
|
|
21
|
+
Fading
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Crystallization extends Effect<CrystallizationConfig> {
|
|
25
|
+
readonly #scale: number;
|
|
26
|
+
readonly #seedCount: number;
|
|
27
|
+
readonly #branchAngle: number;
|
|
28
|
+
readonly #maxDepth: number;
|
|
29
|
+
#speed: number;
|
|
30
|
+
#seeds: CrystalSeed[] = [];
|
|
31
|
+
#time: number = 0;
|
|
32
|
+
#width: number = 0;
|
|
33
|
+
#height: number = 0;
|
|
34
|
+
#colorR: number;
|
|
35
|
+
#colorG: number;
|
|
36
|
+
#colorB: number;
|
|
37
|
+
|
|
38
|
+
constructor(config: CrystallizationConfig = {}) {
|
|
39
|
+
super();
|
|
40
|
+
|
|
41
|
+
this.#scale = config.scale ?? 1;
|
|
42
|
+
this.#seedCount = config.seeds ?? 5;
|
|
43
|
+
this.#speed = config.speed ?? 1;
|
|
44
|
+
this.#branchAngle = (config.branchAngle ?? 60) * DEG_TO_RAD;
|
|
45
|
+
this.#maxDepth = config.maxDepth ?? 5;
|
|
46
|
+
|
|
47
|
+
const {r, g, b} = this.#parseColor(config.color ?? '#88ccff');
|
|
48
|
+
this.#colorR = r;
|
|
49
|
+
this.#colorG = g;
|
|
50
|
+
this.#colorB = b;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
configure(config: Partial<CrystallizationConfig>): void {
|
|
54
|
+
if (config.speed !== undefined) {
|
|
55
|
+
this.#speed = config.speed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onResize(width: number, height: number): void {
|
|
60
|
+
this.#width = width;
|
|
61
|
+
this.#height = height;
|
|
62
|
+
this.#seeds = [];
|
|
63
|
+
|
|
64
|
+
for (let idx = 0; idx < this.#seedCount; ++idx) {
|
|
65
|
+
this.#seeds.push(this.#createSeed(idx * 1.5));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
tick(dt: number, _width: number, _height: number): void {
|
|
70
|
+
this.#time += 0.02 * dt * this.#speed;
|
|
71
|
+
const growSpeed = 0.8 * dt * this.#speed;
|
|
72
|
+
|
|
73
|
+
for (let idx = 0; idx < this.#seeds.length; ++idx) {
|
|
74
|
+
const seed = this.#seeds[idx];
|
|
75
|
+
|
|
76
|
+
if (seed.delay > 0) {
|
|
77
|
+
seed.delay -= 0.02 * dt * this.#speed;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (seed.phase === SeedPhase.Growing) {
|
|
82
|
+
const allGrown = this.#tickBranches(seed, seed.branches, growSpeed);
|
|
83
|
+
|
|
84
|
+
if (allGrown) {
|
|
85
|
+
seed.phase = SeedPhase.Holding;
|
|
86
|
+
seed.holdTimer = 120;
|
|
87
|
+
}
|
|
88
|
+
} else if (seed.phase === SeedPhase.Holding) {
|
|
89
|
+
seed.holdTimer -= dt * this.#speed;
|
|
90
|
+
|
|
91
|
+
if (seed.holdTimer <= 0) {
|
|
92
|
+
seed.phase = SeedPhase.Fading;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
seed.alpha -= 0.008 * dt * this.#speed;
|
|
96
|
+
|
|
97
|
+
if (seed.alpha <= 0) {
|
|
98
|
+
this.#seeds[idx] = this.#createSeed(0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
draw(ctx: CanvasRenderingContext2D, _width: number, _height: number): void {
|
|
105
|
+
const cr = this.#colorR;
|
|
106
|
+
const cg = this.#colorG;
|
|
107
|
+
const cb = this.#colorB;
|
|
108
|
+
|
|
109
|
+
ctx.lineCap = 'round';
|
|
110
|
+
|
|
111
|
+
for (const seed of this.#seeds) {
|
|
112
|
+
if (seed.delay > 0 || seed.alpha <= 0) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const baseAlpha = seed.alpha;
|
|
117
|
+
|
|
118
|
+
this.#drawBranches(ctx, seed.branches, baseAlpha, cr, cg, cb);
|
|
119
|
+
this.#drawSparkles(ctx, seed.branches, baseAlpha, seed.sparklePhase);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ctx.globalAlpha = 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#tickBranches(seed: CrystalSeed, branches: CrystalBranch[], growSpeed: number): boolean {
|
|
126
|
+
let allGrown = true;
|
|
127
|
+
|
|
128
|
+
for (const branch of branches) {
|
|
129
|
+
if (branch.growing) {
|
|
130
|
+
branch.currentLength += growSpeed * (1 + branch.depth * 0.3);
|
|
131
|
+
|
|
132
|
+
if (branch.currentLength >= branch.targetLength) {
|
|
133
|
+
branch.currentLength = branch.targetLength;
|
|
134
|
+
branch.growing = false;
|
|
135
|
+
branch.grown = true;
|
|
136
|
+
|
|
137
|
+
if (branch.depth < this.#maxDepth && seed.branchCount < MAX_BRANCHES_PER_SEED) {
|
|
138
|
+
this.#spawnChildren(seed, branch);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
allGrown = false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (branch.children.length > 0) {
|
|
146
|
+
if (!this.#tickBranches(seed, branch.children, growSpeed)) {
|
|
147
|
+
allGrown = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!branch.grown) {
|
|
152
|
+
allGrown = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return allGrown;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#drawBranches(ctx: CanvasRenderingContext2D, branches: CrystalBranch[], baseAlpha: number, cr: number, cg: number, cb: number): void {
|
|
160
|
+
for (const branch of branches) {
|
|
161
|
+
if (branch.currentLength <= 0) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const endX = branch.x + Math.cos(branch.angle) * branch.currentLength;
|
|
166
|
+
const endY = branch.y + Math.sin(branch.angle) * branch.currentLength;
|
|
167
|
+
const depthFade = 1 - branch.depth * 0.12;
|
|
168
|
+
|
|
169
|
+
ctx.globalAlpha = baseAlpha * 0.12 * depthFade;
|
|
170
|
+
ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`;
|
|
171
|
+
ctx.lineWidth = branch.width + 4 * this.#scale;
|
|
172
|
+
ctx.beginPath();
|
|
173
|
+
ctx.moveTo(branch.x, branch.y);
|
|
174
|
+
ctx.lineTo(endX, endY);
|
|
175
|
+
ctx.stroke();
|
|
176
|
+
|
|
177
|
+
ctx.globalAlpha = baseAlpha * 0.8 * depthFade;
|
|
178
|
+
ctx.lineWidth = branch.width;
|
|
179
|
+
ctx.beginPath();
|
|
180
|
+
ctx.moveTo(branch.x, branch.y);
|
|
181
|
+
ctx.lineTo(endX, endY);
|
|
182
|
+
ctx.stroke();
|
|
183
|
+
|
|
184
|
+
if (branch.children.length > 0) {
|
|
185
|
+
this.#drawBranches(ctx, branch.children, baseAlpha, cr, cg, cb);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#drawSparkles(ctx: CanvasRenderingContext2D, branches: CrystalBranch[], baseAlpha: number, phase: number): void {
|
|
191
|
+
for (const branch of branches) {
|
|
192
|
+
if (branch.currentLength <= 0) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (branch.growing || (branch.grown && branch.children.length === 0)) {
|
|
197
|
+
const tipX = branch.x + Math.cos(branch.angle) * branch.currentLength;
|
|
198
|
+
const tipY = branch.y + Math.sin(branch.angle) * branch.currentLength;
|
|
199
|
+
const sparkle = 0.5 + 0.5 * Math.sin(this.#time * 10 + phase + branch.angle * 3);
|
|
200
|
+
const radius = (1 + sparkle * 1.5) * this.#scale;
|
|
201
|
+
|
|
202
|
+
ctx.globalAlpha = baseAlpha * sparkle * 0.8;
|
|
203
|
+
ctx.fillStyle = '#ffffff';
|
|
204
|
+
ctx.beginPath();
|
|
205
|
+
ctx.arc(tipX, tipY, radius, 0, TAU);
|
|
206
|
+
ctx.fill();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (branch.children.length > 0) {
|
|
210
|
+
this.#drawSparkles(ctx, branch.children, baseAlpha, phase);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#createSeed(delay: number): CrystalSeed {
|
|
216
|
+
const seed: CrystalSeed = {
|
|
217
|
+
x: MULBERRY.next() * this.#width,
|
|
218
|
+
y: MULBERRY.next() * this.#height,
|
|
219
|
+
branches: [],
|
|
220
|
+
sparklePhase: MULBERRY.next() * TAU,
|
|
221
|
+
alpha: 1,
|
|
222
|
+
phase: SeedPhase.Growing,
|
|
223
|
+
holdTimer: 0,
|
|
224
|
+
delay,
|
|
225
|
+
branchCount: 0
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const baseLength = (30 + MULBERRY.next() * 40) * this.#scale;
|
|
229
|
+
|
|
230
|
+
for (let branchIdx = 0; branchIdx < 6; ++branchIdx) {
|
|
231
|
+
const angle = (TAU / 6) * branchIdx + MULBERRY.next() * 0.1;
|
|
232
|
+
|
|
233
|
+
seed.branches.push({
|
|
234
|
+
x: seed.x,
|
|
235
|
+
y: seed.y,
|
|
236
|
+
angle,
|
|
237
|
+
length: 0,
|
|
238
|
+
targetLength: baseLength + MULBERRY.next() * 20 * this.#scale,
|
|
239
|
+
currentLength: 0,
|
|
240
|
+
depth: 0,
|
|
241
|
+
children: [],
|
|
242
|
+
width: (2.5 + MULBERRY.next()) * this.#scale,
|
|
243
|
+
growing: true,
|
|
244
|
+
grown: false
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
seed.branchCount += 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return seed;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#spawnChildren(seed: CrystalSeed, parent: CrystalBranch): void {
|
|
254
|
+
const tipX = parent.x + Math.cos(parent.angle) * parent.targetLength;
|
|
255
|
+
const tipY = parent.y + Math.sin(parent.angle) * parent.targetLength;
|
|
256
|
+
const childLength = parent.targetLength * (0.5 + MULBERRY.next() * 0.2);
|
|
257
|
+
const childWidth = Math.max(0.5 * this.#scale, parent.width * 0.65);
|
|
258
|
+
const nextDepth = parent.depth + 1;
|
|
259
|
+
|
|
260
|
+
const spawnChance = 1 - nextDepth * 0.15;
|
|
261
|
+
|
|
262
|
+
const angles = [
|
|
263
|
+
parent.angle + this.#branchAngle,
|
|
264
|
+
parent.angle - this.#branchAngle
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
for (const angle of angles) {
|
|
268
|
+
if (MULBERRY.next() > spawnChance || seed.branchCount >= MAX_BRANCHES_PER_SEED) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parent.children.push({
|
|
273
|
+
x: tipX,
|
|
274
|
+
y: tipY,
|
|
275
|
+
angle,
|
|
276
|
+
length: 0,
|
|
277
|
+
targetLength: childLength + MULBERRY.next() * 10 * this.#scale,
|
|
278
|
+
currentLength: 0,
|
|
279
|
+
depth: nextDepth,
|
|
280
|
+
children: [],
|
|
281
|
+
width: childWidth,
|
|
282
|
+
growing: true,
|
|
283
|
+
grown: false
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
seed.branchCount += 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (MULBERRY.next() > 0.5 && seed.branchCount < MAX_BRANCHES_PER_SEED) {
|
|
290
|
+
parent.children.push({
|
|
291
|
+
x: tipX,
|
|
292
|
+
y: tipY,
|
|
293
|
+
angle: parent.angle + (MULBERRY.next() - 0.5) * 0.15,
|
|
294
|
+
length: 0,
|
|
295
|
+
targetLength: childLength * 0.7 + MULBERRY.next() * 8 * this.#scale,
|
|
296
|
+
currentLength: 0,
|
|
297
|
+
depth: nextDepth,
|
|
298
|
+
children: [],
|
|
299
|
+
width: childWidth,
|
|
300
|
+
growing: true,
|
|
301
|
+
grown: false
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
seed.branchCount += 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#parseColor(color: string): { r: number; g: number; b: number } {
|
|
309
|
+
const canvas = document.createElement('canvas');
|
|
310
|
+
canvas.width = 1;
|
|
311
|
+
canvas.height = 1;
|
|
312
|
+
const ctx = canvas.getContext('2d')!;
|
|
313
|
+
ctx.fillStyle = color;
|
|
314
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
315
|
+
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
316
|
+
return {r: data[0], g: data[1], b: data[2]};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type CrystalBranch = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
angle: number;
|
|
5
|
+
length: number;
|
|
6
|
+
targetLength: number;
|
|
7
|
+
currentLength: number;
|
|
8
|
+
depth: number;
|
|
9
|
+
children: CrystalBranch[];
|
|
10
|
+
width: number;
|
|
11
|
+
growing: boolean;
|
|
12
|
+
grown: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CrystalSeed = {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
branches: CrystalBranch[];
|
|
19
|
+
sparklePhase: number;
|
|
20
|
+
alpha: number;
|
|
21
|
+
phase: number;
|
|
22
|
+
holdTimer: number;
|
|
23
|
+
delay: number;
|
|
24
|
+
branchCount: number;
|
|
25
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DigitalRain } from './layer';
|
|
2
|
+
import type { DigitalRainConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createDigitalRain(config?: DigitalRainConfig): Effect<DigitalRainConfig> {
|
|
6
|
+
return new DigitalRain(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { DigitalRainConfig };
|
|
10
|
+
export type { DigitalRainColumn, DigitalRainMode } from './types';
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { BINARY_CHARS, HEX_CHARS, MIXED_CHARS, MULBERRY } from './consts';
|
|
4
|
+
import type { DigitalRainColumn, DigitalRainMode } from './types';
|
|
5
|
+
|
|
6
|
+
export interface DigitalRainConfig {
|
|
7
|
+
readonly speed?: number;
|
|
8
|
+
readonly fontSize?: number;
|
|
9
|
+
readonly columns?: number;
|
|
10
|
+
readonly mode?: DigitalRainMode;
|
|
11
|
+
readonly color?: string;
|
|
12
|
+
readonly trailLength?: number;
|
|
13
|
+
readonly scale?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class DigitalRain extends Effect<DigitalRainConfig> {
|
|
17
|
+
#speed: number;
|
|
18
|
+
#trailLength: number;
|
|
19
|
+
readonly #fontSize: number;
|
|
20
|
+
readonly #mode: DigitalRainMode;
|
|
21
|
+
readonly #colorRGB: [number, number, number];
|
|
22
|
+
readonly #scale: number;
|
|
23
|
+
#maxColumns: number;
|
|
24
|
+
#columns: DigitalRainColumn[] = [];
|
|
25
|
+
#respawnTimers: number[] = [];
|
|
26
|
+
#width: number = 960;
|
|
27
|
+
#height: number = 540;
|
|
28
|
+
#initialized: boolean = false;
|
|
29
|
+
|
|
30
|
+
constructor(config: DigitalRainConfig = {}) {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
this.#scale = config.scale ?? 1;
|
|
34
|
+
this.#speed = config.speed ?? 1;
|
|
35
|
+
this.#fontSize = (config.fontSize ?? 14) * this.#scale;
|
|
36
|
+
this.#maxColumns = config.columns ?? 0;
|
|
37
|
+
this.#mode = config.mode ?? 'hex';
|
|
38
|
+
this.#colorRGB = hexToRGB(config.color ?? '#00ffaa');
|
|
39
|
+
this.#trailLength = config.trailLength ?? 20;
|
|
40
|
+
|
|
41
|
+
if (innerWidth < 991 && this.#maxColumns > 0) {
|
|
42
|
+
this.#maxColumns = Math.floor(this.#maxColumns / 2);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onResize(width: number, height: number): void {
|
|
47
|
+
this.#width = width;
|
|
48
|
+
this.#height = height;
|
|
49
|
+
|
|
50
|
+
if (!this.#initialized) {
|
|
51
|
+
this.#initialized = true;
|
|
52
|
+
this.#columns = [];
|
|
53
|
+
this.#respawnTimers = [];
|
|
54
|
+
|
|
55
|
+
const columnWidth = this.#fontSize;
|
|
56
|
+
const totalSlots = Math.floor(width / columnWidth);
|
|
57
|
+
let columnCount: number;
|
|
58
|
+
|
|
59
|
+
if (this.#maxColumns === 0) {
|
|
60
|
+
columnCount = totalSlots;
|
|
61
|
+
} else {
|
|
62
|
+
columnCount = Math.min(this.#maxColumns, totalSlots);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (innerWidth < 991 && this.#maxColumns === 0) {
|
|
66
|
+
columnCount = Math.floor(columnCount / 2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < columnCount; ++i) {
|
|
70
|
+
const column = this.#createColumn(totalSlots, height);
|
|
71
|
+
this.#columns.push(column);
|
|
72
|
+
this.#respawnTimers.push(0);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
configure(config: Partial<DigitalRainConfig>): void {
|
|
78
|
+
if (config.speed !== undefined) {
|
|
79
|
+
this.#speed = config.speed;
|
|
80
|
+
}
|
|
81
|
+
if (config.trailLength !== undefined) {
|
|
82
|
+
this.#trailLength = config.trailLength;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
tick(dt: number, width: number, height: number): void {
|
|
87
|
+
this.#width = width;
|
|
88
|
+
this.#height = height;
|
|
89
|
+
|
|
90
|
+
const columnWidth = this.#fontSize;
|
|
91
|
+
const totalSlots = Math.floor(width / columnWidth);
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < this.#columns.length; ++i) {
|
|
94
|
+
if (this.#respawnTimers[i] > 0) {
|
|
95
|
+
this.#respawnTimers[i] -= dt;
|
|
96
|
+
|
|
97
|
+
if (this.#respawnTimers[i] <= 0) {
|
|
98
|
+
this.#columns[i] = this.#createColumn(totalSlots, height);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const column = this.#columns[i];
|
|
105
|
+
|
|
106
|
+
column.y += column.speed * this.#speed * dt;
|
|
107
|
+
column.life += dt;
|
|
108
|
+
|
|
109
|
+
// Randomly change characters as they fall
|
|
110
|
+
for (let ci = 0; ci < column.chars.length; ++ci) {
|
|
111
|
+
if (MULBERRY.next() < 0.04) {
|
|
112
|
+
column.chars[ci] = this.#randomChar();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const topOfTrail = column.y - (column.chars.length - 1) * this.#fontSize;
|
|
117
|
+
|
|
118
|
+
if (topOfTrail > height) {
|
|
119
|
+
this.#respawnTimers[i] = 10 + MULBERRY.next() * 60;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
125
|
+
ctx.fillStyle = 'rgb(0, 0, 0)';
|
|
126
|
+
ctx.fillRect(0, 0, width, height);
|
|
127
|
+
|
|
128
|
+
const [cr, cg, cb] = this.#colorRGB;
|
|
129
|
+
|
|
130
|
+
ctx.font = `${this.#fontSize}px monospace`;
|
|
131
|
+
ctx.textAlign = 'center';
|
|
132
|
+
|
|
133
|
+
for (const column of this.#columns) {
|
|
134
|
+
const charCount = column.chars.length;
|
|
135
|
+
|
|
136
|
+
for (let ci = 0; ci < charCount; ++ci) {
|
|
137
|
+
const charY = column.y - (charCount - 1 - ci) * this.#fontSize;
|
|
138
|
+
|
|
139
|
+
if (charY < -this.#fontSize || charY > height + this.#fontSize) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const isHead = ci === charCount - 1;
|
|
144
|
+
|
|
145
|
+
if (isHead) {
|
|
146
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
|
147
|
+
} else {
|
|
148
|
+
const trailProgress = ci / (charCount - 1);
|
|
149
|
+
const alpha = trailProgress * 0.8 + 0.05;
|
|
150
|
+
ctx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, ${alpha})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ctx.fillText(column.chars[ci], column.x, charY);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#randomChar(): string {
|
|
159
|
+
let charset: string;
|
|
160
|
+
|
|
161
|
+
switch (this.#mode) {
|
|
162
|
+
case 'binary':
|
|
163
|
+
charset = BINARY_CHARS;
|
|
164
|
+
break;
|
|
165
|
+
case 'hex':
|
|
166
|
+
charset = HEX_CHARS;
|
|
167
|
+
break;
|
|
168
|
+
case 'mixed':
|
|
169
|
+
charset = MIXED_CHARS;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return charset[Math.floor(MULBERRY.next() * charset.length)];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#createColumn(totalSlots: number, height: number): DigitalRainColumn {
|
|
177
|
+
const columnWidth = this.#fontSize;
|
|
178
|
+
const slot = Math.floor(MULBERRY.next() * totalSlots);
|
|
179
|
+
const length = Math.floor(this.#trailLength * 0.5 + MULBERRY.next() * this.#trailLength);
|
|
180
|
+
const chars: string[] = [];
|
|
181
|
+
|
|
182
|
+
for (let ci = 0; ci < length; ++ci) {
|
|
183
|
+
chars.push(this.#randomChar());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
x: slot * columnWidth + columnWidth / 2,
|
|
188
|
+
y: -(MULBERRY.next() * height),
|
|
189
|
+
speed: 1.5 + MULBERRY.next() * 3,
|
|
190
|
+
chars,
|
|
191
|
+
length,
|
|
192
|
+
life: 0
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/donuts/layer.ts
CHANGED
|
@@ -135,7 +135,9 @@ export class Donuts extends Effect<DonutsConfig> {
|
|
|
135
135
|
for (const donut of this.#donuts) {
|
|
136
136
|
const cos = Math.cos(donut.angle);
|
|
137
137
|
const sin = Math.sin(donut.angle);
|
|
138
|
-
|
|
138
|
+
|
|
139
|
+
ctx.save();
|
|
140
|
+
ctx.transform(cos, sin, -sin, cos, donut.x, donut.y);
|
|
139
141
|
|
|
140
142
|
ctx.beginPath();
|
|
141
143
|
ctx.arc(0, 0, donut.outerRadius, 0, Math.PI * 2);
|
|
@@ -144,9 +146,9 @@ export class Donuts extends Effect<DonutsConfig> {
|
|
|
144
146
|
|
|
145
147
|
ctx.fillStyle = donut.color;
|
|
146
148
|
ctx.fill();
|
|
147
|
-
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
+
ctx.restore();
|
|
151
|
+
}
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
#updateDonut(donut: Donut, dt: number): void {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Glitch } from './layer';
|
|
2
|
+
import type { GlitchConfig } from './types';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createGlitch(config?: GlitchConfig): Effect<GlitchConfig> {
|
|
6
|
+
return new Glitch(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { GlitchConfig, GlitchSlice, GlitchBlock } from './types';
|