@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,167 @@
|
|
|
1
|
+
import { Effect } from '../effect';
|
|
2
|
+
import { MULBERRY } from './consts';
|
|
3
|
+
import type { HyperSpaceStar } from './types';
|
|
4
|
+
|
|
5
|
+
export interface HyperSpaceConfig {
|
|
6
|
+
readonly count?: number;
|
|
7
|
+
readonly speed?: number;
|
|
8
|
+
readonly color?: string;
|
|
9
|
+
readonly scale?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HyperSpace extends Effect<HyperSpaceConfig> {
|
|
13
|
+
readonly #scale: number;
|
|
14
|
+
#speed: number;
|
|
15
|
+
#colorR: number;
|
|
16
|
+
#colorG: number;
|
|
17
|
+
#colorB: number;
|
|
18
|
+
#stars: HyperSpaceStar[] = [];
|
|
19
|
+
#maxCount: number;
|
|
20
|
+
#width: number = 960;
|
|
21
|
+
#height: number = 540;
|
|
22
|
+
#initialized: boolean = false;
|
|
23
|
+
|
|
24
|
+
constructor(config: HyperSpaceConfig = {}) {
|
|
25
|
+
super();
|
|
26
|
+
|
|
27
|
+
this.#scale = config.scale ?? 1;
|
|
28
|
+
this.#speed = config.speed ?? 1;
|
|
29
|
+
this.#maxCount = config.count ?? 250;
|
|
30
|
+
|
|
31
|
+
const parsed = this.#parseHex(config.color ?? '#ffffff');
|
|
32
|
+
this.#colorR = parsed[0];
|
|
33
|
+
this.#colorG = parsed[1];
|
|
34
|
+
this.#colorB = parsed[2];
|
|
35
|
+
|
|
36
|
+
if (typeof globalThis.innerWidth !== 'undefined' && globalThis.innerWidth < 991) {
|
|
37
|
+
this.#maxCount = Math.floor(this.#maxCount / 2);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
configure(config: Partial<HyperSpaceConfig>): void {
|
|
42
|
+
if (config.speed !== undefined) {
|
|
43
|
+
this.#speed = config.speed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (config.color !== undefined) {
|
|
47
|
+
const parsed = this.#parseHex(config.color);
|
|
48
|
+
this.#colorR = parsed[0];
|
|
49
|
+
this.#colorG = parsed[1];
|
|
50
|
+
this.#colorB = parsed[2];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onResize(width: number, height: number): void {
|
|
55
|
+
this.#width = width;
|
|
56
|
+
this.#height = height;
|
|
57
|
+
|
|
58
|
+
if (!this.#initialized && width > 0 && height > 0) {
|
|
59
|
+
this.#initialized = true;
|
|
60
|
+
this.#stars = [];
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < this.#maxCount; ++i) {
|
|
63
|
+
this.#stars.push(this.#createStar(true));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
tick(dt: number, width: number, height: number): void {
|
|
69
|
+
this.#width = width;
|
|
70
|
+
this.#height = height;
|
|
71
|
+
|
|
72
|
+
const maxRadius = Math.sqrt((width / 2) ** 2 + (height / 2) ** 2);
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < this.#stars.length; ++i) {
|
|
75
|
+
const star = this.#stars[i];
|
|
76
|
+
|
|
77
|
+
star.prevRadius = star.radius;
|
|
78
|
+
|
|
79
|
+
const acceleration = 1 + (star.radius / maxRadius) * 4;
|
|
80
|
+
star.radius += star.speed * this.#speed * acceleration * dt * 0.3 * this.#scale;
|
|
81
|
+
|
|
82
|
+
if (star.radius >= maxRadius + 10) {
|
|
83
|
+
this.#stars[i] = this.#createStar(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
89
|
+
const cx = width / 2;
|
|
90
|
+
const cy = height / 2;
|
|
91
|
+
const maxRadius = Math.sqrt(cx * cx + cy * cy);
|
|
92
|
+
const cr = this.#colorR;
|
|
93
|
+
const cg = this.#colorG;
|
|
94
|
+
const cb = this.#colorB;
|
|
95
|
+
|
|
96
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
97
|
+
ctx.globalAlpha = 1;
|
|
98
|
+
ctx.fillStyle = 'rgb(0, 0, 8)';
|
|
99
|
+
ctx.fillRect(0, 0, width, height);
|
|
100
|
+
|
|
101
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
102
|
+
|
|
103
|
+
for (const star of this.#stars) {
|
|
104
|
+
const normalizedRadius = star.radius / maxRadius;
|
|
105
|
+
const alpha = Math.min(1, 0.1 + normalizedRadius * 0.9) * star.brightness;
|
|
106
|
+
const lineWidth = Math.max(0.5, this.#scale * (0.3 + normalizedRadius * 1.5));
|
|
107
|
+
|
|
108
|
+
const px = cx + Math.cos(star.angle) * star.radius;
|
|
109
|
+
const py = cy + Math.sin(star.angle) * star.radius;
|
|
110
|
+
|
|
111
|
+
const trailLength = Math.max(star.radius - star.prevRadius, 1);
|
|
112
|
+
const prevRadius = Math.max(0, star.prevRadius);
|
|
113
|
+
const tx = cx + Math.cos(star.angle) * prevRadius;
|
|
114
|
+
const ty = cy + Math.sin(star.angle) * prevRadius;
|
|
115
|
+
|
|
116
|
+
const gradient = ctx.createLinearGradient(tx, ty, px, py);
|
|
117
|
+
gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0)`);
|
|
118
|
+
gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, ${alpha})`);
|
|
119
|
+
|
|
120
|
+
ctx.globalAlpha = 1;
|
|
121
|
+
ctx.beginPath();
|
|
122
|
+
ctx.moveTo(tx, ty);
|
|
123
|
+
ctx.lineTo(px, py);
|
|
124
|
+
ctx.strokeStyle = gradient;
|
|
125
|
+
ctx.lineWidth = lineWidth;
|
|
126
|
+
ctx.lineCap = 'round';
|
|
127
|
+
ctx.stroke();
|
|
128
|
+
|
|
129
|
+
if (normalizedRadius > 0.5) {
|
|
130
|
+
ctx.globalAlpha = alpha * 0.8;
|
|
131
|
+
ctx.fillStyle = `rgb(${cr}, ${cg}, ${cb})`;
|
|
132
|
+
ctx.beginPath();
|
|
133
|
+
ctx.arc(px, py, lineWidth * 0.7, 0, Math.PI * 2);
|
|
134
|
+
ctx.fill();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ctx.globalAlpha = 1;
|
|
139
|
+
ctx.resetTransform();
|
|
140
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#createStar(spread: boolean): HyperSpaceStar {
|
|
144
|
+
const maxRadius = Math.sqrt((this.#width / 2) ** 2 + (this.#height / 2) ** 2);
|
|
145
|
+
const angle = MULBERRY.next() * Math.PI * 2;
|
|
146
|
+
const radius = spread ? MULBERRY.next() * maxRadius * 0.8 : MULBERRY.next() * maxRadius * 0.05;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
angle,
|
|
150
|
+
radius,
|
|
151
|
+
prevRadius: radius,
|
|
152
|
+
speed: 0.5 + MULBERRY.next() * 1.5,
|
|
153
|
+
size: 0.5 + MULBERRY.next() * 1.5,
|
|
154
|
+
brightness: 0.5 + MULBERRY.next() * 0.5
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#parseHex(hex: string): [number, number, number] {
|
|
159
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
160
|
+
|
|
161
|
+
if (!result) {
|
|
162
|
+
return [255, 255, 255];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,31 +1,60 @@
|
|
|
1
1
|
export * from './aurora';
|
|
2
2
|
export * from './balloons';
|
|
3
|
+
export * from './black-hole';
|
|
4
|
+
export * from './boids';
|
|
3
5
|
export * from './bubbles';
|
|
6
|
+
export * from './butterflies';
|
|
4
7
|
export * from './canvas';
|
|
8
|
+
export * from './caustics';
|
|
9
|
+
export * from './clouds';
|
|
5
10
|
export * from './color';
|
|
6
11
|
export * from './confetti';
|
|
12
|
+
export * from './constellation';
|
|
13
|
+
export * from './coral-reef';
|
|
14
|
+
export * from './crystallization';
|
|
15
|
+
export * from './digital-rain';
|
|
7
16
|
export * from './donuts';
|
|
8
17
|
export * from './effect';
|
|
9
18
|
export * from './fireflies';
|
|
10
19
|
export * from './firepit';
|
|
11
20
|
export * from './fireworks';
|
|
21
|
+
export * from './glitch';
|
|
12
22
|
export * from './glitter';
|
|
23
|
+
export * from './gradient-flow';
|
|
24
|
+
export * from './hologram';
|
|
25
|
+
export * from './hyper-space';
|
|
26
|
+
export * from './interference';
|
|
27
|
+
export * from './kaleidoscope';
|
|
13
28
|
export * from './lanterns';
|
|
29
|
+
export * from './lava';
|
|
14
30
|
export * from './leaves';
|
|
15
31
|
export * from './lightning';
|
|
16
32
|
export * from './matrix';
|
|
33
|
+
export * from './murmuration';
|
|
34
|
+
export * from './nebula';
|
|
35
|
+
export * from './neon';
|
|
17
36
|
export * from './orbits';
|
|
18
37
|
export * from './particles';
|
|
19
38
|
export * from './petals';
|
|
20
39
|
export * from './plasma';
|
|
40
|
+
export * from './pollen';
|
|
41
|
+
export * from './popcorn';
|
|
42
|
+
export * from './portal';
|
|
43
|
+
export * from './pulse-grid';
|
|
21
44
|
export * from './rain';
|
|
45
|
+
export * from './roots';
|
|
22
46
|
export * from './sandstorm';
|
|
23
47
|
export * from './scene';
|
|
24
48
|
export * from './shooting-stars';
|
|
49
|
+
export * from './smoke';
|
|
25
50
|
export * from './snow';
|
|
26
51
|
export * from './sparklers';
|
|
27
52
|
export * from './stars';
|
|
28
53
|
export * from './streamers';
|
|
54
|
+
export * from './topography';
|
|
55
|
+
export * from './tornado';
|
|
29
56
|
export * from './trail';
|
|
57
|
+
export * from './voronoi';
|
|
58
|
+
export * from './volcano';
|
|
30
59
|
export * from './waves';
|
|
31
60
|
export * from './wormhole';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Interference } from './layer';
|
|
2
|
+
import type { InterferenceConfig } from './layer';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createInterference(config?: InterferenceConfig): Effect<InterferenceConfig> {
|
|
6
|
+
return new Interference(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { InterferenceConfig };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { DEFAULT_COLORS } from './consts';
|
|
4
|
+
|
|
5
|
+
export interface InterferenceConfig {
|
|
6
|
+
readonly speed?: number;
|
|
7
|
+
readonly scale?: number;
|
|
8
|
+
readonly resolution?: number;
|
|
9
|
+
readonly layers?: number;
|
|
10
|
+
readonly colors?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Interference extends Effect<InterferenceConfig> {
|
|
14
|
+
#speed: number;
|
|
15
|
+
#scale: number;
|
|
16
|
+
readonly #resolution: number;
|
|
17
|
+
readonly #layerCount: number;
|
|
18
|
+
readonly #colorRGBs: [number, number, number][];
|
|
19
|
+
#time: number = 0;
|
|
20
|
+
#offscreen: HTMLCanvasElement | null = null;
|
|
21
|
+
#offscreenCtx: CanvasRenderingContext2D | null = null;
|
|
22
|
+
#imageData: ImageData | null = null;
|
|
23
|
+
#centers: { x: number; y: number; freqX: number; freqY: number; phaseX: number; phaseY: number; speedMul: number }[];
|
|
24
|
+
|
|
25
|
+
constructor(config: InterferenceConfig = {}) {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
this.#speed = config.speed ?? 1;
|
|
29
|
+
this.#scale = config.scale ?? 1;
|
|
30
|
+
this.#resolution = config.resolution ?? 3;
|
|
31
|
+
this.#layerCount = config.layers ?? 3;
|
|
32
|
+
|
|
33
|
+
const colors = config.colors ?? DEFAULT_COLORS;
|
|
34
|
+
this.#colorRGBs = colors.map(c => hexToRGB(c));
|
|
35
|
+
|
|
36
|
+
this.#centers = [];
|
|
37
|
+
|
|
38
|
+
for (let idx = 0; idx < this.#layerCount; idx++) {
|
|
39
|
+
this.#centers.push({
|
|
40
|
+
x: 0,
|
|
41
|
+
y: 0,
|
|
42
|
+
freqX: 0.3 + idx * 0.17,
|
|
43
|
+
freqY: 0.2 + idx * 0.13,
|
|
44
|
+
phaseX: idx * Math.PI * 2 / this.#layerCount,
|
|
45
|
+
phaseY: idx * Math.PI * 1.3 / this.#layerCount,
|
|
46
|
+
speedMul: 0.7 + idx * 0.3
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
configure(config: Partial<InterferenceConfig>): void {
|
|
52
|
+
if (config.speed !== undefined) {
|
|
53
|
+
this.#speed = config.speed;
|
|
54
|
+
}
|
|
55
|
+
if (config.scale !== undefined) {
|
|
56
|
+
this.#scale = config.scale;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
tick(dt: number, width: number, height: number): void {
|
|
61
|
+
this.#time += 0.015 * dt * this.#speed;
|
|
62
|
+
|
|
63
|
+
for (let idx = 0; idx < this.#centers.length; idx++) {
|
|
64
|
+
const center = this.#centers[idx];
|
|
65
|
+
center.x = (0.5 + 0.35 * Math.sin(this.#time * center.freqX + center.phaseX)) * width;
|
|
66
|
+
center.y = (0.5 + 0.35 * Math.cos(this.#time * center.freqY + center.phaseY)) * height;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
71
|
+
const resolution = this.#resolution;
|
|
72
|
+
const offWidth = Math.ceil(width / resolution);
|
|
73
|
+
const offHeight = Math.ceil(height / resolution);
|
|
74
|
+
|
|
75
|
+
if (!this.#offscreen || this.#offscreen.width !== offWidth || this.#offscreen.height !== offHeight) {
|
|
76
|
+
this.#offscreen = document.createElement('canvas');
|
|
77
|
+
this.#offscreen.width = offWidth;
|
|
78
|
+
this.#offscreen.height = offHeight;
|
|
79
|
+
this.#offscreenCtx = this.#offscreen.getContext('2d');
|
|
80
|
+
this.#imageData = this.#offscreenCtx!.createImageData(offWidth, offHeight);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data = this.#imageData!.data;
|
|
84
|
+
const scale = this.#scale;
|
|
85
|
+
const freq = 30 * scale;
|
|
86
|
+
const centers = this.#centers;
|
|
87
|
+
const centerCount = centers.length;
|
|
88
|
+
const colorRGBs = this.#colorRGBs;
|
|
89
|
+
const colorCount = colorRGBs.length;
|
|
90
|
+
|
|
91
|
+
for (let py = 0; py < offHeight; py++) {
|
|
92
|
+
const worldY = py * resolution;
|
|
93
|
+
|
|
94
|
+
for (let px = 0; px < offWidth; px++) {
|
|
95
|
+
const worldX = px * resolution;
|
|
96
|
+
|
|
97
|
+
let totalR = 0;
|
|
98
|
+
let totalG = 0;
|
|
99
|
+
let totalB = 0;
|
|
100
|
+
|
|
101
|
+
for (let ci = 0; ci < centerCount; ci++) {
|
|
102
|
+
const center = centers[ci];
|
|
103
|
+
const dx = worldX - center.x;
|
|
104
|
+
const dy = worldY - center.y;
|
|
105
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
106
|
+
|
|
107
|
+
const wave = (Math.sin(dist / freq) + 1) * 0.5;
|
|
108
|
+
const color = colorRGBs[ci % colorCount];
|
|
109
|
+
|
|
110
|
+
totalR += color[0] * wave;
|
|
111
|
+
totalG += color[1] * wave;
|
|
112
|
+
totalB += color[2] * wave;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const maxVal = centerCount * 255;
|
|
116
|
+
const offset = (py * offWidth + px) * 4;
|
|
117
|
+
data[offset] = Math.min(255, (totalR / maxVal) * 255) | 0;
|
|
118
|
+
data[offset + 1] = Math.min(255, (totalG / maxVal) * 255) | 0;
|
|
119
|
+
data[offset + 2] = Math.min(255, (totalB / maxVal) * 255) | 0;
|
|
120
|
+
data[offset + 3] = 255;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.#offscreenCtx!.putImageData(this.#imageData!, 0, 0);
|
|
125
|
+
|
|
126
|
+
ctx.imageSmoothingEnabled = true;
|
|
127
|
+
ctx.drawImage(this.#offscreen!, 0, 0, width, height);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Kaleidoscope } from './layer';
|
|
2
|
+
import type { KaleidoscopeConfig } from './types';
|
|
3
|
+
import type { Effect } from '../effect';
|
|
4
|
+
|
|
5
|
+
export function createKaleidoscope(config?: KaleidoscopeConfig): Effect<KaleidoscopeConfig> {
|
|
6
|
+
return new Kaleidoscope(config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { KaleidoscopeConfig, KaleidoscopeShape } from './types';
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { hexToRGB } from '@basmilius/utils';
|
|
2
|
+
import { Effect } from '../effect';
|
|
3
|
+
import { DEFAULT_COLORS, MULBERRY } from './consts';
|
|
4
|
+
import type { KaleidoscopeConfig, KaleidoscopeShape } from './types';
|
|
5
|
+
|
|
6
|
+
export class Kaleidoscope extends Effect<KaleidoscopeConfig> {
|
|
7
|
+
#segments: number;
|
|
8
|
+
#speed: number;
|
|
9
|
+
#shapeCount: number;
|
|
10
|
+
#colors: string[];
|
|
11
|
+
#scale: number;
|
|
12
|
+
#shapes: KaleidoscopeShape[] = [];
|
|
13
|
+
#sourceCanvas: HTMLCanvasElement | null = null;
|
|
14
|
+
#sourceCtx: CanvasRenderingContext2D | null = null;
|
|
15
|
+
#sourceRadius: number = 0;
|
|
16
|
+
#time: number = 0;
|
|
17
|
+
#initialized: boolean = false;
|
|
18
|
+
|
|
19
|
+
constructor(config: KaleidoscopeConfig = {}) {
|
|
20
|
+
super();
|
|
21
|
+
|
|
22
|
+
this.#segments = config.segments ?? 8;
|
|
23
|
+
this.#speed = config.speed ?? 1;
|
|
24
|
+
this.#shapeCount = config.shapes ?? 15;
|
|
25
|
+
this.#colors = config.colors ?? DEFAULT_COLORS;
|
|
26
|
+
this.#scale = config.scale ?? 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
configure(config: Partial<KaleidoscopeConfig>): void {
|
|
30
|
+
if (config.speed !== undefined) {
|
|
31
|
+
this.#speed = config.speed;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onResize(width: number, height: number): void {
|
|
36
|
+
this.#sourceRadius = Math.min(width, height) / 2;
|
|
37
|
+
|
|
38
|
+
const size = Math.ceil(this.#sourceRadius * 2);
|
|
39
|
+
this.#sourceCanvas = document.createElement('canvas');
|
|
40
|
+
this.#sourceCanvas.width = size;
|
|
41
|
+
this.#sourceCanvas.height = size;
|
|
42
|
+
this.#sourceCtx = this.#sourceCanvas.getContext('2d')!;
|
|
43
|
+
|
|
44
|
+
if (!this.#initialized) {
|
|
45
|
+
this.#initialized = true;
|
|
46
|
+
this.#shapes = [];
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < this.#shapeCount; index++) {
|
|
49
|
+
this.#shapes.push(this.#createShape());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
tick(dt: number, _width: number, _height: number): void {
|
|
55
|
+
const speedMul = this.#speed * dt;
|
|
56
|
+
this.#time += 0.02 * speedMul;
|
|
57
|
+
|
|
58
|
+
const radius = this.#sourceRadius * 0.8;
|
|
59
|
+
|
|
60
|
+
for (const shape of this.#shapes) {
|
|
61
|
+
shape.x += shape.vx * speedMul;
|
|
62
|
+
shape.y += shape.vy * speedMul;
|
|
63
|
+
shape.rotation += shape.rotationSpeed * speedMul;
|
|
64
|
+
|
|
65
|
+
const dist = Math.sqrt(shape.x * shape.x + shape.y * shape.y);
|
|
66
|
+
|
|
67
|
+
if (dist + shape.size > radius) {
|
|
68
|
+
const nx = shape.x / dist;
|
|
69
|
+
const ny = shape.y / dist;
|
|
70
|
+
const dot = shape.vx * nx + shape.vy * ny;
|
|
71
|
+
shape.vx -= 2 * dot * nx;
|
|
72
|
+
shape.vy -= 2 * dot * ny;
|
|
73
|
+
|
|
74
|
+
const overlap = dist + shape.size - radius;
|
|
75
|
+
shape.x -= nx * overlap;
|
|
76
|
+
shape.y -= ny * overlap;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
82
|
+
if (!this.#sourceCanvas || !this.#sourceCtx) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sourceCtx = this.#sourceCtx;
|
|
87
|
+
const radius = this.#sourceRadius;
|
|
88
|
+
const segmentAngle = (Math.PI * 2) / this.#segments;
|
|
89
|
+
const centerX = width / 2;
|
|
90
|
+
const centerY = height / 2;
|
|
91
|
+
|
|
92
|
+
sourceCtx.clearRect(0, 0, this.#sourceCanvas.width, this.#sourceCanvas.height);
|
|
93
|
+
sourceCtx.save();
|
|
94
|
+
|
|
95
|
+
sourceCtx.beginPath();
|
|
96
|
+
sourceCtx.moveTo(radius, radius);
|
|
97
|
+
sourceCtx.arc(radius, radius, radius, -segmentAngle / 2, segmentAngle / 2);
|
|
98
|
+
sourceCtx.closePath();
|
|
99
|
+
sourceCtx.clip();
|
|
100
|
+
|
|
101
|
+
for (const shape of this.#shapes) {
|
|
102
|
+
const px = radius + shape.x * this.#scale;
|
|
103
|
+
const py = radius + shape.y * this.#scale;
|
|
104
|
+
const size = shape.size * this.#scale;
|
|
105
|
+
const [cr, cg, cb] = hexToRGB(shape.color);
|
|
106
|
+
|
|
107
|
+
sourceCtx.save();
|
|
108
|
+
sourceCtx.translate(px, py);
|
|
109
|
+
sourceCtx.rotate(shape.rotation);
|
|
110
|
+
|
|
111
|
+
if (shape.type === 0) {
|
|
112
|
+
const gradient = sourceCtx.createRadialGradient(0, 0, 0, 0, 0, size);
|
|
113
|
+
gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0.9)`);
|
|
114
|
+
gradient.addColorStop(0.5, `rgba(${cr}, ${cg}, ${cb}, 0.5)`);
|
|
115
|
+
gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
|
|
116
|
+
sourceCtx.fillStyle = gradient;
|
|
117
|
+
sourceCtx.beginPath();
|
|
118
|
+
sourceCtx.arc(0, 0, size, 0, Math.PI * 2);
|
|
119
|
+
sourceCtx.fill();
|
|
120
|
+
} else if (shape.type === 1) {
|
|
121
|
+
sourceCtx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, 0.7)`;
|
|
122
|
+
sourceCtx.beginPath();
|
|
123
|
+
for (let point = 0; point < 6; point++) {
|
|
124
|
+
const angle = (point / 6) * Math.PI * 2;
|
|
125
|
+
const hx = Math.cos(angle) * size;
|
|
126
|
+
const hy = Math.sin(angle) * size;
|
|
127
|
+
|
|
128
|
+
if (point === 0) {
|
|
129
|
+
sourceCtx.moveTo(hx, hy);
|
|
130
|
+
} else {
|
|
131
|
+
sourceCtx.lineTo(hx, hy);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
sourceCtx.closePath();
|
|
135
|
+
sourceCtx.fill();
|
|
136
|
+
|
|
137
|
+
sourceCtx.strokeStyle = `rgba(${Math.min(255, cr + 60)}, ${Math.min(255, cg + 60)}, ${Math.min(255, cb + 60)}, 0.5)`;
|
|
138
|
+
sourceCtx.lineWidth = 1.5;
|
|
139
|
+
sourceCtx.stroke();
|
|
140
|
+
} else if (shape.type === 2) {
|
|
141
|
+
const petalCount = 5;
|
|
142
|
+
|
|
143
|
+
sourceCtx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, 0.6)`;
|
|
144
|
+
|
|
145
|
+
for (let petal = 0; petal < petalCount; petal++) {
|
|
146
|
+
const angle = (petal / petalCount) * Math.PI * 2;
|
|
147
|
+
sourceCtx.save();
|
|
148
|
+
sourceCtx.rotate(angle);
|
|
149
|
+
sourceCtx.beginPath();
|
|
150
|
+
sourceCtx.ellipse(0, -size * 0.5, size * 0.3, size * 0.6, 0, 0, Math.PI * 2);
|
|
151
|
+
sourceCtx.fill();
|
|
152
|
+
sourceCtx.restore();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const coreGradient = sourceCtx.createRadialGradient(0, 0, 0, 0, 0, size * 0.25);
|
|
156
|
+
coreGradient.addColorStop(0, `rgba(255, 255, 255, 0.8)`);
|
|
157
|
+
coreGradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0.3)`);
|
|
158
|
+
sourceCtx.fillStyle = coreGradient;
|
|
159
|
+
sourceCtx.beginPath();
|
|
160
|
+
sourceCtx.arc(0, 0, size * 0.25, 0, Math.PI * 2);
|
|
161
|
+
sourceCtx.fill();
|
|
162
|
+
} else {
|
|
163
|
+
const gradient = sourceCtx.createRadialGradient(0, 0, size * 0.3, 0, 0, size);
|
|
164
|
+
gradient.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, 0)`);
|
|
165
|
+
gradient.addColorStop(0.4, `rgba(${cr}, ${cg}, ${cb}, 0.6)`);
|
|
166
|
+
gradient.addColorStop(0.6, `rgba(${cr}, ${cg}, ${cb}, 0.6)`);
|
|
167
|
+
gradient.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
|
|
168
|
+
sourceCtx.fillStyle = gradient;
|
|
169
|
+
sourceCtx.beginPath();
|
|
170
|
+
sourceCtx.arc(0, 0, size, 0, Math.PI * 2);
|
|
171
|
+
sourceCtx.fill();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
sourceCtx.restore();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
sourceCtx.restore();
|
|
178
|
+
|
|
179
|
+
for (let index = 0; index < this.#segments; index++) {
|
|
180
|
+
const angle = segmentAngle * index;
|
|
181
|
+
const mirrored = index % 2 === 1;
|
|
182
|
+
|
|
183
|
+
ctx.save();
|
|
184
|
+
ctx.translate(centerX, centerY);
|
|
185
|
+
ctx.rotate(angle);
|
|
186
|
+
|
|
187
|
+
if (mirrored) {
|
|
188
|
+
ctx.scale(1, -1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
ctx.drawImage(this.#sourceCanvas, -radius, -radius);
|
|
192
|
+
ctx.restore();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#createShape(): KaleidoscopeShape {
|
|
197
|
+
const maxDist = this.#sourceRadius * 0.6;
|
|
198
|
+
const angle = MULBERRY.next() * Math.PI * 2;
|
|
199
|
+
const dist = MULBERRY.next() * maxDist;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
x: Math.cos(angle) * dist,
|
|
203
|
+
y: Math.sin(angle) * dist,
|
|
204
|
+
vx: (MULBERRY.next() - 0.5) * 1.2,
|
|
205
|
+
vy: (MULBERRY.next() - 0.5) * 1.2,
|
|
206
|
+
size: 8 + MULBERRY.next() * 25,
|
|
207
|
+
color: this.#colors[Math.floor(MULBERRY.next() * this.#colors.length)],
|
|
208
|
+
type: Math.floor(MULBERRY.next() * 4),
|
|
209
|
+
rotation: MULBERRY.next() * Math.PI * 2,
|
|
210
|
+
rotationSpeed: (MULBERRY.next() - 0.5) * 0.06
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface KaleidoscopeConfig {
|
|
2
|
+
readonly segments?: number;
|
|
3
|
+
readonly speed?: number;
|
|
4
|
+
readonly shapes?: number;
|
|
5
|
+
readonly colors?: string[];
|
|
6
|
+
readonly scale?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type KaleidoscopeShape = {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
vx: number;
|
|
13
|
+
vy: number;
|
|
14
|
+
size: number;
|
|
15
|
+
color: string;
|
|
16
|
+
type: number;
|
|
17
|
+
rotation: number;
|
|
18
|
+
rotationSpeed: number;
|
|
19
|
+
};
|
package/src/lanterns/layer.ts
CHANGED
|
@@ -88,7 +88,8 @@ export class Lanterns extends Effect<LanternsConfig> {
|
|
|
88
88
|
ctx.arc(px, py, glowRadius, 0, Math.PI * 2);
|
|
89
89
|
ctx.fill();
|
|
90
90
|
|
|
91
|
-
ctx.
|
|
91
|
+
ctx.save();
|
|
92
|
+
ctx.transform(1, 0, 0, 1, px, py);
|
|
92
93
|
|
|
93
94
|
const bodyW = size * 0.8;
|
|
94
95
|
const bodyH = size;
|
|
@@ -152,7 +153,7 @@ export class Lanterns extends Effect<LanternsConfig> {
|
|
|
152
153
|
ctx.lineWidth = size * 0.04;
|
|
153
154
|
ctx.stroke();
|
|
154
155
|
|
|
155
|
-
ctx.
|
|
156
|
+
ctx.restore();
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
|