@5even7/dlc-ui 0.2.11 → 0.2.13
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/CHANGELOG.md +48 -42
- package/LICENSE +21 -21
- package/README.md +258 -255
- package/dist/capsule.cjs +307 -71
- package/dist/capsule.mjs +828 -604
- package/dist/color.cjs +338 -82
- package/dist/color.mjs +879 -631
- package/dist/index.cjs +997 -253
- package/dist/index.mjs +2704 -1976
- package/dist/index.umd.js +997 -253
- package/dist/index.umd.min.js +1 -1
- package/dist/progress.cjs +632 -172
- package/dist/progress.mjs +1827 -1379
- package/dist/vue2.cjs +1141 -266
- package/dist/vue2.mjs +2893 -2056
- package/dist/vue3.cjs +1141 -266
- package/dist/vue3.mjs +2893 -2056
- package/package.json +118 -118
- package/styles/base.css +32 -32
- package/styles/color.css +18 -18
- package/styles/progress.css +53 -50
- package/types/capsule.d.ts +15 -13
- package/types/color.d.ts +15 -13
- package/types/index.d.ts +204 -116
- package/types/progress.d.ts +16 -14
- package/types/vue3.d.ts +38 -22
package/dist/capsule.mjs
CHANGED
|
@@ -1,117 +1,123 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny event emitter. Accepts any event name so the API stays open for
|
|
3
|
-
* future events (hover, click, custom) without breaking changes.
|
|
4
|
-
*/
|
|
5
|
-
function createEmitter() {
|
|
6
|
-
// Plain object storage (no Map/Set) so the legacy build has no API
|
|
7
|
-
// dependencies beyond what IE11 provides.
|
|
8
|
-
const listeners = Object.create(null);
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
on(event, fn) {
|
|
12
|
-
if (!listeners[event]) listeners[event] = [];
|
|
13
|
-
listeners[event].push(fn);
|
|
14
|
-
return () => {
|
|
15
|
-
const current = listeners[event];
|
|
16
|
-
if (current) {
|
|
17
|
-
const index = current.indexOf(fn);
|
|
18
|
-
if (index !== -1) current.splice(index, 1);
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
off(event, fn) {
|
|
23
|
-
const current = listeners[event];
|
|
24
|
-
if (!current) return false;
|
|
25
|
-
const index = current.indexOf(fn);
|
|
26
|
-
if (index === -1) return false;
|
|
27
|
-
current.splice(index, 1);
|
|
28
|
-
return true;
|
|
29
|
-
},
|
|
30
|
-
emit(event, ...args) {
|
|
31
|
-
const current = listeners[event];
|
|
32
|
-
if (!current) return;
|
|
33
|
-
for (const fn of current.slice()) fn(...args);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Tiny event emitter. Accepts any event name so the API stays open for
|
|
3
|
+
* future events (hover, click, custom) without breaking changes.
|
|
4
|
+
*/
|
|
5
|
+
function createEmitter() {
|
|
6
|
+
// Plain object storage (no Map/Set) so the legacy build has no API
|
|
7
|
+
// dependencies beyond what IE11 provides.
|
|
8
|
+
const listeners = Object.create(null);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
on(event, fn) {
|
|
12
|
+
if (!listeners[event]) listeners[event] = [];
|
|
13
|
+
listeners[event].push(fn);
|
|
14
|
+
return () => {
|
|
15
|
+
const current = listeners[event];
|
|
16
|
+
if (current) {
|
|
17
|
+
const index = current.indexOf(fn);
|
|
18
|
+
if (index !== -1) current.splice(index, 1);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
off(event, fn) {
|
|
23
|
+
const current = listeners[event];
|
|
24
|
+
if (!current) return false;
|
|
25
|
+
const index = current.indexOf(fn);
|
|
26
|
+
if (index === -1) return false;
|
|
27
|
+
current.splice(index, 1);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
emit(event, ...args) {
|
|
31
|
+
const current = listeners[event];
|
|
32
|
+
if (!current) return;
|
|
33
|
+
for (const fn of current.slice()) fn(...args);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const UNIT_PATTERN = /^[\d.]+(?:px|%|vw|vh|vmin|vmax|em|rem)$/;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a size option to a CSS length string.
|
|
42
|
+
* Accepts numbers (treated as px) or strings with px/%, vw/vh/vmin/vmax, em/rem.
|
|
43
|
+
*/
|
|
44
|
+
function parseSize(value) {
|
|
45
|
+
if (typeof value === 'number') {
|
|
46
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`Invalid size: ${value}`);
|
|
47
|
+
return `${value}px`;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== 'string') throw new Error(`Invalid size: ${String(value)}`);
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
if (!trimmed) throw new Error('Invalid size: empty string');
|
|
52
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed)) return `${trimmed}px`;
|
|
53
|
+
if (!UNIT_PATTERN.test(trimmed)) {
|
|
54
|
+
throw new Error(`Invalid size or unsupported unit: "${value}" (use px, %, vw, vh, vmin, vmax, em, rem)`);
|
|
55
|
+
}
|
|
56
|
+
return trimmed;
|
|
36
57
|
}
|
|
37
58
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (typeof value !== 'string') throw new Error(`Invalid size: ${String(value)}`);
|
|
50
|
-
const trimmed = value.trim();
|
|
51
|
-
if (!trimmed) throw new Error('Invalid size: empty string');
|
|
52
|
-
if (/^\d+(?:\.\d+)?$/.test(trimmed)) return `${trimmed}px`;
|
|
53
|
-
if (!UNIT_PATTERN.test(trimmed)) {
|
|
54
|
-
throw new Error(`Invalid size or unsupported unit: "${value}" (use px, %, vw, vh, vmin, vmax, em, rem)`);
|
|
55
|
-
}
|
|
56
|
-
return trimmed;
|
|
59
|
+
const QUALITY_TIERS = {
|
|
60
|
+
low: { dpr: 1 },
|
|
61
|
+
medium: { dpr: 1.5 },
|
|
62
|
+
high: { dpr: 2 }
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function autoDprCap() {
|
|
66
|
+
const nav = typeof navigator !== 'undefined' ? navigator : null;
|
|
67
|
+
const memory = nav && typeof nav.deviceMemory === 'number' ? nav.deviceMemory : 8;
|
|
68
|
+
const cores = nav && typeof nav.hardwareConcurrency === 'number' ? nav.hardwareConcurrency : 8;
|
|
69
|
+
return memory <= 4 || cores <= 4 ? 1.5 : 2;
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
function autoDprCap() {
|
|
66
|
-
const nav = typeof navigator !== 'undefined' ? navigator : null;
|
|
67
|
-
const memory = nav && typeof nav.deviceMemory === 'number' ? nav.deviceMemory : 8;
|
|
68
|
-
const cores = nav && typeof nav.hardwareConcurrency === 'number' ? nav.hardwareConcurrency : 8;
|
|
69
|
-
return memory <= 4 || cores <= 4 ? 1.5 : 2;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function dprCapFor(quality) {
|
|
73
|
-
if (quality === 'auto') return autoDprCap();
|
|
74
|
-
const tier = QUALITY_TIERS[quality] || QUALITY_TIERS.medium;
|
|
75
|
-
return tier.dpr;
|
|
72
|
+
function dprCapFor(quality) {
|
|
73
|
+
if (quality === 'auto') return autoDprCap();
|
|
74
|
+
const tier = QUALITY_TIERS[quality] || QUALITY_TIERS.medium;
|
|
75
|
+
return tier.dpr;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
*/
|
|
83
|
-
function normalizeOptions(defaults, preset, user) {
|
|
84
|
-
// undefined means "not provided" (e.g. Vue $props with unset props):
|
|
85
|
-
// drop those keys before merging so defaults/preset values survive.
|
|
86
|
-
const omitUndefined = (source) => {
|
|
87
|
-
const result = {};
|
|
88
|
-
for (const key of Object.keys(source)) {
|
|
89
|
-
if (source[key] !== undefined) result[key] = source[key];
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
};
|
|
93
|
-
const presetOptions = omitUndefined(preset && typeof preset === 'object' ? preset : {});
|
|
94
|
-
const userOptions = omitUndefined(user && typeof user === 'object' ? user : {});
|
|
95
|
-
return { ...defaults, ...presetOptions, ...userOptions };
|
|
78
|
+
function effectiveDprCap(quality, renderScale = 1) {
|
|
79
|
+
const scale = Number(renderScale);
|
|
80
|
+
const normalizedScale = Number.isFinite(scale) ? Math.min(1, Math.max(0.25, scale)) : 1;
|
|
81
|
+
return Math.max(0.5, dprCapFor(quality) * normalizedScale);
|
|
96
82
|
}
|
|
97
83
|
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Merge defaults < preset < user. Unknown user keys are preserved so the
|
|
86
|
+
* component API can grow (new props, cssVars, callbacks) without a breaking
|
|
87
|
+
* change.
|
|
88
|
+
*/
|
|
89
|
+
function normalizeOptions(defaults, preset, user) {
|
|
90
|
+
// undefined means "not provided" (e.g. Vue $props with unset props):
|
|
91
|
+
// drop those keys before merging so defaults/preset values survive.
|
|
92
|
+
const omitUndefined = (source) => {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const key of Object.keys(source)) {
|
|
95
|
+
if (source[key] !== undefined) result[key] = source[key];
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
};
|
|
99
|
+
const presetOptions = omitUndefined(preset && typeof preset === 'object' ? preset : {});
|
|
100
|
+
const userOptions = omitUndefined(user && typeof user === 'object' ? user : {});
|
|
101
|
+
return { ...defaults, ...presetOptions, ...userOptions };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 公共色板:NC-01~NC-06 的四色组(底色 / 主色 / 辅色 / 高光色)。
|
|
106
|
+
* Capsule 预置与 dlc-color 等组件共用,新增色板只改这里。
|
|
107
|
+
*/
|
|
108
|
+
const PALETTES = {
|
|
109
|
+
original: ['#FFF3EA', '#F5B27A', '#F67BC6', '#A978E8'],
|
|
110
|
+
ocean: ['#EAF6FF', '#8FD0FF', '#3B87F6', '#6B58E9'],
|
|
111
|
+
klein: ['#EDF2FF', '#2F58D5', '#1B2040', '#E07A43'],
|
|
112
|
+
ultraviolet: ['#F2EEFF', '#B99AF1', '#8F74DB', '#D7D85C'],
|
|
113
|
+
chrome: ['#F5F6F8', '#B9C0CC', '#7F8793', '#4A4F59'],
|
|
114
|
+
plus: ['#FFF0E6', '#F6C26B', '#F98A64', '#E86D74']
|
|
109
115
|
};
|
|
110
116
|
|
|
111
|
-
/**
|
|
112
|
-
* Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
|
|
113
|
-
* 颜色引用公共色板,seed/speed 决定形态与流速。
|
|
114
|
-
*/
|
|
117
|
+
/**
|
|
118
|
+
* Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
|
|
119
|
+
* 颜色引用公共色板,seed/speed 决定形态与流速。
|
|
120
|
+
*/
|
|
115
121
|
const CAPSULE_PRESETS = [
|
|
116
122
|
{ id: 'original', code: 'NC-01', name: '初光', group: 'warm', seed: 1.7, speed: 0.5, colors: [...PALETTES.original] },
|
|
117
123
|
{ id: 'ocean', code: 'NC-02', name: '沧溟', group: 'cold', seed: 8.2, speed: 0.48, colors: [...PALETTES.ocean] },
|
|
@@ -147,6 +153,11 @@ const DEFAULTS = {
|
|
|
147
153
|
height: 160,
|
|
148
154
|
quality: 'auto',
|
|
149
155
|
renderer: 'auto',
|
|
156
|
+
renderScale: 1,
|
|
157
|
+
powerPreference: 'high-performance',
|
|
158
|
+
fps: 60,
|
|
159
|
+
paused: false,
|
|
160
|
+
static: false,
|
|
150
161
|
respectReducedMotion: true,
|
|
151
162
|
mouseColor: true,
|
|
152
163
|
textRatio: 39
|
|
@@ -158,7 +169,7 @@ const DEFAULTS = {
|
|
|
158
169
|
* Templates use {brand} {code} {name} placeholders.
|
|
159
170
|
*/
|
|
160
171
|
const COPY = {
|
|
161
|
-
capsuleAria: '打开 {name} 沉浸预览'
|
|
172
|
+
capsuleAria: '打开 {name} 沉浸预览'
|
|
162
173
|
};
|
|
163
174
|
|
|
164
175
|
/**
|
|
@@ -377,231 +388,254 @@ function hexToRgb01(color) {
|
|
|
377
388
|
];
|
|
378
389
|
}
|
|
379
390
|
|
|
380
|
-
const VERTEX_SHADER = `#version 300 es
|
|
381
|
-
in vec2 a_position;
|
|
382
|
-
out vec2 v_uv;
|
|
383
|
-
void main() {
|
|
384
|
-
v_uv = a_position * 0.5 + 0.5;
|
|
385
|
-
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
386
|
-
}`;
|
|
387
|
-
|
|
388
|
-
const FRAGMENT_SHADER = `#version 300 es
|
|
389
|
-
precision highp float;
|
|
390
|
-
|
|
391
|
-
in vec2 v_uv;
|
|
392
|
-
out vec4 outColor;
|
|
393
|
-
|
|
394
|
-
uniform vec2 u_resolution;
|
|
395
|
-
uniform float u_time;
|
|
396
|
-
uniform float u_seed;
|
|
397
|
-
uniform float u_motion;
|
|
398
|
-
uniform vec2 u_pointer;
|
|
399
|
-
uniform vec3 u_colorA;
|
|
400
|
-
uniform vec3 u_colorB;
|
|
401
|
-
uniform vec3 u_colorC;
|
|
402
|
-
uniform vec3 u_colorD;
|
|
403
|
-
|
|
404
|
-
float hash21(vec2 p) {
|
|
405
|
-
p = fract(p * vec2(123.34, 456.21));
|
|
406
|
-
p += dot(p, p + 45.32 + u_seed);
|
|
407
|
-
return fract(p.x * p.y);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
float noise(vec2 p) {
|
|
411
|
-
vec2 i = floor(p);
|
|
412
|
-
vec2 f = fract(p);
|
|
413
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
414
|
-
float a = hash21(i);
|
|
415
|
-
float b = hash21(i + vec2(1.0, 0.0));
|
|
416
|
-
float c = hash21(i + vec2(0.0, 1.0));
|
|
417
|
-
float d = hash21(i + vec2(1.0, 1.0));
|
|
418
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
float fbm(vec2 p) {
|
|
422
|
-
float value = 0.0;
|
|
423
|
-
float amplitude = 0.52;
|
|
424
|
-
mat2 rotation = mat2(0.80, 0.60, -0.60, 0.80);
|
|
425
|
-
for (int i = 0; i < 6; i++) {
|
|
426
|
-
value += amplitude * noise(p);
|
|
427
|
-
p = rotation * p * 2.03 + 17.7;
|
|
428
|
-
amplitude *= 0.5;
|
|
429
|
-
}
|
|
430
|
-
return value;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
float gaussian(float value, float center, float width) {
|
|
434
|
-
return exp(-pow(value - center, 2.0) / max(width, 0.0001));
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
vec3 palette(float t) {
|
|
438
|
-
t = clamp(t, 0.0, 1.0);
|
|
439
|
-
vec3 shadow = mix(u_colorA, u_colorB, smoothstep(0.06, 0.62, t));
|
|
440
|
-
vec3 body = mix(u_colorB, u_colorC, smoothstep(0.30, 0.82, t));
|
|
441
|
-
vec3 highlight = mix(u_colorC, u_colorD, smoothstep(0.74, 1.0, t));
|
|
442
|
-
vec3 restrained = mix(shadow, body, smoothstep(0.26, 0.72, t));
|
|
443
|
-
return mix(restrained, highlight, smoothstep(0.78, 0.97, t));
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
vec3 renderNebula(vec2 uv, vec2 p, vec2 pointer, float distanceToPointer, float t) {
|
|
447
|
-
vec2 delta = p - pointer;
|
|
448
|
-
float influence = exp(-distanceToPointer * 4.6) * u_motion;
|
|
449
|
-
float angle = influence * 1.7;
|
|
450
|
-
mat2 swirl = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
|
|
451
|
-
p = pointer + swirl * delta;
|
|
452
|
-
p += normalize(delta + 0.0001) * influence * 0.08;
|
|
453
|
-
|
|
454
|
-
vec2 drift = vec2(t * 0.22, -t * 0.13);
|
|
455
|
-
vec2 q = vec2(
|
|
456
|
-
fbm(p * 1.35 + drift + u_seed),
|
|
457
|
-
fbm(p * 1.35 + vec2(5.2, 1.3) - drift * 0.85)
|
|
458
|
-
);
|
|
459
|
-
vec2 r = vec2(
|
|
460
|
-
fbm(p * 2.0 + 3.6 * q + vec2(1.7, 9.2) + t * 0.10),
|
|
461
|
-
fbm(p * 2.0 + 3.0 * q + vec2(8.3, 2.8) - t * 0.085)
|
|
462
|
-
);
|
|
463
|
-
|
|
464
|
-
float cloud = fbm(p * 1.7 + 4.2 * r);
|
|
465
|
-
float veins = fbm(p * 4.0 - 2.0 * q + t * 0.065);
|
|
466
|
-
float nebula = smoothstep(0.18, 0.91, cloud * 0.9 + veins * 0.22);
|
|
467
|
-
|
|
468
|
-
vec3 color = palette(nebula);
|
|
469
|
-
color += u_colorD * pow(max(cloud - 0.63, 0.0), 2.0) * 1.05;
|
|
470
|
-
color *= 0.78 + 0.34 * smoothstep(0.15, 0.9, veins);
|
|
471
|
-
|
|
472
|
-
vec2 starGrid = floor((uv + vec2(u_seed * 0.013, 0.0)) * vec2(132.0, 58.0));
|
|
473
|
-
vec2 starCell = fract(uv * vec2(132.0, 58.0)) - 0.5;
|
|
474
|
-
float starRandom = hash21(starGrid);
|
|
475
|
-
float starShape = smoothstep(0.075, 0.0, length(starCell));
|
|
476
|
-
float starMask = step(0.989, starRandom) * starShape;
|
|
477
|
-
float twinkle = 0.35 + 0.65 * sin(t * (1.0 + starRandom * 2.4) + starRandom * 40.0) * 0.5 + 0.5;
|
|
478
|
-
color += starMask * twinkle * mix(u_colorC, u_colorD, starRandom) * 1.05;
|
|
479
|
-
|
|
480
|
-
float pointerGlow = exp(-distanceToPointer * 7.0) * u_motion;
|
|
481
|
-
color += u_colorD * pointerGlow * 0.28;
|
|
482
|
-
return color;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
void main() {
|
|
486
|
-
vec2 uv = v_uv;
|
|
487
|
-
vec2 p = uv - 0.5;
|
|
488
|
-
p.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
489
|
-
|
|
490
|
-
vec2 pointer = u_pointer - 0.5;
|
|
491
|
-
pointer.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
492
|
-
float distanceToPointer = length(p - pointer);
|
|
493
|
-
|
|
494
|
-
vec3 color = renderNebula(uv, p, pointer, distanceToPointer, u_time);
|
|
495
|
-
float vignette = smoothstep(0.94, 0.18, length((uv - 0.5) * vec2(1.0, 1.35)));
|
|
496
|
-
color *= 0.70 + vignette * 0.42;
|
|
497
|
-
color = pow(max(color, vec3(0.0)), vec3(0.88));
|
|
498
|
-
|
|
499
|
-
outColor = vec4(color, 1.0);
|
|
500
|
-
}`;
|
|
501
|
-
|
|
502
|
-
function compileShader(gl, type, source) {
|
|
503
|
-
const shader = gl.createShader(type);
|
|
504
|
-
gl.shaderSource(shader, source);
|
|
505
|
-
gl.compileShader(shader);
|
|
506
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
507
|
-
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
508
|
-
gl.deleteShader(shader);
|
|
509
|
-
throw new Error(message);
|
|
510
|
-
}
|
|
511
|
-
return shader;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function createProgram(gl) {
|
|
515
|
-
const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
516
|
-
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
517
|
-
const program = gl.createProgram();
|
|
518
|
-
gl.attachShader(program, vertex);
|
|
519
|
-
gl.attachShader(program, fragment);
|
|
520
|
-
gl.linkProgram(program);
|
|
521
|
-
gl.deleteShader(vertex);
|
|
522
|
-
gl.deleteShader(fragment);
|
|
523
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
524
|
-
const message = gl.getProgramInfoLog(program) || 'Unknown program link error';
|
|
525
|
-
gl.deleteProgram(program);
|
|
526
|
-
throw new Error(message);
|
|
527
|
-
}
|
|
528
|
-
return program;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
class CosmicRenderer {
|
|
532
|
-
constructor(canvas, preset, options = {}) {
|
|
533
|
-
this.canvas = canvas;
|
|
534
|
-
this.preset = { ...preset };
|
|
535
|
-
this.options = { dprCap: 1.75, mouseColor: true, ...options };
|
|
536
|
-
this.gl = canvas.getContext('webgl2', {
|
|
537
|
-
alpha: false,
|
|
538
|
-
antialias: false,
|
|
539
|
-
depth: false,
|
|
540
|
-
powerPreference: 'high-performance',
|
|
541
|
-
preserveDrawingBuffer: false
|
|
542
|
-
});
|
|
543
|
-
if (!this.gl) throw new Error('WebGL2 is not available');
|
|
544
|
-
|
|
545
|
-
this.program = createProgram(this.gl);
|
|
546
|
-
this.locations = this.#getLocations();
|
|
547
|
-
this.pointer = [0.72, 0.45];
|
|
548
|
-
this.pointerTarget = [...this.pointer];
|
|
549
|
-
this.motion = 0;
|
|
550
|
-
this.motionTarget = 0;
|
|
551
|
-
this.timeOffset = preset.seed * 0.73;
|
|
552
|
-
this.
|
|
553
|
-
this.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.#
|
|
557
|
-
this
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
this.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
this.
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
this.
|
|
597
|
-
this.eventTarget.addEventListener('
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
391
|
+
const VERTEX_SHADER = `#version 300 es
|
|
392
|
+
in vec2 a_position;
|
|
393
|
+
out vec2 v_uv;
|
|
394
|
+
void main() {
|
|
395
|
+
v_uv = a_position * 0.5 + 0.5;
|
|
396
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
397
|
+
}`;
|
|
398
|
+
|
|
399
|
+
const FRAGMENT_SHADER = `#version 300 es
|
|
400
|
+
precision highp float;
|
|
401
|
+
|
|
402
|
+
in vec2 v_uv;
|
|
403
|
+
out vec4 outColor;
|
|
404
|
+
|
|
405
|
+
uniform vec2 u_resolution;
|
|
406
|
+
uniform float u_time;
|
|
407
|
+
uniform float u_seed;
|
|
408
|
+
uniform float u_motion;
|
|
409
|
+
uniform vec2 u_pointer;
|
|
410
|
+
uniform vec3 u_colorA;
|
|
411
|
+
uniform vec3 u_colorB;
|
|
412
|
+
uniform vec3 u_colorC;
|
|
413
|
+
uniform vec3 u_colorD;
|
|
414
|
+
|
|
415
|
+
float hash21(vec2 p) {
|
|
416
|
+
p = fract(p * vec2(123.34, 456.21));
|
|
417
|
+
p += dot(p, p + 45.32 + u_seed);
|
|
418
|
+
return fract(p.x * p.y);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
float noise(vec2 p) {
|
|
422
|
+
vec2 i = floor(p);
|
|
423
|
+
vec2 f = fract(p);
|
|
424
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
425
|
+
float a = hash21(i);
|
|
426
|
+
float b = hash21(i + vec2(1.0, 0.0));
|
|
427
|
+
float c = hash21(i + vec2(0.0, 1.0));
|
|
428
|
+
float d = hash21(i + vec2(1.0, 1.0));
|
|
429
|
+
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
float fbm(vec2 p) {
|
|
433
|
+
float value = 0.0;
|
|
434
|
+
float amplitude = 0.52;
|
|
435
|
+
mat2 rotation = mat2(0.80, 0.60, -0.60, 0.80);
|
|
436
|
+
for (int i = 0; i < 6; i++) {
|
|
437
|
+
value += amplitude * noise(p);
|
|
438
|
+
p = rotation * p * 2.03 + 17.7;
|
|
439
|
+
amplitude *= 0.5;
|
|
440
|
+
}
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
float gaussian(float value, float center, float width) {
|
|
445
|
+
return exp(-pow(value - center, 2.0) / max(width, 0.0001));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
vec3 palette(float t) {
|
|
449
|
+
t = clamp(t, 0.0, 1.0);
|
|
450
|
+
vec3 shadow = mix(u_colorA, u_colorB, smoothstep(0.06, 0.62, t));
|
|
451
|
+
vec3 body = mix(u_colorB, u_colorC, smoothstep(0.30, 0.82, t));
|
|
452
|
+
vec3 highlight = mix(u_colorC, u_colorD, smoothstep(0.74, 1.0, t));
|
|
453
|
+
vec3 restrained = mix(shadow, body, smoothstep(0.26, 0.72, t));
|
|
454
|
+
return mix(restrained, highlight, smoothstep(0.78, 0.97, t));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
vec3 renderNebula(vec2 uv, vec2 p, vec2 pointer, float distanceToPointer, float t) {
|
|
458
|
+
vec2 delta = p - pointer;
|
|
459
|
+
float influence = exp(-distanceToPointer * 4.6) * u_motion;
|
|
460
|
+
float angle = influence * 1.7;
|
|
461
|
+
mat2 swirl = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
|
|
462
|
+
p = pointer + swirl * delta;
|
|
463
|
+
p += normalize(delta + 0.0001) * influence * 0.08;
|
|
464
|
+
|
|
465
|
+
vec2 drift = vec2(t * 0.22, -t * 0.13);
|
|
466
|
+
vec2 q = vec2(
|
|
467
|
+
fbm(p * 1.35 + drift + u_seed),
|
|
468
|
+
fbm(p * 1.35 + vec2(5.2, 1.3) - drift * 0.85)
|
|
469
|
+
);
|
|
470
|
+
vec2 r = vec2(
|
|
471
|
+
fbm(p * 2.0 + 3.6 * q + vec2(1.7, 9.2) + t * 0.10),
|
|
472
|
+
fbm(p * 2.0 + 3.0 * q + vec2(8.3, 2.8) - t * 0.085)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
float cloud = fbm(p * 1.7 + 4.2 * r);
|
|
476
|
+
float veins = fbm(p * 4.0 - 2.0 * q + t * 0.065);
|
|
477
|
+
float nebula = smoothstep(0.18, 0.91, cloud * 0.9 + veins * 0.22);
|
|
478
|
+
|
|
479
|
+
vec3 color = palette(nebula);
|
|
480
|
+
color += u_colorD * pow(max(cloud - 0.63, 0.0), 2.0) * 1.05;
|
|
481
|
+
color *= 0.78 + 0.34 * smoothstep(0.15, 0.9, veins);
|
|
482
|
+
|
|
483
|
+
vec2 starGrid = floor((uv + vec2(u_seed * 0.013, 0.0)) * vec2(132.0, 58.0));
|
|
484
|
+
vec2 starCell = fract(uv * vec2(132.0, 58.0)) - 0.5;
|
|
485
|
+
float starRandom = hash21(starGrid);
|
|
486
|
+
float starShape = smoothstep(0.075, 0.0, length(starCell));
|
|
487
|
+
float starMask = step(0.989, starRandom) * starShape;
|
|
488
|
+
float twinkle = 0.35 + 0.65 * sin(t * (1.0 + starRandom * 2.4) + starRandom * 40.0) * 0.5 + 0.5;
|
|
489
|
+
color += starMask * twinkle * mix(u_colorC, u_colorD, starRandom) * 1.05;
|
|
490
|
+
|
|
491
|
+
float pointerGlow = exp(-distanceToPointer * 7.0) * u_motion;
|
|
492
|
+
color += u_colorD * pointerGlow * 0.28;
|
|
493
|
+
return color;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
void main() {
|
|
497
|
+
vec2 uv = v_uv;
|
|
498
|
+
vec2 p = uv - 0.5;
|
|
499
|
+
p.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
500
|
+
|
|
501
|
+
vec2 pointer = u_pointer - 0.5;
|
|
502
|
+
pointer.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
503
|
+
float distanceToPointer = length(p - pointer);
|
|
504
|
+
|
|
505
|
+
vec3 color = renderNebula(uv, p, pointer, distanceToPointer, u_time);
|
|
506
|
+
float vignette = smoothstep(0.94, 0.18, length((uv - 0.5) * vec2(1.0, 1.35)));
|
|
507
|
+
color *= 0.70 + vignette * 0.42;
|
|
508
|
+
color = pow(max(color, vec3(0.0)), vec3(0.88));
|
|
509
|
+
|
|
510
|
+
outColor = vec4(color, 1.0);
|
|
511
|
+
}`;
|
|
512
|
+
|
|
513
|
+
function compileShader(gl, type, source) {
|
|
514
|
+
const shader = gl.createShader(type);
|
|
515
|
+
gl.shaderSource(shader, source);
|
|
516
|
+
gl.compileShader(shader);
|
|
517
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
518
|
+
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
519
|
+
gl.deleteShader(shader);
|
|
520
|
+
throw new Error(message);
|
|
521
|
+
}
|
|
522
|
+
return shader;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function createProgram(gl) {
|
|
526
|
+
const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
527
|
+
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
528
|
+
const program = gl.createProgram();
|
|
529
|
+
gl.attachShader(program, vertex);
|
|
530
|
+
gl.attachShader(program, fragment);
|
|
531
|
+
gl.linkProgram(program);
|
|
532
|
+
gl.deleteShader(vertex);
|
|
533
|
+
gl.deleteShader(fragment);
|
|
534
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
535
|
+
const message = gl.getProgramInfoLog(program) || 'Unknown program link error';
|
|
536
|
+
gl.deleteProgram(program);
|
|
537
|
+
throw new Error(message);
|
|
538
|
+
}
|
|
539
|
+
return program;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
class CosmicRenderer {
|
|
543
|
+
constructor(canvas, preset, options = {}) {
|
|
544
|
+
this.canvas = canvas;
|
|
545
|
+
this.preset = { ...preset };
|
|
546
|
+
this.options = { dprCap: 1.75, mouseColor: true, ...options };
|
|
547
|
+
this.gl = canvas.getContext('webgl2', {
|
|
548
|
+
alpha: false,
|
|
549
|
+
antialias: false,
|
|
550
|
+
depth: false,
|
|
551
|
+
powerPreference: this.options.powerPreference || 'high-performance',
|
|
552
|
+
preserveDrawingBuffer: false
|
|
553
|
+
});
|
|
554
|
+
if (!this.gl) throw new Error('WebGL2 is not available');
|
|
555
|
+
|
|
556
|
+
this.program = createProgram(this.gl);
|
|
557
|
+
this.locations = this.#getLocations();
|
|
558
|
+
this.pointer = [0.72, 0.45];
|
|
559
|
+
this.pointerTarget = [...this.pointer];
|
|
560
|
+
this.motion = 0;
|
|
561
|
+
this.motionTarget = 0;
|
|
562
|
+
this.timeOffset = preset.seed * 0.73;
|
|
563
|
+
this.colors = preset.colors.map(hexToRgb01);
|
|
564
|
+
this.visible = true;
|
|
565
|
+
this.disposed = false;
|
|
566
|
+
|
|
567
|
+
this.#setupGeometry();
|
|
568
|
+
this.#bindEvents();
|
|
569
|
+
this.#bindContextEvents();
|
|
570
|
+
this.resize();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#getLocations() {
|
|
574
|
+
const gl = this.gl;
|
|
575
|
+
const uniform = (name) => gl.getUniformLocation(this.program, name);
|
|
576
|
+
return {
|
|
577
|
+
position: gl.getAttribLocation(this.program, 'a_position'),
|
|
578
|
+
resolution: uniform('u_resolution'),
|
|
579
|
+
time: uniform('u_time'),
|
|
580
|
+
seed: uniform('u_seed'),
|
|
581
|
+
motion: uniform('u_motion'),
|
|
582
|
+
pointer: uniform('u_pointer'),
|
|
583
|
+
colorA: uniform('u_colorA'),
|
|
584
|
+
colorB: uniform('u_colorB'),
|
|
585
|
+
colorC: uniform('u_colorC'),
|
|
586
|
+
colorD: uniform('u_colorD')
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
#setupGeometry() {
|
|
591
|
+
const gl = this.gl;
|
|
592
|
+
const vertices = new Float32Array([-1, -1, 3, -1, -1, 3]);
|
|
593
|
+
this.buffer = gl.createBuffer();
|
|
594
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
595
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
#bindEvents() {
|
|
599
|
+
if (this.options.mouseColor === false) return;
|
|
600
|
+
this.eventTarget = this.options.eventTarget || this.canvas.parentElement || this.canvas;
|
|
601
|
+
this.onPointerMove = (event) => {
|
|
602
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
603
|
+
this.pointerTarget[0] = (event.clientX - rect.left) / Math.max(rect.width, 1);
|
|
604
|
+
this.pointerTarget[1] = 1 - (event.clientY - rect.top) / Math.max(rect.height, 1);
|
|
605
|
+
this.motionTarget = 1;
|
|
606
|
+
};
|
|
607
|
+
this.onPointerLeave = () => { this.motionTarget = 0; };
|
|
608
|
+
this.eventTarget.addEventListener('pointermove', this.onPointerMove, { passive: true });
|
|
609
|
+
this.eventTarget.addEventListener('pointerdown', this.onPointerMove, { passive: true });
|
|
610
|
+
this.eventTarget.addEventListener('pointerleave', this.onPointerLeave, { passive: true });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#bindContextEvents() {
|
|
614
|
+
this.onContextLost = (event) => {
|
|
615
|
+
event.preventDefault();
|
|
616
|
+
if (this.disposed) return;
|
|
617
|
+
this.visible = false;
|
|
618
|
+
if (typeof this.options.onContextLost === 'function') this.options.onContextLost(event);
|
|
619
|
+
};
|
|
620
|
+
this.onContextRestored = () => {
|
|
621
|
+
if (this.disposed) return;
|
|
622
|
+
this.program = createProgram(this.gl);
|
|
623
|
+
this.locations = this.#getLocations();
|
|
624
|
+
this.#setupGeometry();
|
|
625
|
+
this.visible = true;
|
|
626
|
+
this.resize();
|
|
627
|
+
if (typeof this.options.onContextRestored === 'function') this.options.onContextRestored();
|
|
628
|
+
};
|
|
629
|
+
this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
|
|
630
|
+
this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
setPreset(preset) {
|
|
634
|
+
this.preset = { ...preset };
|
|
635
|
+
this.colors = preset.colors.map(hexToRgb01);
|
|
636
|
+
this.timeOffset = preset.seed * 0.73;
|
|
637
|
+
}
|
|
638
|
+
|
|
605
639
|
setDprCap(cap) {
|
|
606
640
|
this.options.dprCap = cap;
|
|
607
641
|
this.resize();
|
|
@@ -627,123 +661,128 @@ class CosmicRenderer {
|
|
|
627
661
|
}
|
|
628
662
|
|
|
629
663
|
randomize() {
|
|
630
|
-
this.preset.seed = Math.random() * 100;
|
|
631
|
-
this.timeOffset = Math.random() * 40;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
resize() {
|
|
635
|
-
const dpr = Math.min(window.devicePixelRatio || 1, this.options.dprCap);
|
|
636
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
637
|
-
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
638
|
-
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
639
|
-
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
640
|
-
this.canvas.width = width;
|
|
641
|
-
this.canvas.height = height;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
draw(elapsedSeconds, paused = false) {
|
|
647
|
-
if (this.disposed || !this.visible) return;
|
|
648
|
-
this.
|
|
649
|
-
|
|
650
|
-
this.pointer[
|
|
651
|
-
this.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
gl.
|
|
655
|
-
gl.
|
|
656
|
-
gl.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
gl.
|
|
661
|
-
gl.uniform1f(this.locations.
|
|
662
|
-
gl.
|
|
663
|
-
gl.
|
|
664
|
-
gl.
|
|
665
|
-
gl.uniform3fv(this.locations.
|
|
666
|
-
gl.uniform3fv(this.locations.
|
|
667
|
-
gl.
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
this.
|
|
674
|
-
|
|
675
|
-
target.removeEventListener('
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
this.gl.deleteBuffer(this.buffer);
|
|
679
|
-
this.gl.deleteProgram(this.program);
|
|
680
|
-
const lose = this.gl.getExtension('WEBGL_lose_context');
|
|
681
|
-
if (lose) lose.loseContext();
|
|
682
|
-
}
|
|
664
|
+
this.preset.seed = Math.random() * 100;
|
|
665
|
+
this.timeOffset = Math.random() * 40;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
resize() {
|
|
669
|
+
const dpr = Math.min(window.devicePixelRatio || 1, this.options.dprCap);
|
|
670
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
671
|
+
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
672
|
+
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
673
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
674
|
+
this.canvas.width = width;
|
|
675
|
+
this.canvas.height = height;
|
|
676
|
+
}
|
|
677
|
+
this.gl.viewport(0, 0, width, height);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
draw(elapsedSeconds, paused = false) {
|
|
681
|
+
if (this.disposed || !this.visible) return;
|
|
682
|
+
const gl = this.gl;
|
|
683
|
+
this.pointer[0] += (this.pointerTarget[0] - this.pointer[0]) * 0.08;
|
|
684
|
+
this.pointer[1] += (this.pointerTarget[1] - this.pointer[1]) * 0.08;
|
|
685
|
+
this.motion += (this.motionTarget - this.motion) * 0.07;
|
|
686
|
+
|
|
687
|
+
gl.useProgram(this.program);
|
|
688
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
689
|
+
gl.enableVertexAttribArray(this.locations.position);
|
|
690
|
+
gl.vertexAttribPointer(this.locations.position, 2, gl.FLOAT, false, 0, 0);
|
|
691
|
+
|
|
692
|
+
gl.uniform2f(this.locations.resolution, this.canvas.width, this.canvas.height);
|
|
693
|
+
gl.uniform1f(this.locations.time, this.timeOffset + (paused ? 0 : elapsedSeconds * this.preset.speed));
|
|
694
|
+
gl.uniform1f(this.locations.seed, this.preset.seed);
|
|
695
|
+
gl.uniform1f(this.locations.motion, this.motion);
|
|
696
|
+
gl.uniform2f(this.locations.pointer, this.pointer[0], this.pointer[1]);
|
|
697
|
+
gl.uniform3fv(this.locations.colorA, this.colors[0]);
|
|
698
|
+
gl.uniform3fv(this.locations.colorB, this.colors[1]);
|
|
699
|
+
gl.uniform3fv(this.locations.colorC, this.colors[2]);
|
|
700
|
+
gl.uniform3fv(this.locations.colorD, this.colors[3]);
|
|
701
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
dispose() {
|
|
705
|
+
this.disposed = true;
|
|
706
|
+
const target = this.eventTarget || this.canvas;
|
|
707
|
+
target.removeEventListener('pointermove', this.onPointerMove);
|
|
708
|
+
target.removeEventListener('pointerdown', this.onPointerMove);
|
|
709
|
+
target.removeEventListener('pointerleave', this.onPointerLeave);
|
|
710
|
+
this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
|
|
711
|
+
this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
712
|
+
this.gl.deleteBuffer(this.buffer);
|
|
713
|
+
this.gl.deleteProgram(this.program);
|
|
714
|
+
const lose = this.gl.getExtension('WEBGL_lose_context');
|
|
715
|
+
if (lose) lose.loseContext();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function rgb(color, alpha = 1) {
|
|
720
|
+
const [r, g, b] = hexToRgb01(color).map((value) => Math.round(value * 255));
|
|
721
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
683
722
|
}
|
|
684
723
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
class FallbackRenderer {
|
|
691
|
-
constructor(canvas, preset) {
|
|
724
|
+
class FallbackRenderer {
|
|
725
|
+
constructor(canvas, preset, options = {}) {
|
|
692
726
|
this.canvas = canvas;
|
|
693
727
|
this.preset = preset;
|
|
694
728
|
this.context = canvas.getContext('2d');
|
|
695
729
|
this.visible = true;
|
|
696
730
|
this.timePhase = 0;
|
|
731
|
+
this.dprCap = options.dprCap || 1.5;
|
|
697
732
|
}
|
|
698
|
-
|
|
699
|
-
resize() {
|
|
700
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
701
|
-
const dpr = Math.min(window.devicePixelRatio || 1,
|
|
702
|
-
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
703
|
-
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
704
|
-
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
705
|
-
this.canvas.width = width;
|
|
706
|
-
this.canvas.height = height;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
733
|
+
|
|
734
|
+
resize() {
|
|
735
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
736
|
+
const dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
|
|
737
|
+
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
738
|
+
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
739
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
740
|
+
this.canvas.width = width;
|
|
741
|
+
this.canvas.height = height;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
710
745
|
drawNebula(time) {
|
|
711
746
|
const ctx = this.context;
|
|
712
747
|
const { width, height } = this.canvas;
|
|
713
748
|
const t = (time + this.timePhase) * (this.preset.speed || 1);
|
|
714
749
|
const phase = (this.preset.seed || 0) * 0.7;
|
|
715
750
|
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
|
716
|
-
gradient.addColorStop(0, this.preset.colors[0]);
|
|
717
|
-
gradient.addColorStop(0.38, this.preset.colors[1]);
|
|
718
|
-
gradient.addColorStop(0.72, this.preset.colors[2]);
|
|
719
|
-
gradient.addColorStop(1, this.preset.colors[3]);
|
|
720
|
-
ctx.fillStyle = gradient;
|
|
721
|
-
ctx.fillRect(0, 0, width, height);
|
|
722
|
-
|
|
751
|
+
gradient.addColorStop(0, this.preset.colors[0]);
|
|
752
|
+
gradient.addColorStop(0.38, this.preset.colors[1]);
|
|
753
|
+
gradient.addColorStop(0.72, this.preset.colors[2]);
|
|
754
|
+
gradient.addColorStop(1, this.preset.colors[3]);
|
|
755
|
+
ctx.fillStyle = gradient;
|
|
756
|
+
ctx.fillRect(0, 0, width, height);
|
|
757
|
+
|
|
723
758
|
ctx.globalCompositeOperation = 'screen';
|
|
724
759
|
for (let index = 0; index < 6; index += 1) {
|
|
725
760
|
const x = (0.5 + 0.45 * Math.sin(t * 0.32 + index * 1.7 + phase)) * width;
|
|
726
761
|
const y = (0.5 + 0.4 * Math.cos(t * 0.25 + index + phase)) * height;
|
|
727
|
-
const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
|
|
728
|
-
const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
729
|
-
glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
|
|
730
|
-
glow.addColorStop(1, rgb(this.preset.colors[index % 4], 0));
|
|
731
|
-
ctx.fillStyle = glow;
|
|
732
|
-
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
733
|
-
}
|
|
734
|
-
ctx.globalCompositeOperation = 'source-over';
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
draw(time) {
|
|
738
|
-
if (!this.visible) return;
|
|
739
|
-
this.
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
762
|
+
const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
|
|
763
|
+
const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
764
|
+
glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
|
|
765
|
+
glow.addColorStop(1, rgb(this.preset.colors[index % 4], 0));
|
|
766
|
+
ctx.fillStyle = glow;
|
|
767
|
+
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
768
|
+
}
|
|
769
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
draw(time) {
|
|
773
|
+
if (!this.visible) return;
|
|
774
|
+
this.drawNebula(time);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
setPreset(preset) {
|
|
778
|
+
this.preset = preset;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
setDprCap(cap) {
|
|
782
|
+
this.dprCap = cap;
|
|
783
|
+
this.resize();
|
|
784
|
+
}
|
|
785
|
+
|
|
747
786
|
randomize() {
|
|
748
787
|
if (this.preset) {
|
|
749
788
|
this.preset.seed = Math.random() * 100;
|
|
@@ -754,101 +793,119 @@ class FallbackRenderer {
|
|
|
754
793
|
dispose() {}
|
|
755
794
|
}
|
|
756
795
|
|
|
757
|
-
/**
|
|
758
|
-
* Document-level shared rAF scheduler. Every component instance subscribes
|
|
759
|
-
* its own frame callback; the whole page runs ONE animation loop (like the
|
|
760
|
-
* original demo), which avoids jank from many competing rAF loops.
|
|
761
|
-
*/
|
|
762
|
-
const subscribers = [];
|
|
763
|
-
let running = false;
|
|
764
|
-
let rafId = 0;
|
|
765
|
-
let last = 0;
|
|
766
|
-
|
|
767
|
-
function tick(now) {
|
|
768
|
-
if (!running) return;
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Document-level shared rAF scheduler. Every component instance subscribes
|
|
798
|
+
* its own frame callback; the whole page runs ONE animation loop (like the
|
|
799
|
+
* original demo), which avoids jank from many competing rAF loops.
|
|
800
|
+
*/
|
|
801
|
+
const subscribers = [];
|
|
802
|
+
let running = false;
|
|
803
|
+
let rafId = 0;
|
|
804
|
+
let last = 0;
|
|
805
|
+
|
|
806
|
+
function tick(now) {
|
|
807
|
+
if (!running) return;
|
|
808
|
+
const activeItems = [];
|
|
809
|
+
{
|
|
810
|
+
for (const item of subscribers.slice()) {
|
|
811
|
+
try {
|
|
812
|
+
if (!item.isPaused()) activeItems.push(item);
|
|
813
|
+
} catch {}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (activeItems.length === 0) {
|
|
817
|
+
running = false;
|
|
818
|
+
rafId = 0;
|
|
819
|
+
last = 0;
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
|
|
823
|
+
last = now;
|
|
824
|
+
// Schedule the next frame BEFORE running callbacks so one throwing
|
|
825
|
+
// subscriber can never kill the whole animation loop.
|
|
826
|
+
rafId = requestAnimationFrame(tick);
|
|
827
|
+
for (const item of activeItems) {
|
|
828
|
+
try {
|
|
829
|
+
item.onFrame(delta, now);
|
|
830
|
+
} catch (error) {
|
|
779
831
|
console.warn('[dlc-ui] frame error:', error);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (subscribers.length === 0) {
|
|
835
|
+
cancelAnimationFrame(rafId);
|
|
836
|
+
running = false;
|
|
837
|
+
rafId = 0;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function start() {
|
|
842
|
+
if (running) return;
|
|
843
|
+
running = true;
|
|
844
|
+
last = 0;
|
|
845
|
+
rafId = requestAnimationFrame(tick);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function wakeScheduler() {
|
|
849
|
+
start();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function subscribeScheduler(onFrame, isPaused) {
|
|
853
|
+
const item = { onFrame, isPaused };
|
|
854
|
+
subscribers.push(item);
|
|
855
|
+
start();
|
|
856
|
+
return () => {
|
|
857
|
+
const index = subscribers.indexOf(item);
|
|
858
|
+
if (index !== -1) subscribers.splice(index, 1);
|
|
859
|
+
if (subscribers.length === 0 && rafId) {
|
|
860
|
+
cancelAnimationFrame(rafId);
|
|
861
|
+
running = false;
|
|
862
|
+
rafId = 0;
|
|
863
|
+
}
|
|
864
|
+
};
|
|
810
865
|
}
|
|
811
866
|
|
|
812
|
-
/**
|
|
813
|
-
* Gates drawing on "element intersects viewport AND the page tab is visible".
|
|
814
|
-
* Falls back to always-visible when IntersectionObserver is unavailable.
|
|
815
|
-
*/
|
|
816
|
-
function createVisibilityGuard(element) {
|
|
817
|
-
let intersecting = true;
|
|
818
|
-
let pageVisible = typeof document === 'undefined' || !document.hidden;
|
|
819
|
-
let disposed = false;
|
|
820
|
-
let observer = null;
|
|
821
|
-
|
|
822
|
-
if (typeof IntersectionObserver !== 'undefined') {
|
|
823
|
-
observer = new IntersectionObserver(
|
|
824
|
-
(entries) => {
|
|
825
|
-
intersecting = entries.some((entry) => entry.isIntersecting);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
867
|
+
/**
|
|
868
|
+
* Gates drawing on "element intersects viewport AND the page tab is visible".
|
|
869
|
+
* Falls back to always-visible when IntersectionObserver is unavailable.
|
|
870
|
+
*/
|
|
871
|
+
function createVisibilityGuard(element, onChange = null) {
|
|
872
|
+
let intersecting = true;
|
|
873
|
+
let pageVisible = typeof document === 'undefined' || !document.hidden;
|
|
874
|
+
let disposed = false;
|
|
875
|
+
let observer = null;
|
|
876
|
+
|
|
877
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
878
|
+
observer = new IntersectionObserver(
|
|
879
|
+
(entries) => {
|
|
880
|
+
intersecting = entries.some((entry) => entry.isIntersecting);
|
|
881
|
+
if (typeof onChange === 'function') onChange();
|
|
882
|
+
},
|
|
883
|
+
{ rootMargin: '180px' }
|
|
884
|
+
);
|
|
885
|
+
observer.observe(element);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const onVisibilityChange = () => {
|
|
889
|
+
pageVisible = typeof document !== 'undefined' && !document.hidden;
|
|
890
|
+
if (typeof onChange === 'function') onChange();
|
|
891
|
+
};
|
|
892
|
+
if (typeof document !== 'undefined') {
|
|
893
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
isVisible() {
|
|
898
|
+
return intersecting && pageVisible;
|
|
899
|
+
},
|
|
900
|
+
dispose() {
|
|
901
|
+
if (disposed) return;
|
|
902
|
+
disposed = true;
|
|
903
|
+
if (observer) observer.disconnect();
|
|
904
|
+
if (typeof document !== 'undefined') {
|
|
905
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
};
|
|
852
909
|
}
|
|
853
910
|
|
|
854
911
|
/**
|
|
@@ -867,30 +924,96 @@ function nextTick(fn) {
|
|
|
867
924
|
}
|
|
868
925
|
}
|
|
869
926
|
|
|
870
|
-
function
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
927
|
+
function normalizeFps(value, fallback = 60) {
|
|
928
|
+
const fps = Number(value);
|
|
929
|
+
if (!Number.isFinite(fps)) return fallback;
|
|
930
|
+
return Math.min(60, Math.max(1, fps));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function createFrameGate(initialFps = 60) {
|
|
934
|
+
let fps = normalizeFps(initialFps);
|
|
935
|
+
let elapsed = 0;
|
|
936
|
+
return {
|
|
937
|
+
shouldDraw(delta) {
|
|
938
|
+
elapsed += delta;
|
|
939
|
+
const interval = 1 / fps;
|
|
940
|
+
if (elapsed + 0.0001 < interval) return false;
|
|
941
|
+
elapsed %= interval;
|
|
942
|
+
return true;
|
|
943
|
+
},
|
|
944
|
+
setFps(value) {
|
|
945
|
+
fps = normalizeFps(value, fps);
|
|
946
|
+
elapsed = 0;
|
|
947
|
+
return fps;
|
|
948
|
+
},
|
|
949
|
+
getFps() {
|
|
950
|
+
return fps;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function createReducedMotionPreference(enabled, onChange) {
|
|
956
|
+
const query = enabled && typeof matchMedia !== 'undefined'
|
|
957
|
+
? matchMedia('(prefers-reduced-motion: reduce)')
|
|
958
|
+
: null;
|
|
959
|
+
const notify = () => {
|
|
960
|
+
if (typeof onChange === 'function') onChange(Boolean(query && query.matches));
|
|
961
|
+
};
|
|
962
|
+
if (query) {
|
|
963
|
+
if (typeof query.addEventListener === 'function') query.addEventListener('change', notify);
|
|
964
|
+
else if (typeof query.addListener === 'function') query.addListener(notify);
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
matches() {
|
|
968
|
+
return Boolean(query && query.matches);
|
|
969
|
+
},
|
|
970
|
+
dispose() {
|
|
971
|
+
if (!query) return;
|
|
972
|
+
if (typeof query.removeEventListener === 'function') query.removeEventListener('change', notify);
|
|
973
|
+
else if (typeof query.removeListener === 'function') query.removeListener(notify);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function toFiniteNumber(value) {
|
|
979
|
+
if (value == null || typeof value === 'boolean') return null;
|
|
980
|
+
if (typeof value === 'string' && value.trim() === '') return null;
|
|
981
|
+
const number = Number(value);
|
|
982
|
+
return Number.isFinite(number) ? number : null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
875
986
|
* Mount a cosmic (nebula) capsule into `container`.
|
|
876
987
|
*
|
|
877
988
|
* Options: preset (literary name), width, height (number=px or string with
|
|
878
989
|
* px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
|
|
879
990
|
* width in percent), text (HTML string or DOM nodes for the text slot),
|
|
880
991
|
* colorContent (HTML string or DOM nodes for the color slot), quality,
|
|
881
|
-
* renderer,
|
|
992
|
+
* renderer, quality, renderScale, powerPreference, fps, paused/static,
|
|
993
|
+
* respectReducedMotion, mouseColor, cssVars.
|
|
882
994
|
*/
|
|
883
995
|
function createCapsule(container, options = {}) {
|
|
884
|
-
if (!container || typeof container.appendChild !== 'function') {
|
|
885
|
-
throw new Error('createCapsule: container element is required');
|
|
886
|
-
}
|
|
887
|
-
|
|
996
|
+
if (!container || typeof container.appendChild !== 'function') {
|
|
997
|
+
throw new Error('createCapsule: container element is required');
|
|
998
|
+
}
|
|
999
|
+
|
|
888
1000
|
const preset = { ...getPreset('capsule', options.preset ?? '初光') };
|
|
889
1001
|
const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
|
|
890
|
-
const normalizedColors = merged.colors.map(normalizeColor);
|
|
891
|
-
if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
892
|
-
|
|
893
|
-
|
|
1002
|
+
const normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
|
|
1003
|
+
if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
1004
|
+
const initialSeed = toFiniteNumber(merged.seed);
|
|
1005
|
+
const initialSpeed = toFiniteNumber(merged.speed);
|
|
1006
|
+
if (initialSeed !== null) preset.seed = initialSeed;
|
|
1007
|
+
if (initialSpeed !== null) preset.speed = initialSpeed;
|
|
1008
|
+
merged.colors = [...preset.colors];
|
|
1009
|
+
merged.seed = preset.seed;
|
|
1010
|
+
merged.speed = preset.speed;
|
|
1011
|
+
const optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
|
|
1012
|
+
let colorOverride = optionColors.length === 4 && optionColors.every(Boolean)
|
|
1013
|
+
? [...optionColors]
|
|
1014
|
+
: null;
|
|
1015
|
+
let seedOverride = options.seed !== undefined ? toFiniteNumber(options.seed) : null;
|
|
1016
|
+
let speedOverride = options.speed !== undefined ? toFiniteNumber(options.speed) : null;
|
|
894
1017
|
let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
|
|
895
1018
|
|
|
896
1019
|
const root = document.createElement('div');
|
|
@@ -938,35 +1061,67 @@ function createCapsule(container, options = {}) {
|
|
|
938
1061
|
root.appendChild(visualLayer);
|
|
939
1062
|
root.appendChild(canvas);
|
|
940
1063
|
container.appendChild(root);
|
|
941
|
-
|
|
1064
|
+
|
|
942
1065
|
const emitter = createEmitter();
|
|
943
|
-
let
|
|
1066
|
+
let manuallyPaused = merged.paused === true;
|
|
1067
|
+
let reducedPaused = false;
|
|
1068
|
+
let staticMode = merged.static === true;
|
|
1069
|
+
let contextLost = false;
|
|
944
1070
|
let disposed = false;
|
|
1071
|
+
let renderOnce = () => {};
|
|
945
1072
|
const dirty = {
|
|
946
1073
|
preset: false,
|
|
947
1074
|
seed: false,
|
|
948
1075
|
speed: false,
|
|
949
1076
|
colors: false,
|
|
950
1077
|
textRatio: false,
|
|
951
|
-
cssVars: false
|
|
1078
|
+
cssVars: false,
|
|
1079
|
+
mouseColor: false,
|
|
1080
|
+
quality: false,
|
|
1081
|
+
renderScale: false,
|
|
1082
|
+
paused: false,
|
|
1083
|
+
static: false,
|
|
1084
|
+
fps: false
|
|
952
1085
|
};
|
|
953
|
-
|
|
954
|
-
let renderer;
|
|
955
|
-
const useWebgl = merged.renderer !== 'canvas2d';
|
|
956
|
-
if (useWebgl) {
|
|
957
|
-
try {
|
|
958
|
-
renderer = new CosmicRenderer(canvas, merged, {
|
|
959
|
-
|
|
960
|
-
|
|
1086
|
+
|
|
1087
|
+
let renderer;
|
|
1088
|
+
const useWebgl = merged.renderer !== 'canvas2d';
|
|
1089
|
+
if (useWebgl) {
|
|
1090
|
+
try {
|
|
1091
|
+
renderer = new CosmicRenderer(canvas, merged, {
|
|
1092
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
1093
|
+
mouseColor: merged.mouseColor !== false,
|
|
1094
|
+
powerPreference: merged.powerPreference,
|
|
1095
|
+
onContextLost: () => {
|
|
1096
|
+
contextLost = true;
|
|
1097
|
+
emitter.emit('contextlost', {});
|
|
1098
|
+
},
|
|
1099
|
+
onContextRestored: () => {
|
|
1100
|
+
contextLost = false;
|
|
1101
|
+
emitter.emit('contextrestored', {});
|
|
1102
|
+
renderOnce();
|
|
1103
|
+
wakeScheduler();
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
renderer = new FallbackRenderer(canvas, merged, {
|
|
1108
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale)
|
|
1109
|
+
});
|
|
961
1110
|
nextTick(() => {
|
|
962
1111
|
if (disposed) return;
|
|
963
1112
|
emitter.emit('error', { message: String(error && error.message ? error.message : error) });
|
|
964
1113
|
});
|
|
965
|
-
}
|
|
966
|
-
} else {
|
|
967
|
-
renderer = new FallbackRenderer(canvas, merged
|
|
968
|
-
|
|
969
|
-
|
|
1114
|
+
}
|
|
1115
|
+
} else {
|
|
1116
|
+
renderer = new FallbackRenderer(canvas, merged, {
|
|
1117
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
let animationTime = 0;
|
|
1122
|
+
const frameGate = createFrameGate(merged.fps);
|
|
1123
|
+
renderOnce = () => renderer.draw(animationTime);
|
|
1124
|
+
|
|
970
1125
|
const applySize = () => {
|
|
971
1126
|
root.style.width = parseSize(merged.width);
|
|
972
1127
|
root.style.height = parseSize(merged.height);
|
|
@@ -980,39 +1135,53 @@ function createCapsule(container, options = {}) {
|
|
|
980
1135
|
}
|
|
981
1136
|
renderer.resize();
|
|
982
1137
|
};
|
|
983
|
-
applySize();
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1138
|
+
applySize();
|
|
1139
|
+
|
|
1140
|
+
const resize = () => {
|
|
1141
|
+
renderer.resize();
|
|
1142
|
+
if (manuallyPaused || reducedPaused || staticMode) renderOnce();
|
|
1143
|
+
};
|
|
1144
|
+
const resizeObserver = typeof ResizeObserver !== 'undefined'
|
|
1145
|
+
? new ResizeObserver(resize)
|
|
1146
|
+
: null;
|
|
1147
|
+
if (resizeObserver) resizeObserver.observe(root);
|
|
1148
|
+
else window.addEventListener('resize', resize);
|
|
1149
|
+
|
|
1150
|
+
const visibility = createVisibilityGuard(root, wakeScheduler);
|
|
1151
|
+
const motionPreference = createReducedMotionPreference(
|
|
1152
|
+
merged.respectReducedMotion,
|
|
1153
|
+
(matches) => {
|
|
1154
|
+
reducedPaused = matches;
|
|
1155
|
+
if (matches) renderOnce();
|
|
1156
|
+
else wakeScheduler();
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
reducedPaused = motionPreference.matches();
|
|
1160
|
+
const isMotionPaused = () => manuallyPaused || reducedPaused || staticMode || contextLost;
|
|
1161
|
+
|
|
993
1162
|
root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
|
|
994
1163
|
root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
|
|
995
|
-
root.addEventListener('click', (event) => {
|
|
996
|
-
emitter.emit('click', { event, preset: { ...preset } });
|
|
997
|
-
});
|
|
998
|
-
root.addEventListener('pointerdown', (event) => emitter.emit('pointerdown', { event, preset: { ...preset } }));
|
|
999
|
-
root.addEventListener('pointerup', (event) => emitter.emit('pointerup', { event, preset: { ...preset } }));
|
|
1000
|
-
root.addEventListener('dblclick', (event) => emitter.emit('dblclick', { event, preset: { ...preset } }));
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const unsubscribe = subscribeScheduler(
|
|
1004
|
-
(delta) => {
|
|
1005
|
-
animationTime += delta;
|
|
1006
|
-
if (
|
|
1007
|
-
},
|
|
1008
|
-
() =>
|
|
1009
|
-
);
|
|
1010
|
-
|
|
1164
|
+
root.addEventListener('click', (event) => {
|
|
1165
|
+
emitter.emit('click', { event, preset: { ...preset } });
|
|
1166
|
+
});
|
|
1167
|
+
root.addEventListener('pointerdown', (event) => emitter.emit('pointerdown', { event, preset: { ...preset } }));
|
|
1168
|
+
root.addEventListener('pointerup', (event) => emitter.emit('pointerup', { event, preset: { ...preset } }));
|
|
1169
|
+
root.addEventListener('dblclick', (event) => emitter.emit('dblclick', { event, preset: { ...preset } }));
|
|
1170
|
+
|
|
1171
|
+
renderOnce();
|
|
1172
|
+
const unsubscribe = subscribeScheduler(
|
|
1173
|
+
(delta) => {
|
|
1174
|
+
animationTime += delta;
|
|
1175
|
+
if (frameGate.shouldDraw(delta)) renderer.draw(animationTime);
|
|
1176
|
+
},
|
|
1177
|
+
() => isMotionPaused() || !visibility.isVisible()
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1011
1180
|
nextTick(() => {
|
|
1012
1181
|
if (disposed) return;
|
|
1013
1182
|
emitter.emit('ready', { preset: { ...preset } });
|
|
1014
1183
|
});
|
|
1015
|
-
|
|
1184
|
+
|
|
1016
1185
|
const syncTheme = () => {
|
|
1017
1186
|
root.dataset.mode = 'nebula';
|
|
1018
1187
|
root.dataset.theme = preset.theme || 'light';
|
|
@@ -1020,45 +1189,55 @@ function createCapsule(container, options = {}) {
|
|
|
1020
1189
|
};
|
|
1021
1190
|
|
|
1022
1191
|
return {
|
|
1023
|
-
element: root,
|
|
1024
|
-
canvas,
|
|
1025
|
-
preset,
|
|
1026
|
-
on: emitter.on,
|
|
1027
|
-
off: emitter.off,
|
|
1192
|
+
element: root,
|
|
1193
|
+
canvas,
|
|
1194
|
+
preset,
|
|
1195
|
+
on: emitter.on,
|
|
1196
|
+
off: emitter.off,
|
|
1028
1197
|
setPreset(ref) {
|
|
1029
1198
|
const next = getPreset('capsule', ref);
|
|
1030
1199
|
dirty.preset = true;
|
|
1031
1200
|
Object.assign(preset, next);
|
|
1201
|
+
if (colorOverride) preset.colors = [...colorOverride];
|
|
1202
|
+
if (seedOverride !== null) preset.seed = seedOverride;
|
|
1203
|
+
if (speedOverride !== null) preset.speed = speedOverride;
|
|
1032
1204
|
const nextColors = preset.colors.map(normalizeColor);
|
|
1033
1205
|
if (nextColors.every(Boolean)) preset.colors = nextColors;
|
|
1034
1206
|
renderer.setPreset({ ...preset });
|
|
1035
1207
|
syncTheme();
|
|
1036
1208
|
renderer.resize();
|
|
1209
|
+
if (isMotionPaused()) renderOnce();
|
|
1037
1210
|
emitter.emit('presetchange', { preset: { ...next } });
|
|
1038
1211
|
return this;
|
|
1039
|
-
},
|
|
1212
|
+
},
|
|
1040
1213
|
setColors(colors) {
|
|
1041
1214
|
const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
|
|
1042
1215
|
if (next.length !== 4 || next.some((color) => !color)) return this;
|
|
1043
1216
|
dirty.colors = true;
|
|
1217
|
+
colorOverride = [...next];
|
|
1044
1218
|
preset.colors = next;
|
|
1045
1219
|
renderer.setPreset({ ...preset, colors: next });
|
|
1220
|
+
if (isMotionPaused()) renderOnce();
|
|
1046
1221
|
return this;
|
|
1047
1222
|
},
|
|
1048
1223
|
setSeed(seed) {
|
|
1049
|
-
const value =
|
|
1050
|
-
if (
|
|
1224
|
+
const value = toFiniteNumber(seed);
|
|
1225
|
+
if (value === null) return this;
|
|
1051
1226
|
dirty.seed = true;
|
|
1227
|
+
seedOverride = value;
|
|
1052
1228
|
preset.seed = value;
|
|
1053
1229
|
renderer.setPreset({ ...preset });
|
|
1230
|
+
if (isMotionPaused()) renderOnce();
|
|
1054
1231
|
return this;
|
|
1055
1232
|
},
|
|
1056
1233
|
setSpeed(speed) {
|
|
1057
|
-
const value =
|
|
1058
|
-
if (
|
|
1234
|
+
const value = toFiniteNumber(speed);
|
|
1235
|
+
if (value === null) return this;
|
|
1059
1236
|
dirty.speed = true;
|
|
1237
|
+
speedOverride = value;
|
|
1060
1238
|
preset.speed = value;
|
|
1061
1239
|
renderer.setPreset({ ...preset });
|
|
1240
|
+
if (isMotionPaused()) renderOnce();
|
|
1062
1241
|
return this;
|
|
1063
1242
|
},
|
|
1064
1243
|
setText(content) {
|
|
@@ -1091,6 +1270,7 @@ function createCapsule(container, options = {}) {
|
|
|
1091
1270
|
return this;
|
|
1092
1271
|
},
|
|
1093
1272
|
setMouseColor(value) {
|
|
1273
|
+
dirty.mouseColor = true;
|
|
1094
1274
|
merged.mouseColor = value !== false;
|
|
1095
1275
|
if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
|
|
1096
1276
|
return this;
|
|
@@ -1105,36 +1285,80 @@ function createCapsule(container, options = {}) {
|
|
|
1105
1285
|
merged.height = height;
|
|
1106
1286
|
}
|
|
1107
1287
|
applySize();
|
|
1288
|
+
if (isMotionPaused()) renderOnce();
|
|
1108
1289
|
return this;
|
|
1109
1290
|
},
|
|
1110
1291
|
randomize() {
|
|
1111
1292
|
this.setSeed(Math.random() * 100);
|
|
1112
1293
|
return this;
|
|
1113
1294
|
},
|
|
1114
|
-
pause() {
|
|
1115
|
-
paused = true;
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1295
|
+
pause() {
|
|
1296
|
+
dirty.paused = true;
|
|
1297
|
+
manuallyPaused = true;
|
|
1298
|
+
renderOnce();
|
|
1299
|
+
return this;
|
|
1300
|
+
},
|
|
1301
|
+
resume() {
|
|
1302
|
+
dirty.paused = true;
|
|
1303
|
+
manuallyPaused = false;
|
|
1304
|
+
wakeScheduler();
|
|
1305
|
+
return this;
|
|
1306
|
+
},
|
|
1307
|
+
setPaused(value) {
|
|
1308
|
+
return value ? this.pause() : this.resume();
|
|
1309
|
+
},
|
|
1310
|
+
setStatic(value) {
|
|
1311
|
+
dirty.static = true;
|
|
1312
|
+
staticMode = value === true;
|
|
1313
|
+
merged.static = staticMode;
|
|
1314
|
+
if (staticMode) renderOnce();
|
|
1315
|
+
else wakeScheduler();
|
|
1316
|
+
return this;
|
|
1317
|
+
},
|
|
1318
|
+
setFps(fps) {
|
|
1319
|
+
dirty.fps = true;
|
|
1320
|
+
merged.fps = frameGate.setFps(fps);
|
|
1321
|
+
wakeScheduler();
|
|
1322
|
+
return this;
|
|
1323
|
+
},
|
|
1324
|
+
setQuality(quality) {
|
|
1325
|
+
dirty.quality = true;
|
|
1326
|
+
merged.quality = quality;
|
|
1327
|
+
if (typeof renderer.setDprCap === 'function') {
|
|
1328
|
+
renderer.setDprCap(effectiveDprCap(quality, merged.renderScale));
|
|
1329
|
+
}
|
|
1330
|
+
if (isMotionPaused()) renderOnce();
|
|
1331
|
+
return this;
|
|
1332
|
+
},
|
|
1333
|
+
setRenderScale(renderScale) {
|
|
1334
|
+
const value = Number(renderScale);
|
|
1335
|
+
if (!Number.isFinite(value)) return this;
|
|
1336
|
+
dirty.renderScale = true;
|
|
1337
|
+
merged.renderScale = Math.min(1, Math.max(0.25, value));
|
|
1338
|
+
if (typeof renderer.setDprCap === 'function') {
|
|
1339
|
+
renderer.setDprCap(effectiveDprCap(merged.quality, merged.renderScale));
|
|
1340
|
+
}
|
|
1341
|
+
if (isMotionPaused()) renderOnce();
|
|
1342
|
+
return this;
|
|
1343
|
+
},
|
|
1127
1344
|
dispose() {
|
|
1128
1345
|
disposed = true;
|
|
1129
1346
|
unsubscribe();
|
|
1130
|
-
visibility.dispose();
|
|
1131
|
-
|
|
1132
|
-
|
|
1347
|
+
visibility.dispose();
|
|
1348
|
+
motionPreference.dispose();
|
|
1349
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
1350
|
+
else window.removeEventListener('resize', resize);
|
|
1133
1351
|
renderer.dispose();
|
|
1134
1352
|
root.remove();
|
|
1135
1353
|
},
|
|
1136
1354
|
get textRatio() { return merged.textRatio; },
|
|
1137
1355
|
get cssVars() { return merged.cssVars; },
|
|
1356
|
+
get mouseColor() { return merged.mouseColor; },
|
|
1357
|
+
get quality() { return merged.quality; },
|
|
1358
|
+
get renderScale() { return merged.renderScale; },
|
|
1359
|
+
get paused() { return manuallyPaused; },
|
|
1360
|
+
get static() { return staticMode; },
|
|
1361
|
+
get fps() { return frameGate.getFps(); },
|
|
1138
1362
|
dirty
|
|
1139
1363
|
};
|
|
1140
1364
|
}
|