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