@5even7/dlc-ui 0.2.11 → 0.2.12
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 +42 -42
- package/LICENSE +21 -21
- package/README.md +255 -255
- package/dist/capsule.cjs +298 -71
- package/dist/capsule.mjs +828 -604
- package/dist/color.cjs +329 -82
- package/dist/color.mjs +879 -631
- package/dist/index.cjs +979 -252
- package/dist/index.mjs +2695 -1979
- package/dist/index.umd.js +979 -252
- package/dist/index.umd.min.js +1 -1
- package/dist/progress.cjs +614 -171
- package/dist/progress.mjs +1815 -1379
- package/dist/vue2.cjs +1123 -265
- package/dist/vue2.mjs +2884 -2059
- package/dist/vue3.cjs +1123 -265
- package/dist/vue3.mjs +2884 -2059
- 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/index.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
|
-
};
|
|
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;
|
|
57
|
-
}
|
|
58
|
-
|
|
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;
|
|
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;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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;
|
|
57
|
+
}
|
|
58
|
+
|
|
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;
|
|
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;
|
|
76
|
+
}
|
|
77
|
+
|
|
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);
|
|
82
|
+
}
|
|
83
|
+
|
|
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$1 = {
|
|
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$1.original] },
|
|
117
123
|
{ id: 'ocean', code: 'NC-02', name: '沧溟', group: 'cold', seed: 8.2, speed: 0.48, colors: [...PALETTES$1.ocean] },
|
|
@@ -121,47 +127,47 @@ const CAPSULE_PRESETS = [
|
|
|
121
127
|
{ id: 'plus', code: 'NC-06', name: '熔金', group: 'warm', seed: 51.3, speed: 0.5, colors: [...PALETTES$1.plus] }
|
|
122
128
|
];
|
|
123
129
|
|
|
124
|
-
const PROGRESS_PRESETS = [
|
|
130
|
+
const PROGRESS_PRESETS = [
|
|
125
131
|
{
|
|
126
132
|
id: 'model-training',
|
|
127
133
|
code: 'NC-10',
|
|
128
134
|
name: '星火',
|
|
129
|
-
subtitle: 'SHA 4.5 + 100 2026 TKN',
|
|
130
|
-
group: 'progress',
|
|
131
|
-
initialProgress: 43,
|
|
132
|
-
edgeStyle: 'flow',
|
|
133
|
-
colors: ['#2B1025', '#FF3F94', '#FF8A3D', '#FFF06A']
|
|
134
|
-
},
|
|
135
|
+
subtitle: 'SHA 4.5 + 100 2026 TKN',
|
|
136
|
+
group: 'progress',
|
|
137
|
+
initialProgress: 43,
|
|
138
|
+
edgeStyle: 'flow',
|
|
139
|
+
colors: ['#2B1025', '#FF3F94', '#FF8A3D', '#FFF06A']
|
|
140
|
+
},
|
|
135
141
|
{
|
|
136
142
|
id: 'agent-migration',
|
|
137
143
|
code: 'NC-11',
|
|
138
144
|
name: '迁流',
|
|
139
|
-
subtitle: 'TRANSFERRING PROTOCOL',
|
|
140
|
-
group: 'progress',
|
|
141
|
-
initialProgress: 33,
|
|
142
|
-
edgeStyle: 'flow',
|
|
143
|
-
colors: ['#101C37', '#245BFF', '#00CFFF', '#5DFFE6']
|
|
144
|
-
},
|
|
145
|
+
subtitle: 'TRANSFERRING PROTOCOL',
|
|
146
|
+
group: 'progress',
|
|
147
|
+
initialProgress: 33,
|
|
148
|
+
edgeStyle: 'flow',
|
|
149
|
+
colors: ['#101C37', '#245BFF', '#00CFFF', '#5DFFE6']
|
|
150
|
+
},
|
|
145
151
|
{
|
|
146
152
|
id: 'visual-training',
|
|
147
153
|
code: 'NC-12',
|
|
148
154
|
name: '幻境',
|
|
149
|
-
subtitle: 'GENERATING POWER ++',
|
|
150
|
-
group: 'progress',
|
|
151
|
-
initialProgress: 58,
|
|
152
|
-
edgeStyle: 'flow',
|
|
153
|
-
colors: ['#21142D', '#7042FF', '#42F58D', '#C4FF8A']
|
|
154
|
-
},
|
|
155
|
+
subtitle: 'GENERATING POWER ++',
|
|
156
|
+
group: 'progress',
|
|
157
|
+
initialProgress: 58,
|
|
158
|
+
edgeStyle: 'flow',
|
|
159
|
+
colors: ['#21142D', '#7042FF', '#42F58D', '#C4FF8A']
|
|
160
|
+
},
|
|
155
161
|
{
|
|
156
162
|
id: 'tide',
|
|
157
163
|
code: 'NC-13',
|
|
158
164
|
name: '汐潮',
|
|
159
|
-
subtitle: 'MOON PULL / COAST',
|
|
160
|
-
group: 'progress',
|
|
161
|
-
initialProgress: 30,
|
|
162
|
-
edgeStyle: 'tide',
|
|
163
|
-
colors: ['#0A2239', '#2E9BFF', '#7FE3FF', '#EAF9FF']
|
|
164
|
-
}
|
|
165
|
+
subtitle: 'MOON PULL / COAST',
|
|
166
|
+
group: 'progress',
|
|
167
|
+
initialProgress: 30,
|
|
168
|
+
edgeStyle: 'tide',
|
|
169
|
+
colors: ['#0A2239', '#2E9BFF', '#7FE3FF', '#EAF9FF']
|
|
170
|
+
}
|
|
165
171
|
];
|
|
166
172
|
|
|
167
173
|
const PRESETS = CAPSULE_PRESETS;
|
|
@@ -193,6 +199,11 @@ const DEFAULTS = {
|
|
|
193
199
|
height: 160,
|
|
194
200
|
quality: 'auto',
|
|
195
201
|
renderer: 'auto',
|
|
202
|
+
renderScale: 1,
|
|
203
|
+
powerPreference: 'high-performance',
|
|
204
|
+
fps: 60,
|
|
205
|
+
paused: false,
|
|
206
|
+
static: false,
|
|
196
207
|
respectReducedMotion: true,
|
|
197
208
|
mouseColor: true,
|
|
198
209
|
textRatio: 39
|
|
@@ -202,13 +213,22 @@ const DEFAULTS = {
|
|
|
202
213
|
height: 104,
|
|
203
214
|
min: 0,
|
|
204
215
|
max: 100,
|
|
216
|
+
step: 'any',
|
|
205
217
|
draggable: true,
|
|
218
|
+
keyboard: true,
|
|
206
219
|
disabled: false,
|
|
207
220
|
readonly: false,
|
|
221
|
+
direction: 'ltr',
|
|
222
|
+
precision: 0,
|
|
208
223
|
valueSuffix: '%',
|
|
209
224
|
edgeStyle: 'flow',
|
|
210
225
|
quality: 'auto',
|
|
211
226
|
renderer: 'auto',
|
|
227
|
+
renderScale: 1,
|
|
228
|
+
powerPreference: 'high-performance',
|
|
229
|
+
fps: 60,
|
|
230
|
+
paused: false,
|
|
231
|
+
static: false,
|
|
212
232
|
textRatio: 54,
|
|
213
233
|
showValue: true,
|
|
214
234
|
respectReducedMotion: true
|
|
@@ -223,8 +243,8 @@ const DEFAULTS = {
|
|
|
223
243
|
const COPY = {
|
|
224
244
|
brandName: '画境观屿',
|
|
225
245
|
valueSuffix: '%',
|
|
226
|
-
progressAria: '{brand} {code} 加载进度',
|
|
227
|
-
capsuleAria: '打开 {name} 沉浸预览'
|
|
246
|
+
progressAria: '{brand} {code} 加载进度',
|
|
247
|
+
capsuleAria: '打开 {name} 沉浸预览'
|
|
228
248
|
};
|
|
229
249
|
|
|
230
250
|
/**
|
|
@@ -459,231 +479,254 @@ function validateColors(colors) {
|
|
|
459
479
|
&& colors.every((color) => normalizeColor(color) !== null);
|
|
460
480
|
}
|
|
461
481
|
|
|
462
|
-
const VERTEX_SHADER$1 = `#version 300 es
|
|
463
|
-
in vec2 a_position;
|
|
464
|
-
out vec2 v_uv;
|
|
465
|
-
void main() {
|
|
466
|
-
v_uv = a_position * 0.5 + 0.5;
|
|
467
|
-
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
468
|
-
}`;
|
|
469
|
-
|
|
470
|
-
const FRAGMENT_SHADER$1 = `#version 300 es
|
|
471
|
-
precision highp float;
|
|
472
|
-
|
|
473
|
-
in vec2 v_uv;
|
|
474
|
-
out vec4 outColor;
|
|
475
|
-
|
|
476
|
-
uniform vec2 u_resolution;
|
|
477
|
-
uniform float u_time;
|
|
478
|
-
uniform float u_seed;
|
|
479
|
-
uniform float u_motion;
|
|
480
|
-
uniform vec2 u_pointer;
|
|
481
|
-
uniform vec3 u_colorA;
|
|
482
|
-
uniform vec3 u_colorB;
|
|
483
|
-
uniform vec3 u_colorC;
|
|
484
|
-
uniform vec3 u_colorD;
|
|
485
|
-
|
|
486
|
-
float hash21(vec2 p) {
|
|
487
|
-
p = fract(p * vec2(123.34, 456.21));
|
|
488
|
-
p += dot(p, p + 45.32 + u_seed);
|
|
489
|
-
return fract(p.x * p.y);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
float noise(vec2 p) {
|
|
493
|
-
vec2 i = floor(p);
|
|
494
|
-
vec2 f = fract(p);
|
|
495
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
496
|
-
float a = hash21(i);
|
|
497
|
-
float b = hash21(i + vec2(1.0, 0.0));
|
|
498
|
-
float c = hash21(i + vec2(0.0, 1.0));
|
|
499
|
-
float d = hash21(i + vec2(1.0, 1.0));
|
|
500
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
float fbm(vec2 p) {
|
|
504
|
-
float value = 0.0;
|
|
505
|
-
float amplitude = 0.52;
|
|
506
|
-
mat2 rotation = mat2(0.80, 0.60, -0.60, 0.80);
|
|
507
|
-
for (int i = 0; i < 6; i++) {
|
|
508
|
-
value += amplitude * noise(p);
|
|
509
|
-
p = rotation * p * 2.03 + 17.7;
|
|
510
|
-
amplitude *= 0.5;
|
|
511
|
-
}
|
|
512
|
-
return value;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
float gaussian(float value, float center, float width) {
|
|
516
|
-
return exp(-pow(value - center, 2.0) / max(width, 0.0001));
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
vec3 palette(float t) {
|
|
520
|
-
t = clamp(t, 0.0, 1.0);
|
|
521
|
-
vec3 shadow = mix(u_colorA, u_colorB, smoothstep(0.06, 0.62, t));
|
|
522
|
-
vec3 body = mix(u_colorB, u_colorC, smoothstep(0.30, 0.82, t));
|
|
523
|
-
vec3 highlight = mix(u_colorC, u_colorD, smoothstep(0.74, 1.0, t));
|
|
524
|
-
vec3 restrained = mix(shadow, body, smoothstep(0.26, 0.72, t));
|
|
525
|
-
return mix(restrained, highlight, smoothstep(0.78, 0.97, t));
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
vec3 renderNebula(vec2 uv, vec2 p, vec2 pointer, float distanceToPointer, float t) {
|
|
529
|
-
vec2 delta = p - pointer;
|
|
530
|
-
float influence = exp(-distanceToPointer * 4.6) * u_motion;
|
|
531
|
-
float angle = influence * 1.7;
|
|
532
|
-
mat2 swirl = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
|
|
533
|
-
p = pointer + swirl * delta;
|
|
534
|
-
p += normalize(delta + 0.0001) * influence * 0.08;
|
|
535
|
-
|
|
536
|
-
vec2 drift = vec2(t * 0.22, -t * 0.13);
|
|
537
|
-
vec2 q = vec2(
|
|
538
|
-
fbm(p * 1.35 + drift + u_seed),
|
|
539
|
-
fbm(p * 1.35 + vec2(5.2, 1.3) - drift * 0.85)
|
|
540
|
-
);
|
|
541
|
-
vec2 r = vec2(
|
|
542
|
-
fbm(p * 2.0 + 3.6 * q + vec2(1.7, 9.2) + t * 0.10),
|
|
543
|
-
fbm(p * 2.0 + 3.0 * q + vec2(8.3, 2.8) - t * 0.085)
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
float cloud = fbm(p * 1.7 + 4.2 * r);
|
|
547
|
-
float veins = fbm(p * 4.0 - 2.0 * q + t * 0.065);
|
|
548
|
-
float nebula = smoothstep(0.18, 0.91, cloud * 0.9 + veins * 0.22);
|
|
549
|
-
|
|
550
|
-
vec3 color = palette(nebula);
|
|
551
|
-
color += u_colorD * pow(max(cloud - 0.63, 0.0), 2.0) * 1.05;
|
|
552
|
-
color *= 0.78 + 0.34 * smoothstep(0.15, 0.9, veins);
|
|
553
|
-
|
|
554
|
-
vec2 starGrid = floor((uv + vec2(u_seed * 0.013, 0.0)) * vec2(132.0, 58.0));
|
|
555
|
-
vec2 starCell = fract(uv * vec2(132.0, 58.0)) - 0.5;
|
|
556
|
-
float starRandom = hash21(starGrid);
|
|
557
|
-
float starShape = smoothstep(0.075, 0.0, length(starCell));
|
|
558
|
-
float starMask = step(0.989, starRandom) * starShape;
|
|
559
|
-
float twinkle = 0.35 + 0.65 * sin(t * (1.0 + starRandom * 2.4) + starRandom * 40.0) * 0.5 + 0.5;
|
|
560
|
-
color += starMask * twinkle * mix(u_colorC, u_colorD, starRandom) * 1.05;
|
|
561
|
-
|
|
562
|
-
float pointerGlow = exp(-distanceToPointer * 7.0) * u_motion;
|
|
563
|
-
color += u_colorD * pointerGlow * 0.28;
|
|
564
|
-
return color;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
void main() {
|
|
568
|
-
vec2 uv = v_uv;
|
|
569
|
-
vec2 p = uv - 0.5;
|
|
570
|
-
p.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
571
|
-
|
|
572
|
-
vec2 pointer = u_pointer - 0.5;
|
|
573
|
-
pointer.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
574
|
-
float distanceToPointer = length(p - pointer);
|
|
575
|
-
|
|
576
|
-
vec3 color = renderNebula(uv, p, pointer, distanceToPointer, u_time);
|
|
577
|
-
float vignette = smoothstep(0.94, 0.18, length((uv - 0.5) * vec2(1.0, 1.35)));
|
|
578
|
-
color *= 0.70 + vignette * 0.42;
|
|
579
|
-
color = pow(max(color, vec3(0.0)), vec3(0.88));
|
|
580
|
-
|
|
581
|
-
outColor = vec4(color, 1.0);
|
|
582
|
-
}`;
|
|
583
|
-
|
|
584
|
-
function compileShader$1(gl, type, source) {
|
|
585
|
-
const shader = gl.createShader(type);
|
|
586
|
-
gl.shaderSource(shader, source);
|
|
587
|
-
gl.compileShader(shader);
|
|
588
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
589
|
-
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
590
|
-
gl.deleteShader(shader);
|
|
591
|
-
throw new Error(message);
|
|
592
|
-
}
|
|
593
|
-
return shader;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function createProgram$1(gl) {
|
|
597
|
-
const vertex = compileShader$1(gl, gl.VERTEX_SHADER, VERTEX_SHADER$1);
|
|
598
|
-
const fragment = compileShader$1(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER$1);
|
|
599
|
-
const program = gl.createProgram();
|
|
600
|
-
gl.attachShader(program, vertex);
|
|
601
|
-
gl.attachShader(program, fragment);
|
|
602
|
-
gl.linkProgram(program);
|
|
603
|
-
gl.deleteShader(vertex);
|
|
604
|
-
gl.deleteShader(fragment);
|
|
605
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
606
|
-
const message = gl.getProgramInfoLog(program) || 'Unknown program link error';
|
|
607
|
-
gl.deleteProgram(program);
|
|
608
|
-
throw new Error(message);
|
|
609
|
-
}
|
|
610
|
-
return program;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
class CosmicRenderer {
|
|
614
|
-
constructor(canvas, preset, options = {}) {
|
|
615
|
-
this.canvas = canvas;
|
|
616
|
-
this.preset = { ...preset };
|
|
617
|
-
this.options = { dprCap: 1.75, mouseColor: true, ...options };
|
|
618
|
-
this.gl = canvas.getContext('webgl2', {
|
|
619
|
-
alpha: false,
|
|
620
|
-
antialias: false,
|
|
621
|
-
depth: false,
|
|
622
|
-
powerPreference: 'high-performance',
|
|
623
|
-
preserveDrawingBuffer: false
|
|
624
|
-
});
|
|
625
|
-
if (!this.gl) throw new Error('WebGL2 is not available');
|
|
626
|
-
|
|
627
|
-
this.program = createProgram$1(this.gl);
|
|
628
|
-
this.locations = this.#getLocations();
|
|
629
|
-
this.pointer = [0.72, 0.45];
|
|
630
|
-
this.pointerTarget = [...this.pointer];
|
|
631
|
-
this.motion = 0;
|
|
632
|
-
this.motionTarget = 0;
|
|
633
|
-
this.timeOffset = preset.seed * 0.73;
|
|
634
|
-
this.
|
|
635
|
-
this.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
this.#
|
|
639
|
-
this
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
this.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
this.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
this.
|
|
679
|
-
this.eventTarget.addEventListener('
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
482
|
+
const VERTEX_SHADER$1 = `#version 300 es
|
|
483
|
+
in vec2 a_position;
|
|
484
|
+
out vec2 v_uv;
|
|
485
|
+
void main() {
|
|
486
|
+
v_uv = a_position * 0.5 + 0.5;
|
|
487
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
488
|
+
}`;
|
|
489
|
+
|
|
490
|
+
const FRAGMENT_SHADER$1 = `#version 300 es
|
|
491
|
+
precision highp float;
|
|
492
|
+
|
|
493
|
+
in vec2 v_uv;
|
|
494
|
+
out vec4 outColor;
|
|
495
|
+
|
|
496
|
+
uniform vec2 u_resolution;
|
|
497
|
+
uniform float u_time;
|
|
498
|
+
uniform float u_seed;
|
|
499
|
+
uniform float u_motion;
|
|
500
|
+
uniform vec2 u_pointer;
|
|
501
|
+
uniform vec3 u_colorA;
|
|
502
|
+
uniform vec3 u_colorB;
|
|
503
|
+
uniform vec3 u_colorC;
|
|
504
|
+
uniform vec3 u_colorD;
|
|
505
|
+
|
|
506
|
+
float hash21(vec2 p) {
|
|
507
|
+
p = fract(p * vec2(123.34, 456.21));
|
|
508
|
+
p += dot(p, p + 45.32 + u_seed);
|
|
509
|
+
return fract(p.x * p.y);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
float noise(vec2 p) {
|
|
513
|
+
vec2 i = floor(p);
|
|
514
|
+
vec2 f = fract(p);
|
|
515
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
516
|
+
float a = hash21(i);
|
|
517
|
+
float b = hash21(i + vec2(1.0, 0.0));
|
|
518
|
+
float c = hash21(i + vec2(0.0, 1.0));
|
|
519
|
+
float d = hash21(i + vec2(1.0, 1.0));
|
|
520
|
+
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
float fbm(vec2 p) {
|
|
524
|
+
float value = 0.0;
|
|
525
|
+
float amplitude = 0.52;
|
|
526
|
+
mat2 rotation = mat2(0.80, 0.60, -0.60, 0.80);
|
|
527
|
+
for (int i = 0; i < 6; i++) {
|
|
528
|
+
value += amplitude * noise(p);
|
|
529
|
+
p = rotation * p * 2.03 + 17.7;
|
|
530
|
+
amplitude *= 0.5;
|
|
531
|
+
}
|
|
532
|
+
return value;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
float gaussian(float value, float center, float width) {
|
|
536
|
+
return exp(-pow(value - center, 2.0) / max(width, 0.0001));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
vec3 palette(float t) {
|
|
540
|
+
t = clamp(t, 0.0, 1.0);
|
|
541
|
+
vec3 shadow = mix(u_colorA, u_colorB, smoothstep(0.06, 0.62, t));
|
|
542
|
+
vec3 body = mix(u_colorB, u_colorC, smoothstep(0.30, 0.82, t));
|
|
543
|
+
vec3 highlight = mix(u_colorC, u_colorD, smoothstep(0.74, 1.0, t));
|
|
544
|
+
vec3 restrained = mix(shadow, body, smoothstep(0.26, 0.72, t));
|
|
545
|
+
return mix(restrained, highlight, smoothstep(0.78, 0.97, t));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
vec3 renderNebula(vec2 uv, vec2 p, vec2 pointer, float distanceToPointer, float t) {
|
|
549
|
+
vec2 delta = p - pointer;
|
|
550
|
+
float influence = exp(-distanceToPointer * 4.6) * u_motion;
|
|
551
|
+
float angle = influence * 1.7;
|
|
552
|
+
mat2 swirl = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
|
|
553
|
+
p = pointer + swirl * delta;
|
|
554
|
+
p += normalize(delta + 0.0001) * influence * 0.08;
|
|
555
|
+
|
|
556
|
+
vec2 drift = vec2(t * 0.22, -t * 0.13);
|
|
557
|
+
vec2 q = vec2(
|
|
558
|
+
fbm(p * 1.35 + drift + u_seed),
|
|
559
|
+
fbm(p * 1.35 + vec2(5.2, 1.3) - drift * 0.85)
|
|
560
|
+
);
|
|
561
|
+
vec2 r = vec2(
|
|
562
|
+
fbm(p * 2.0 + 3.6 * q + vec2(1.7, 9.2) + t * 0.10),
|
|
563
|
+
fbm(p * 2.0 + 3.0 * q + vec2(8.3, 2.8) - t * 0.085)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
float cloud = fbm(p * 1.7 + 4.2 * r);
|
|
567
|
+
float veins = fbm(p * 4.0 - 2.0 * q + t * 0.065);
|
|
568
|
+
float nebula = smoothstep(0.18, 0.91, cloud * 0.9 + veins * 0.22);
|
|
569
|
+
|
|
570
|
+
vec3 color = palette(nebula);
|
|
571
|
+
color += u_colorD * pow(max(cloud - 0.63, 0.0), 2.0) * 1.05;
|
|
572
|
+
color *= 0.78 + 0.34 * smoothstep(0.15, 0.9, veins);
|
|
573
|
+
|
|
574
|
+
vec2 starGrid = floor((uv + vec2(u_seed * 0.013, 0.0)) * vec2(132.0, 58.0));
|
|
575
|
+
vec2 starCell = fract(uv * vec2(132.0, 58.0)) - 0.5;
|
|
576
|
+
float starRandom = hash21(starGrid);
|
|
577
|
+
float starShape = smoothstep(0.075, 0.0, length(starCell));
|
|
578
|
+
float starMask = step(0.989, starRandom) * starShape;
|
|
579
|
+
float twinkle = 0.35 + 0.65 * sin(t * (1.0 + starRandom * 2.4) + starRandom * 40.0) * 0.5 + 0.5;
|
|
580
|
+
color += starMask * twinkle * mix(u_colorC, u_colorD, starRandom) * 1.05;
|
|
581
|
+
|
|
582
|
+
float pointerGlow = exp(-distanceToPointer * 7.0) * u_motion;
|
|
583
|
+
color += u_colorD * pointerGlow * 0.28;
|
|
584
|
+
return color;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
void main() {
|
|
588
|
+
vec2 uv = v_uv;
|
|
589
|
+
vec2 p = uv - 0.5;
|
|
590
|
+
p.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
591
|
+
|
|
592
|
+
vec2 pointer = u_pointer - 0.5;
|
|
593
|
+
pointer.x *= u_resolution.x / max(u_resolution.y, 1.0);
|
|
594
|
+
float distanceToPointer = length(p - pointer);
|
|
595
|
+
|
|
596
|
+
vec3 color = renderNebula(uv, p, pointer, distanceToPointer, u_time);
|
|
597
|
+
float vignette = smoothstep(0.94, 0.18, length((uv - 0.5) * vec2(1.0, 1.35)));
|
|
598
|
+
color *= 0.70 + vignette * 0.42;
|
|
599
|
+
color = pow(max(color, vec3(0.0)), vec3(0.88));
|
|
600
|
+
|
|
601
|
+
outColor = vec4(color, 1.0);
|
|
602
|
+
}`;
|
|
603
|
+
|
|
604
|
+
function compileShader$1(gl, type, source) {
|
|
605
|
+
const shader = gl.createShader(type);
|
|
606
|
+
gl.shaderSource(shader, source);
|
|
607
|
+
gl.compileShader(shader);
|
|
608
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
609
|
+
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
610
|
+
gl.deleteShader(shader);
|
|
611
|
+
throw new Error(message);
|
|
612
|
+
}
|
|
613
|
+
return shader;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function createProgram$1(gl) {
|
|
617
|
+
const vertex = compileShader$1(gl, gl.VERTEX_SHADER, VERTEX_SHADER$1);
|
|
618
|
+
const fragment = compileShader$1(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER$1);
|
|
619
|
+
const program = gl.createProgram();
|
|
620
|
+
gl.attachShader(program, vertex);
|
|
621
|
+
gl.attachShader(program, fragment);
|
|
622
|
+
gl.linkProgram(program);
|
|
623
|
+
gl.deleteShader(vertex);
|
|
624
|
+
gl.deleteShader(fragment);
|
|
625
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
626
|
+
const message = gl.getProgramInfoLog(program) || 'Unknown program link error';
|
|
627
|
+
gl.deleteProgram(program);
|
|
628
|
+
throw new Error(message);
|
|
629
|
+
}
|
|
630
|
+
return program;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
class CosmicRenderer {
|
|
634
|
+
constructor(canvas, preset, options = {}) {
|
|
635
|
+
this.canvas = canvas;
|
|
636
|
+
this.preset = { ...preset };
|
|
637
|
+
this.options = { dprCap: 1.75, mouseColor: true, ...options };
|
|
638
|
+
this.gl = canvas.getContext('webgl2', {
|
|
639
|
+
alpha: false,
|
|
640
|
+
antialias: false,
|
|
641
|
+
depth: false,
|
|
642
|
+
powerPreference: this.options.powerPreference || 'high-performance',
|
|
643
|
+
preserveDrawingBuffer: false
|
|
644
|
+
});
|
|
645
|
+
if (!this.gl) throw new Error('WebGL2 is not available');
|
|
646
|
+
|
|
647
|
+
this.program = createProgram$1(this.gl);
|
|
648
|
+
this.locations = this.#getLocations();
|
|
649
|
+
this.pointer = [0.72, 0.45];
|
|
650
|
+
this.pointerTarget = [...this.pointer];
|
|
651
|
+
this.motion = 0;
|
|
652
|
+
this.motionTarget = 0;
|
|
653
|
+
this.timeOffset = preset.seed * 0.73;
|
|
654
|
+
this.colors = preset.colors.map(hexToRgb01$1);
|
|
655
|
+
this.visible = true;
|
|
656
|
+
this.disposed = false;
|
|
657
|
+
|
|
658
|
+
this.#setupGeometry();
|
|
659
|
+
this.#bindEvents();
|
|
660
|
+
this.#bindContextEvents();
|
|
661
|
+
this.resize();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
#getLocations() {
|
|
665
|
+
const gl = this.gl;
|
|
666
|
+
const uniform = (name) => gl.getUniformLocation(this.program, name);
|
|
667
|
+
return {
|
|
668
|
+
position: gl.getAttribLocation(this.program, 'a_position'),
|
|
669
|
+
resolution: uniform('u_resolution'),
|
|
670
|
+
time: uniform('u_time'),
|
|
671
|
+
seed: uniform('u_seed'),
|
|
672
|
+
motion: uniform('u_motion'),
|
|
673
|
+
pointer: uniform('u_pointer'),
|
|
674
|
+
colorA: uniform('u_colorA'),
|
|
675
|
+
colorB: uniform('u_colorB'),
|
|
676
|
+
colorC: uniform('u_colorC'),
|
|
677
|
+
colorD: uniform('u_colorD')
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#setupGeometry() {
|
|
682
|
+
const gl = this.gl;
|
|
683
|
+
const vertices = new Float32Array([-1, -1, 3, -1, -1, 3]);
|
|
684
|
+
this.buffer = gl.createBuffer();
|
|
685
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
686
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
#bindEvents() {
|
|
690
|
+
if (this.options.mouseColor === false) return;
|
|
691
|
+
this.eventTarget = this.options.eventTarget || this.canvas.parentElement || this.canvas;
|
|
692
|
+
this.onPointerMove = (event) => {
|
|
693
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
694
|
+
this.pointerTarget[0] = (event.clientX - rect.left) / Math.max(rect.width, 1);
|
|
695
|
+
this.pointerTarget[1] = 1 - (event.clientY - rect.top) / Math.max(rect.height, 1);
|
|
696
|
+
this.motionTarget = 1;
|
|
697
|
+
};
|
|
698
|
+
this.onPointerLeave = () => { this.motionTarget = 0; };
|
|
699
|
+
this.eventTarget.addEventListener('pointermove', this.onPointerMove, { passive: true });
|
|
700
|
+
this.eventTarget.addEventListener('pointerdown', this.onPointerMove, { passive: true });
|
|
701
|
+
this.eventTarget.addEventListener('pointerleave', this.onPointerLeave, { passive: true });
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
#bindContextEvents() {
|
|
705
|
+
this.onContextLost = (event) => {
|
|
706
|
+
event.preventDefault();
|
|
707
|
+
if (this.disposed) return;
|
|
708
|
+
this.visible = false;
|
|
709
|
+
if (typeof this.options.onContextLost === 'function') this.options.onContextLost(event);
|
|
710
|
+
};
|
|
711
|
+
this.onContextRestored = () => {
|
|
712
|
+
if (this.disposed) return;
|
|
713
|
+
this.program = createProgram$1(this.gl);
|
|
714
|
+
this.locations = this.#getLocations();
|
|
715
|
+
this.#setupGeometry();
|
|
716
|
+
this.visible = true;
|
|
717
|
+
this.resize();
|
|
718
|
+
if (typeof this.options.onContextRestored === 'function') this.options.onContextRestored();
|
|
719
|
+
};
|
|
720
|
+
this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
|
|
721
|
+
this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
setPreset(preset) {
|
|
725
|
+
this.preset = { ...preset };
|
|
726
|
+
this.colors = preset.colors.map(hexToRgb01$1);
|
|
727
|
+
this.timeOffset = preset.seed * 0.73;
|
|
728
|
+
}
|
|
729
|
+
|
|
687
730
|
setDprCap(cap) {
|
|
688
731
|
this.options.dprCap = cap;
|
|
689
732
|
this.resize();
|
|
@@ -709,123 +752,128 @@ class CosmicRenderer {
|
|
|
709
752
|
}
|
|
710
753
|
|
|
711
754
|
randomize() {
|
|
712
|
-
this.preset.seed = Math.random() * 100;
|
|
713
|
-
this.timeOffset = Math.random() * 40;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
resize() {
|
|
717
|
-
const dpr = Math.min(window.devicePixelRatio || 1, this.options.dprCap);
|
|
718
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
719
|
-
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
720
|
-
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
721
|
-
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
722
|
-
this.canvas.width = width;
|
|
723
|
-
this.canvas.height = height;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
draw(elapsedSeconds, paused = false) {
|
|
729
|
-
if (this.disposed || !this.visible) return;
|
|
730
|
-
this.
|
|
731
|
-
|
|
732
|
-
this.pointer[
|
|
733
|
-
this.
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
gl.
|
|
737
|
-
gl.
|
|
738
|
-
gl.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
gl.
|
|
743
|
-
gl.uniform1f(this.locations.
|
|
744
|
-
gl.
|
|
745
|
-
gl.
|
|
746
|
-
gl.
|
|
747
|
-
gl.uniform3fv(this.locations.
|
|
748
|
-
gl.uniform3fv(this.locations.
|
|
749
|
-
gl.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
this.
|
|
756
|
-
|
|
757
|
-
target.removeEventListener('
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
this.gl.deleteBuffer(this.buffer);
|
|
761
|
-
this.gl.deleteProgram(this.program);
|
|
762
|
-
const lose = this.gl.getExtension('WEBGL_lose_context');
|
|
763
|
-
if (lose) lose.loseContext();
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function rgb(color, alpha = 1) {
|
|
768
|
-
const [r, g, b] = hexToRgb01$1(color).map((value) => Math.round(value * 255));
|
|
769
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
class FallbackRenderer {
|
|
773
|
-
constructor(canvas, preset) {
|
|
755
|
+
this.preset.seed = Math.random() * 100;
|
|
756
|
+
this.timeOffset = Math.random() * 40;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
resize() {
|
|
760
|
+
const dpr = Math.min(window.devicePixelRatio || 1, this.options.dprCap);
|
|
761
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
762
|
+
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
763
|
+
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
764
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
765
|
+
this.canvas.width = width;
|
|
766
|
+
this.canvas.height = height;
|
|
767
|
+
}
|
|
768
|
+
this.gl.viewport(0, 0, width, height);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
draw(elapsedSeconds, paused = false) {
|
|
772
|
+
if (this.disposed || !this.visible) return;
|
|
773
|
+
const gl = this.gl;
|
|
774
|
+
this.pointer[0] += (this.pointerTarget[0] - this.pointer[0]) * 0.08;
|
|
775
|
+
this.pointer[1] += (this.pointerTarget[1] - this.pointer[1]) * 0.08;
|
|
776
|
+
this.motion += (this.motionTarget - this.motion) * 0.07;
|
|
777
|
+
|
|
778
|
+
gl.useProgram(this.program);
|
|
779
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
780
|
+
gl.enableVertexAttribArray(this.locations.position);
|
|
781
|
+
gl.vertexAttribPointer(this.locations.position, 2, gl.FLOAT, false, 0, 0);
|
|
782
|
+
|
|
783
|
+
gl.uniform2f(this.locations.resolution, this.canvas.width, this.canvas.height);
|
|
784
|
+
gl.uniform1f(this.locations.time, this.timeOffset + (paused ? 0 : elapsedSeconds * this.preset.speed));
|
|
785
|
+
gl.uniform1f(this.locations.seed, this.preset.seed);
|
|
786
|
+
gl.uniform1f(this.locations.motion, this.motion);
|
|
787
|
+
gl.uniform2f(this.locations.pointer, this.pointer[0], this.pointer[1]);
|
|
788
|
+
gl.uniform3fv(this.locations.colorA, this.colors[0]);
|
|
789
|
+
gl.uniform3fv(this.locations.colorB, this.colors[1]);
|
|
790
|
+
gl.uniform3fv(this.locations.colorC, this.colors[2]);
|
|
791
|
+
gl.uniform3fv(this.locations.colorD, this.colors[3]);
|
|
792
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
dispose() {
|
|
796
|
+
this.disposed = true;
|
|
797
|
+
const target = this.eventTarget || this.canvas;
|
|
798
|
+
target.removeEventListener('pointermove', this.onPointerMove);
|
|
799
|
+
target.removeEventListener('pointerdown', this.onPointerMove);
|
|
800
|
+
target.removeEventListener('pointerleave', this.onPointerLeave);
|
|
801
|
+
this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
|
|
802
|
+
this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
803
|
+
this.gl.deleteBuffer(this.buffer);
|
|
804
|
+
this.gl.deleteProgram(this.program);
|
|
805
|
+
const lose = this.gl.getExtension('WEBGL_lose_context');
|
|
806
|
+
if (lose) lose.loseContext();
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function rgb(color, alpha = 1) {
|
|
811
|
+
const [r, g, b] = hexToRgb01$1(color).map((value) => Math.round(value * 255));
|
|
812
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
class FallbackRenderer {
|
|
816
|
+
constructor(canvas, preset, options = {}) {
|
|
774
817
|
this.canvas = canvas;
|
|
775
818
|
this.preset = preset;
|
|
776
819
|
this.context = canvas.getContext('2d');
|
|
777
820
|
this.visible = true;
|
|
778
821
|
this.timePhase = 0;
|
|
822
|
+
this.dprCap = options.dprCap || 1.5;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
resize() {
|
|
826
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
827
|
+
const dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
|
|
828
|
+
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
829
|
+
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
830
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
831
|
+
this.canvas.width = width;
|
|
832
|
+
this.canvas.height = height;
|
|
833
|
+
}
|
|
779
834
|
}
|
|
780
|
-
|
|
781
|
-
resize() {
|
|
782
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
783
|
-
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
784
|
-
const width = Math.max(2, Math.round(rect.width * dpr));
|
|
785
|
-
const height = Math.max(2, Math.round(rect.height * dpr));
|
|
786
|
-
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
787
|
-
this.canvas.width = width;
|
|
788
|
-
this.canvas.height = height;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
835
|
+
|
|
792
836
|
drawNebula(time) {
|
|
793
837
|
const ctx = this.context;
|
|
794
838
|
const { width, height } = this.canvas;
|
|
795
839
|
const t = (time + this.timePhase) * (this.preset.speed || 1);
|
|
796
840
|
const phase = (this.preset.seed || 0) * 0.7;
|
|
797
841
|
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
|
798
|
-
gradient.addColorStop(0, this.preset.colors[0]);
|
|
799
|
-
gradient.addColorStop(0.38, this.preset.colors[1]);
|
|
800
|
-
gradient.addColorStop(0.72, this.preset.colors[2]);
|
|
801
|
-
gradient.addColorStop(1, this.preset.colors[3]);
|
|
802
|
-
ctx.fillStyle = gradient;
|
|
803
|
-
ctx.fillRect(0, 0, width, height);
|
|
804
|
-
|
|
842
|
+
gradient.addColorStop(0, this.preset.colors[0]);
|
|
843
|
+
gradient.addColorStop(0.38, this.preset.colors[1]);
|
|
844
|
+
gradient.addColorStop(0.72, this.preset.colors[2]);
|
|
845
|
+
gradient.addColorStop(1, this.preset.colors[3]);
|
|
846
|
+
ctx.fillStyle = gradient;
|
|
847
|
+
ctx.fillRect(0, 0, width, height);
|
|
848
|
+
|
|
805
849
|
ctx.globalCompositeOperation = 'screen';
|
|
806
850
|
for (let index = 0; index < 6; index += 1) {
|
|
807
851
|
const x = (0.5 + 0.45 * Math.sin(t * 0.32 + index * 1.7 + phase)) * width;
|
|
808
852
|
const y = (0.5 + 0.4 * Math.cos(t * 0.25 + index + phase)) * height;
|
|
809
|
-
const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
|
|
810
|
-
const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
811
|
-
glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
|
|
812
|
-
glow.addColorStop(1, rgb(this.preset.colors[index % 4], 0));
|
|
813
|
-
ctx.fillStyle = glow;
|
|
814
|
-
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
815
|
-
}
|
|
816
|
-
ctx.globalCompositeOperation = 'source-over';
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
draw(time) {
|
|
820
|
-
if (!this.visible) return;
|
|
821
|
-
this.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
853
|
+
const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
|
|
854
|
+
const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
855
|
+
glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
|
|
856
|
+
glow.addColorStop(1, rgb(this.preset.colors[index % 4], 0));
|
|
857
|
+
ctx.fillStyle = glow;
|
|
858
|
+
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
859
|
+
}
|
|
860
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
draw(time) {
|
|
864
|
+
if (!this.visible) return;
|
|
865
|
+
this.drawNebula(time);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
setPreset(preset) {
|
|
869
|
+
this.preset = preset;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
setDprCap(cap) {
|
|
873
|
+
this.dprCap = cap;
|
|
874
|
+
this.resize();
|
|
875
|
+
}
|
|
876
|
+
|
|
829
877
|
randomize() {
|
|
830
878
|
if (this.preset) {
|
|
831
879
|
this.preset.seed = Math.random() * 100;
|
|
@@ -836,110 +884,129 @@ class FallbackRenderer {
|
|
|
836
884
|
dispose() {}
|
|
837
885
|
}
|
|
838
886
|
|
|
839
|
-
/**
|
|
840
|
-
* Document-level shared rAF scheduler. Every component instance subscribes
|
|
841
|
-
* its own frame callback; the whole page runs ONE animation loop (like the
|
|
842
|
-
* original demo), which avoids jank from many competing rAF loops.
|
|
843
|
-
*/
|
|
844
|
-
const subscribers = [];
|
|
845
|
-
let running = false;
|
|
846
|
-
let globallyPaused = false;
|
|
847
|
-
let rafId = 0;
|
|
848
|
-
let last = 0;
|
|
849
|
-
|
|
850
|
-
function tick(now) {
|
|
851
|
-
if (!running) return;
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
887
|
+
/**
|
|
888
|
+
* Document-level shared rAF scheduler. Every component instance subscribes
|
|
889
|
+
* its own frame callback; the whole page runs ONE animation loop (like the
|
|
890
|
+
* original demo), which avoids jank from many competing rAF loops.
|
|
891
|
+
*/
|
|
892
|
+
const subscribers = [];
|
|
893
|
+
let running = false;
|
|
894
|
+
let globallyPaused = false;
|
|
895
|
+
let rafId = 0;
|
|
896
|
+
let last = 0;
|
|
897
|
+
|
|
898
|
+
function tick(now) {
|
|
899
|
+
if (!running) return;
|
|
900
|
+
const activeItems = [];
|
|
901
|
+
if (!globallyPaused) {
|
|
902
|
+
for (const item of subscribers.slice()) {
|
|
903
|
+
try {
|
|
904
|
+
if (!item.isPaused()) activeItems.push(item);
|
|
905
|
+
} catch {}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (activeItems.length === 0) {
|
|
909
|
+
running = false;
|
|
910
|
+
rafId = 0;
|
|
911
|
+
last = 0;
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
|
|
915
|
+
last = now;
|
|
916
|
+
// Schedule the next frame BEFORE running callbacks so one throwing
|
|
917
|
+
// subscriber can never kill the whole animation loop.
|
|
918
|
+
rafId = requestAnimationFrame(tick);
|
|
919
|
+
for (const item of activeItems) {
|
|
920
|
+
try {
|
|
921
|
+
item.onFrame(delta, now);
|
|
922
|
+
} catch (error) {
|
|
862
923
|
console.warn('[dlc-ui] frame error:', error);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (subscribers.length === 0) {
|
|
927
|
+
cancelAnimationFrame(rafId);
|
|
928
|
+
running = false;
|
|
929
|
+
rafId = 0;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function start() {
|
|
934
|
+
if (running) return;
|
|
935
|
+
running = true;
|
|
936
|
+
last = 0;
|
|
937
|
+
rafId = requestAnimationFrame(tick);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function pauseAll() {
|
|
941
|
+
globallyPaused = true;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function resumeAll() {
|
|
945
|
+
globallyPaused = false;
|
|
946
|
+
start();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function wakeScheduler() {
|
|
950
|
+
start();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function subscribeScheduler(onFrame, isPaused) {
|
|
954
|
+
const item = { onFrame, isPaused };
|
|
955
|
+
subscribers.push(item);
|
|
956
|
+
start();
|
|
957
|
+
return () => {
|
|
958
|
+
const index = subscribers.indexOf(item);
|
|
959
|
+
if (index !== -1) subscribers.splice(index, 1);
|
|
960
|
+
if (subscribers.length === 0 && rafId) {
|
|
961
|
+
cancelAnimationFrame(rafId);
|
|
962
|
+
running = false;
|
|
963
|
+
rafId = 0;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Gates drawing on "element intersects viewport AND the page tab is visible".
|
|
970
|
+
* Falls back to always-visible when IntersectionObserver is unavailable.
|
|
971
|
+
*/
|
|
972
|
+
function createVisibilityGuard(element, onChange = null) {
|
|
973
|
+
let intersecting = true;
|
|
974
|
+
let pageVisible = typeof document === 'undefined' || !document.hidden;
|
|
975
|
+
let disposed = false;
|
|
976
|
+
let observer = null;
|
|
977
|
+
|
|
978
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
979
|
+
observer = new IntersectionObserver(
|
|
980
|
+
(entries) => {
|
|
981
|
+
intersecting = entries.some((entry) => entry.isIntersecting);
|
|
982
|
+
if (typeof onChange === 'function') onChange();
|
|
983
|
+
},
|
|
984
|
+
{ rootMargin: '180px' }
|
|
985
|
+
);
|
|
986
|
+
observer.observe(element);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const onVisibilityChange = () => {
|
|
990
|
+
pageVisible = typeof document !== 'undefined' && !document.hidden;
|
|
991
|
+
if (typeof onChange === 'function') onChange();
|
|
992
|
+
};
|
|
993
|
+
if (typeof document !== 'undefined') {
|
|
994
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return {
|
|
998
|
+
isVisible() {
|
|
999
|
+
return intersecting && pageVisible;
|
|
1000
|
+
},
|
|
1001
|
+
dispose() {
|
|
1002
|
+
if (disposed) return;
|
|
1003
|
+
disposed = true;
|
|
1004
|
+
if (observer) observer.disconnect();
|
|
1005
|
+
if (typeof document !== 'undefined') {
|
|
1006
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
943
1010
|
}
|
|
944
1011
|
|
|
945
1012
|
/**
|
|
@@ -958,30 +1025,96 @@ function nextTick(fn) {
|
|
|
958
1025
|
}
|
|
959
1026
|
}
|
|
960
1027
|
|
|
961
|
-
function
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1028
|
+
function normalizeFps(value, fallback = 60) {
|
|
1029
|
+
const fps = Number(value);
|
|
1030
|
+
if (!Number.isFinite(fps)) return fallback;
|
|
1031
|
+
return Math.min(60, Math.max(1, fps));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function createFrameGate(initialFps = 60) {
|
|
1035
|
+
let fps = normalizeFps(initialFps);
|
|
1036
|
+
let elapsed = 0;
|
|
1037
|
+
return {
|
|
1038
|
+
shouldDraw(delta) {
|
|
1039
|
+
elapsed += delta;
|
|
1040
|
+
const interval = 1 / fps;
|
|
1041
|
+
if (elapsed + 0.0001 < interval) return false;
|
|
1042
|
+
elapsed %= interval;
|
|
1043
|
+
return true;
|
|
1044
|
+
},
|
|
1045
|
+
setFps(value) {
|
|
1046
|
+
fps = normalizeFps(value, fps);
|
|
1047
|
+
elapsed = 0;
|
|
1048
|
+
return fps;
|
|
1049
|
+
},
|
|
1050
|
+
getFps() {
|
|
1051
|
+
return fps;
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function createReducedMotionPreference(enabled, onChange) {
|
|
1057
|
+
const query = enabled && typeof matchMedia !== 'undefined'
|
|
1058
|
+
? matchMedia('(prefers-reduced-motion: reduce)')
|
|
1059
|
+
: null;
|
|
1060
|
+
const notify = () => {
|
|
1061
|
+
if (typeof onChange === 'function') onChange(Boolean(query && query.matches));
|
|
1062
|
+
};
|
|
1063
|
+
if (query) {
|
|
1064
|
+
if (typeof query.addEventListener === 'function') query.addEventListener('change', notify);
|
|
1065
|
+
else if (typeof query.addListener === 'function') query.addListener(notify);
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
matches() {
|
|
1069
|
+
return Boolean(query && query.matches);
|
|
1070
|
+
},
|
|
1071
|
+
dispose() {
|
|
1072
|
+
if (!query) return;
|
|
1073
|
+
if (typeof query.removeEventListener === 'function') query.removeEventListener('change', notify);
|
|
1074
|
+
else if (typeof query.removeListener === 'function') query.removeListener(notify);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function toFiniteNumber(value) {
|
|
1080
|
+
if (value == null || typeof value === 'boolean') return null;
|
|
1081
|
+
if (typeof value === 'string' && value.trim() === '') return null;
|
|
1082
|
+
const number = Number(value);
|
|
1083
|
+
return Number.isFinite(number) ? number : null;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
966
1087
|
* Mount a cosmic (nebula) capsule into `container`.
|
|
967
1088
|
*
|
|
968
1089
|
* Options: preset (literary name), width, height (number=px or string with
|
|
969
1090
|
* px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
|
|
970
1091
|
* width in percent), text (HTML string or DOM nodes for the text slot),
|
|
971
1092
|
* colorContent (HTML string or DOM nodes for the color slot), quality,
|
|
972
|
-
* renderer,
|
|
1093
|
+
* renderer, quality, renderScale, powerPreference, fps, paused/static,
|
|
1094
|
+
* respectReducedMotion, mouseColor, cssVars.
|
|
973
1095
|
*/
|
|
974
1096
|
function createCapsule(container, options = {}) {
|
|
975
|
-
if (!container || typeof container.appendChild !== 'function') {
|
|
976
|
-
throw new Error('createCapsule: container element is required');
|
|
977
|
-
}
|
|
978
|
-
|
|
1097
|
+
if (!container || typeof container.appendChild !== 'function') {
|
|
1098
|
+
throw new Error('createCapsule: container element is required');
|
|
1099
|
+
}
|
|
1100
|
+
|
|
979
1101
|
const preset = { ...getPreset('capsule', options.preset ?? '初光') };
|
|
980
1102
|
const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
|
|
981
|
-
const normalizedColors = merged.colors.map(normalizeColor);
|
|
982
|
-
if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
983
|
-
|
|
984
|
-
|
|
1103
|
+
const normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
|
|
1104
|
+
if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
1105
|
+
const initialSeed = toFiniteNumber(merged.seed);
|
|
1106
|
+
const initialSpeed = toFiniteNumber(merged.speed);
|
|
1107
|
+
if (initialSeed !== null) preset.seed = initialSeed;
|
|
1108
|
+
if (initialSpeed !== null) preset.speed = initialSpeed;
|
|
1109
|
+
merged.colors = [...preset.colors];
|
|
1110
|
+
merged.seed = preset.seed;
|
|
1111
|
+
merged.speed = preset.speed;
|
|
1112
|
+
const optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
|
|
1113
|
+
let colorOverride = optionColors.length === 4 && optionColors.every(Boolean)
|
|
1114
|
+
? [...optionColors]
|
|
1115
|
+
: null;
|
|
1116
|
+
let seedOverride = options.seed !== undefined ? toFiniteNumber(options.seed) : null;
|
|
1117
|
+
let speedOverride = options.speed !== undefined ? toFiniteNumber(options.speed) : null;
|
|
985
1118
|
let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
|
|
986
1119
|
|
|
987
1120
|
const root = document.createElement('div');
|
|
@@ -1029,35 +1162,67 @@ function createCapsule(container, options = {}) {
|
|
|
1029
1162
|
root.appendChild(visualLayer);
|
|
1030
1163
|
root.appendChild(canvas);
|
|
1031
1164
|
container.appendChild(root);
|
|
1032
|
-
|
|
1165
|
+
|
|
1033
1166
|
const emitter = createEmitter();
|
|
1034
|
-
let
|
|
1167
|
+
let manuallyPaused = merged.paused === true;
|
|
1168
|
+
let reducedPaused = false;
|
|
1169
|
+
let staticMode = merged.static === true;
|
|
1170
|
+
let contextLost = false;
|
|
1035
1171
|
let disposed = false;
|
|
1172
|
+
let renderOnce = () => {};
|
|
1036
1173
|
const dirty = {
|
|
1037
1174
|
preset: false,
|
|
1038
1175
|
seed: false,
|
|
1039
1176
|
speed: false,
|
|
1040
1177
|
colors: false,
|
|
1041
1178
|
textRatio: false,
|
|
1042
|
-
cssVars: false
|
|
1179
|
+
cssVars: false,
|
|
1180
|
+
mouseColor: false,
|
|
1181
|
+
quality: false,
|
|
1182
|
+
renderScale: false,
|
|
1183
|
+
paused: false,
|
|
1184
|
+
static: false,
|
|
1185
|
+
fps: false
|
|
1043
1186
|
};
|
|
1044
|
-
|
|
1045
|
-
let renderer;
|
|
1046
|
-
const useWebgl = merged.renderer !== 'canvas2d';
|
|
1047
|
-
if (useWebgl) {
|
|
1048
|
-
try {
|
|
1049
|
-
renderer = new CosmicRenderer(canvas, merged, {
|
|
1050
|
-
|
|
1051
|
-
|
|
1187
|
+
|
|
1188
|
+
let renderer;
|
|
1189
|
+
const useWebgl = merged.renderer !== 'canvas2d';
|
|
1190
|
+
if (useWebgl) {
|
|
1191
|
+
try {
|
|
1192
|
+
renderer = new CosmicRenderer(canvas, merged, {
|
|
1193
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
1194
|
+
mouseColor: merged.mouseColor !== false,
|
|
1195
|
+
powerPreference: merged.powerPreference,
|
|
1196
|
+
onContextLost: () => {
|
|
1197
|
+
contextLost = true;
|
|
1198
|
+
emitter.emit('contextlost', {});
|
|
1199
|
+
},
|
|
1200
|
+
onContextRestored: () => {
|
|
1201
|
+
contextLost = false;
|
|
1202
|
+
emitter.emit('contextrestored', {});
|
|
1203
|
+
renderOnce();
|
|
1204
|
+
wakeScheduler();
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
renderer = new FallbackRenderer(canvas, merged, {
|
|
1209
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale)
|
|
1210
|
+
});
|
|
1052
1211
|
nextTick(() => {
|
|
1053
1212
|
if (disposed) return;
|
|
1054
1213
|
emitter.emit('error', { message: String(error && error.message ? error.message : error) });
|
|
1055
1214
|
});
|
|
1056
|
-
}
|
|
1057
|
-
} else {
|
|
1058
|
-
renderer = new FallbackRenderer(canvas, merged
|
|
1059
|
-
|
|
1060
|
-
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
renderer = new FallbackRenderer(canvas, merged, {
|
|
1218
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale)
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
let animationTime = 0;
|
|
1223
|
+
const frameGate = createFrameGate(merged.fps);
|
|
1224
|
+
renderOnce = () => renderer.draw(animationTime);
|
|
1225
|
+
|
|
1061
1226
|
const applySize = () => {
|
|
1062
1227
|
root.style.width = parseSize(merged.width);
|
|
1063
1228
|
root.style.height = parseSize(merged.height);
|
|
@@ -1071,39 +1236,53 @@ function createCapsule(container, options = {}) {
|
|
|
1071
1236
|
}
|
|
1072
1237
|
renderer.resize();
|
|
1073
1238
|
};
|
|
1074
|
-
applySize();
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1239
|
+
applySize();
|
|
1240
|
+
|
|
1241
|
+
const resize = () => {
|
|
1242
|
+
renderer.resize();
|
|
1243
|
+
if (manuallyPaused || reducedPaused || staticMode) renderOnce();
|
|
1244
|
+
};
|
|
1245
|
+
const resizeObserver = typeof ResizeObserver !== 'undefined'
|
|
1246
|
+
? new ResizeObserver(resize)
|
|
1247
|
+
: null;
|
|
1248
|
+
if (resizeObserver) resizeObserver.observe(root);
|
|
1249
|
+
else window.addEventListener('resize', resize);
|
|
1250
|
+
|
|
1251
|
+
const visibility = createVisibilityGuard(root, wakeScheduler);
|
|
1252
|
+
const motionPreference = createReducedMotionPreference(
|
|
1253
|
+
merged.respectReducedMotion,
|
|
1254
|
+
(matches) => {
|
|
1255
|
+
reducedPaused = matches;
|
|
1256
|
+
if (matches) renderOnce();
|
|
1257
|
+
else wakeScheduler();
|
|
1258
|
+
}
|
|
1259
|
+
);
|
|
1260
|
+
reducedPaused = motionPreference.matches();
|
|
1261
|
+
const isMotionPaused = () => manuallyPaused || reducedPaused || staticMode || contextLost;
|
|
1262
|
+
|
|
1084
1263
|
root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
|
|
1085
1264
|
root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
|
|
1086
|
-
root.addEventListener('click', (event) => {
|
|
1087
|
-
emitter.emit('click', { event, preset: { ...preset } });
|
|
1088
|
-
});
|
|
1089
|
-
root.addEventListener('pointerdown', (event) => emitter.emit('pointerdown', { event, preset: { ...preset } }));
|
|
1090
|
-
root.addEventListener('pointerup', (event) => emitter.emit('pointerup', { event, preset: { ...preset } }));
|
|
1091
|
-
root.addEventListener('dblclick', (event) => emitter.emit('dblclick', { event, preset: { ...preset } }));
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const unsubscribe = subscribeScheduler(
|
|
1095
|
-
(delta) => {
|
|
1096
|
-
animationTime += delta;
|
|
1097
|
-
if (
|
|
1098
|
-
},
|
|
1099
|
-
() =>
|
|
1100
|
-
);
|
|
1101
|
-
|
|
1265
|
+
root.addEventListener('click', (event) => {
|
|
1266
|
+
emitter.emit('click', { event, preset: { ...preset } });
|
|
1267
|
+
});
|
|
1268
|
+
root.addEventListener('pointerdown', (event) => emitter.emit('pointerdown', { event, preset: { ...preset } }));
|
|
1269
|
+
root.addEventListener('pointerup', (event) => emitter.emit('pointerup', { event, preset: { ...preset } }));
|
|
1270
|
+
root.addEventListener('dblclick', (event) => emitter.emit('dblclick', { event, preset: { ...preset } }));
|
|
1271
|
+
|
|
1272
|
+
renderOnce();
|
|
1273
|
+
const unsubscribe = subscribeScheduler(
|
|
1274
|
+
(delta) => {
|
|
1275
|
+
animationTime += delta;
|
|
1276
|
+
if (frameGate.shouldDraw(delta)) renderer.draw(animationTime);
|
|
1277
|
+
},
|
|
1278
|
+
() => isMotionPaused() || !visibility.isVisible()
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1102
1281
|
nextTick(() => {
|
|
1103
1282
|
if (disposed) return;
|
|
1104
1283
|
emitter.emit('ready', { preset: { ...preset } });
|
|
1105
1284
|
});
|
|
1106
|
-
|
|
1285
|
+
|
|
1107
1286
|
const syncTheme = () => {
|
|
1108
1287
|
root.dataset.mode = 'nebula';
|
|
1109
1288
|
root.dataset.theme = preset.theme || 'light';
|
|
@@ -1111,45 +1290,55 @@ function createCapsule(container, options = {}) {
|
|
|
1111
1290
|
};
|
|
1112
1291
|
|
|
1113
1292
|
return {
|
|
1114
|
-
element: root,
|
|
1115
|
-
canvas,
|
|
1116
|
-
preset,
|
|
1117
|
-
on: emitter.on,
|
|
1118
|
-
off: emitter.off,
|
|
1293
|
+
element: root,
|
|
1294
|
+
canvas,
|
|
1295
|
+
preset,
|
|
1296
|
+
on: emitter.on,
|
|
1297
|
+
off: emitter.off,
|
|
1119
1298
|
setPreset(ref) {
|
|
1120
1299
|
const next = getPreset('capsule', ref);
|
|
1121
1300
|
dirty.preset = true;
|
|
1122
1301
|
Object.assign(preset, next);
|
|
1302
|
+
if (colorOverride) preset.colors = [...colorOverride];
|
|
1303
|
+
if (seedOverride !== null) preset.seed = seedOverride;
|
|
1304
|
+
if (speedOverride !== null) preset.speed = speedOverride;
|
|
1123
1305
|
const nextColors = preset.colors.map(normalizeColor);
|
|
1124
1306
|
if (nextColors.every(Boolean)) preset.colors = nextColors;
|
|
1125
1307
|
renderer.setPreset({ ...preset });
|
|
1126
1308
|
syncTheme();
|
|
1127
1309
|
renderer.resize();
|
|
1310
|
+
if (isMotionPaused()) renderOnce();
|
|
1128
1311
|
emitter.emit('presetchange', { preset: { ...next } });
|
|
1129
1312
|
return this;
|
|
1130
|
-
},
|
|
1313
|
+
},
|
|
1131
1314
|
setColors(colors) {
|
|
1132
1315
|
const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
|
|
1133
1316
|
if (next.length !== 4 || next.some((color) => !color)) return this;
|
|
1134
1317
|
dirty.colors = true;
|
|
1318
|
+
colorOverride = [...next];
|
|
1135
1319
|
preset.colors = next;
|
|
1136
1320
|
renderer.setPreset({ ...preset, colors: next });
|
|
1321
|
+
if (isMotionPaused()) renderOnce();
|
|
1137
1322
|
return this;
|
|
1138
1323
|
},
|
|
1139
1324
|
setSeed(seed) {
|
|
1140
|
-
const value =
|
|
1141
|
-
if (
|
|
1325
|
+
const value = toFiniteNumber(seed);
|
|
1326
|
+
if (value === null) return this;
|
|
1142
1327
|
dirty.seed = true;
|
|
1328
|
+
seedOverride = value;
|
|
1143
1329
|
preset.seed = value;
|
|
1144
1330
|
renderer.setPreset({ ...preset });
|
|
1331
|
+
if (isMotionPaused()) renderOnce();
|
|
1145
1332
|
return this;
|
|
1146
1333
|
},
|
|
1147
1334
|
setSpeed(speed) {
|
|
1148
|
-
const value =
|
|
1149
|
-
if (
|
|
1335
|
+
const value = toFiniteNumber(speed);
|
|
1336
|
+
if (value === null) return this;
|
|
1150
1337
|
dirty.speed = true;
|
|
1338
|
+
speedOverride = value;
|
|
1151
1339
|
preset.speed = value;
|
|
1152
1340
|
renderer.setPreset({ ...preset });
|
|
1341
|
+
if (isMotionPaused()) renderOnce();
|
|
1153
1342
|
return this;
|
|
1154
1343
|
},
|
|
1155
1344
|
setText(content) {
|
|
@@ -1182,6 +1371,7 @@ function createCapsule(container, options = {}) {
|
|
|
1182
1371
|
return this;
|
|
1183
1372
|
},
|
|
1184
1373
|
setMouseColor(value) {
|
|
1374
|
+
dirty.mouseColor = true;
|
|
1185
1375
|
merged.mouseColor = value !== false;
|
|
1186
1376
|
if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
|
|
1187
1377
|
return this;
|
|
@@ -1196,1113 +1386,1331 @@ function createCapsule(container, options = {}) {
|
|
|
1196
1386
|
merged.height = height;
|
|
1197
1387
|
}
|
|
1198
1388
|
applySize();
|
|
1389
|
+
if (isMotionPaused()) renderOnce();
|
|
1199
1390
|
return this;
|
|
1200
1391
|
},
|
|
1201
1392
|
randomize() {
|
|
1202
1393
|
this.setSeed(Math.random() * 100);
|
|
1203
1394
|
return this;
|
|
1204
1395
|
},
|
|
1205
|
-
pause() {
|
|
1206
|
-
paused = true;
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1396
|
+
pause() {
|
|
1397
|
+
dirty.paused = true;
|
|
1398
|
+
manuallyPaused = true;
|
|
1399
|
+
renderOnce();
|
|
1400
|
+
return this;
|
|
1401
|
+
},
|
|
1402
|
+
resume() {
|
|
1403
|
+
dirty.paused = true;
|
|
1404
|
+
manuallyPaused = false;
|
|
1405
|
+
wakeScheduler();
|
|
1406
|
+
return this;
|
|
1407
|
+
},
|
|
1408
|
+
setPaused(value) {
|
|
1409
|
+
return value ? this.pause() : this.resume();
|
|
1410
|
+
},
|
|
1411
|
+
setStatic(value) {
|
|
1412
|
+
dirty.static = true;
|
|
1413
|
+
staticMode = value === true;
|
|
1414
|
+
merged.static = staticMode;
|
|
1415
|
+
if (staticMode) renderOnce();
|
|
1416
|
+
else wakeScheduler();
|
|
1417
|
+
return this;
|
|
1418
|
+
},
|
|
1419
|
+
setFps(fps) {
|
|
1420
|
+
dirty.fps = true;
|
|
1421
|
+
merged.fps = frameGate.setFps(fps);
|
|
1422
|
+
wakeScheduler();
|
|
1423
|
+
return this;
|
|
1424
|
+
},
|
|
1425
|
+
setQuality(quality) {
|
|
1426
|
+
dirty.quality = true;
|
|
1427
|
+
merged.quality = quality;
|
|
1428
|
+
if (typeof renderer.setDprCap === 'function') {
|
|
1429
|
+
renderer.setDprCap(effectiveDprCap(quality, merged.renderScale));
|
|
1430
|
+
}
|
|
1431
|
+
if (isMotionPaused()) renderOnce();
|
|
1432
|
+
return this;
|
|
1433
|
+
},
|
|
1434
|
+
setRenderScale(renderScale) {
|
|
1435
|
+
const value = Number(renderScale);
|
|
1436
|
+
if (!Number.isFinite(value)) return this;
|
|
1437
|
+
dirty.renderScale = true;
|
|
1438
|
+
merged.renderScale = Math.min(1, Math.max(0.25, value));
|
|
1439
|
+
if (typeof renderer.setDprCap === 'function') {
|
|
1440
|
+
renderer.setDprCap(effectiveDprCap(merged.quality, merged.renderScale));
|
|
1441
|
+
}
|
|
1442
|
+
if (isMotionPaused()) renderOnce();
|
|
1443
|
+
return this;
|
|
1444
|
+
},
|
|
1218
1445
|
dispose() {
|
|
1219
1446
|
disposed = true;
|
|
1220
1447
|
unsubscribe();
|
|
1221
|
-
visibility.dispose();
|
|
1222
|
-
|
|
1223
|
-
|
|
1448
|
+
visibility.dispose();
|
|
1449
|
+
motionPreference.dispose();
|
|
1450
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
1451
|
+
else window.removeEventListener('resize', resize);
|
|
1224
1452
|
renderer.dispose();
|
|
1225
1453
|
root.remove();
|
|
1226
1454
|
},
|
|
1227
1455
|
get textRatio() { return merged.textRatio; },
|
|
1228
1456
|
get cssVars() { return merged.cssVars; },
|
|
1457
|
+
get mouseColor() { return merged.mouseColor; },
|
|
1458
|
+
get quality() { return merged.quality; },
|
|
1459
|
+
get renderScale() { return merged.renderScale; },
|
|
1460
|
+
get paused() { return manuallyPaused; },
|
|
1461
|
+
get static() { return staticMode; },
|
|
1462
|
+
get fps() { return frameGate.getFps(); },
|
|
1229
1463
|
dirty
|
|
1230
1464
|
};
|
|
1231
1465
|
}
|
|
1232
1466
|
|
|
1233
|
-
const PROGRESS_MOTION_WIDTH = 240;
|
|
1234
|
-
const PROGRESS_MOTION_HEIGHT = 80;
|
|
1235
|
-
const PROGRESS_MOTION_DURATION = 12.0;
|
|
1236
|
-
const PROGRESS_MOTION_MAX_PX = 40.0;
|
|
1237
|
-
|
|
1238
|
-
const PROFILE_CONFIG = {
|
|
1239
|
-
'model-training': { seed: 0.37, broad: 0.58, middle: 0.25, detail: 0.13, lobe: 0.24 },
|
|
1240
|
-
'agent-migration': { seed: 1.71, broad: 0.72, middle: 0.10, detail: 0.03, lobe: 0.18 },
|
|
1241
|
-
'visual-training': { seed: 2.83, broad: 0.66, middle: 0.16, detail: 0.06, lobe: 0.23 },
|
|
1242
|
-
// ponytail: first-pass tide = asymmetric time warp on the same motion pipeline.
|
|
1243
|
-
// Refine (foam line / wash streaks) in the shader after visual QA.
|
|
1244
|
-
'tide': { seed: 4.12, broad: 0.82, middle: 0.07, detail: 0.02, lobe: 0.30, warp: 0.5 }
|
|
1245
|
-
};
|
|
1246
|
-
|
|
1247
|
-
const CACHE$1 = Object.create(null);
|
|
1248
|
-
|
|
1249
|
-
function gaussian(value, center, width) {
|
|
1250
|
-
const delta = (value - center) / Math.max(width, 0.001);
|
|
1251
|
-
return Math.exp(-delta * delta);
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
function createMotionData(id, edgeStyle = 'flow') {
|
|
1255
|
-
const profile = edgeStyle === 'tide'
|
|
1256
|
-
? PROFILE_CONFIG.tide
|
|
1257
|
-
: (PROFILE_CONFIG[id] || PROFILE_CONFIG['visual-training']);
|
|
1258
|
-
const data = new Uint8Array(PROGRESS_MOTION_WIDTH * PROGRESS_MOTION_HEIGHT);
|
|
1259
|
-
|
|
1260
|
-
for (let x = 0; x < PROGRESS_MOTION_WIDTH; x += 1) {
|
|
1261
|
-
let time = (x / PROGRESS_MOTION_WIDTH) * Math.PI * 2;
|
|
1262
|
-
if (profile.warp) time += profile.warp * Math.sin(time * 2);
|
|
1263
|
-
const centerA = 0.28 + Math.sin(time * 0.53 + profile.seed) * 0.13;
|
|
1264
|
-
const centerB = 0.70 + Math.cos(time * 0.47 + profile.seed * 1.7) * 0.12;
|
|
1265
|
-
|
|
1266
|
-
for (let y = 0; y < PROGRESS_MOTION_HEIGHT; y += 1) {
|
|
1267
|
-
const ratio = y / Math.max(PROGRESS_MOTION_HEIGHT - 1, 1);
|
|
1268
|
-
const envelope = Math.pow(Math.max(Math.sin(Math.PI * ratio), 0), 0.48);
|
|
1269
|
-
const broad = Math.sin(ratio * Math.PI * 2 * 1.35 + time * 0.58 + profile.seed) * profile.broad;
|
|
1270
|
-
const middle = Math.sin(ratio * Math.PI * 2 * 3.2 - time * 0.91 + profile.seed * 2.1) * profile.middle;
|
|
1271
|
-
const detail = Math.sin(ratio * Math.PI * 2 * 6.1 + time * 1.31 + profile.seed * 3.2) * profile.detail;
|
|
1272
|
-
const lobes = (
|
|
1273
|
-
gaussian(ratio, centerA, 0.09) * Math.sin(time * 1.11 + profile.seed * 4.0) -
|
|
1274
|
-
gaussian(ratio, centerB, 0.10) * Math.cos(time * 0.97 + profile.seed * 3.3)
|
|
1275
|
-
) * profile.lobe;
|
|
1276
|
-
const normalized = Math.max(-1, Math.min(1, (broad + middle + detail + lobes) * envelope));
|
|
1277
|
-
data[y * PROGRESS_MOTION_WIDTH + x] = Math.round((normalized * 0.5 + 0.5) * 255);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
return data;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function getProgressMotionData(id, edgeStyle = 'flow') {
|
|
1285
|
-
const key = `${id}:${edgeStyle}`;
|
|
1286
|
-
if (!CACHE$1[key]) CACHE$1[key] = createMotionData(id, edgeStyle);
|
|
1287
|
-
return CACHE$1[key];
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function hexToRgb01(hex) {
|
|
1291
|
-
const value = Number.parseInt(hex.replace('#', ''), 16);
|
|
1292
|
-
return [
|
|
1293
|
-
((value >> 16) & 255) / 255,
|
|
1294
|
-
((value >> 8) & 255) / 255,
|
|
1295
|
-
(value & 255) / 255
|
|
1296
|
-
];
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
function stringSeed$1(value) {
|
|
1300
|
-
let hash = 2166136261;
|
|
1301
|
-
for (const character of value) {
|
|
1302
|
-
hash ^= character.charCodeAt(0);
|
|
1303
|
-
hash = Math.imul(hash, 16777619);
|
|
1304
|
-
}
|
|
1305
|
-
return (hash >>> 0) / 4294967295;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
const PROFILE_INDEX = {
|
|
1309
|
-
'model-training': 0,
|
|
1310
|
-
'agent-migration': 1,
|
|
1311
|
-
'visual-training': 2,
|
|
1312
|
-
'tide': 3
|
|
1313
|
-
};
|
|
1314
|
-
|
|
1315
|
-
const MOTION_SCALE_FACTORS = {
|
|
1316
|
-
'model-training': 1.05,
|
|
1317
|
-
'agent-migration': 1.04,
|
|
1318
|
-
'visual-training': 1.04,
|
|
1319
|
-
'tide': 1.18
|
|
1320
|
-
};
|
|
1321
|
-
|
|
1322
|
-
const VERTEX_SHADER = `#version 300 es
|
|
1323
|
-
in vec2 a_position;
|
|
1324
|
-
out vec2 v_uv;
|
|
1325
|
-
void main() {
|
|
1326
|
-
v_uv = a_position * 0.5 + 0.5;
|
|
1327
|
-
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
1328
|
-
}`;
|
|
1329
|
-
|
|
1330
|
-
const FRAGMENT_SHADER = `#version 300 es
|
|
1331
|
-
precision highp float;
|
|
1332
|
-
|
|
1333
|
-
in vec2 v_uv;
|
|
1334
|
-
out vec4 outColor;
|
|
1335
|
-
|
|
1336
|
-
uniform vec2 u_resolution;
|
|
1337
|
-
uniform float u_time;
|
|
1338
|
-
uniform float u_progress;
|
|
1339
|
-
uniform float u_seed;
|
|
1340
|
-
uniform float u_profile;
|
|
1341
|
-
uniform sampler2D u_motion;
|
|
1342
|
-
uniform sampler2D u_effect;
|
|
1343
|
-
uniform float u_hasEffect;
|
|
1344
|
-
uniform float u_effectFrames;
|
|
1345
|
-
uniform float u_motionDuration;
|
|
1346
|
-
uniform float u_motionScale;
|
|
1347
|
-
uniform vec3 u_dark;
|
|
1348
|
-
uniform vec3 u_accentA;
|
|
1349
|
-
uniform vec3 u_accentB;
|
|
1350
|
-
uniform vec3 u_glow;
|
|
1351
|
-
|
|
1352
|
-
float hash21(vec2 p) {
|
|
1353
|
-
p = fract(p * vec2(123.34, 456.21));
|
|
1354
|
-
p += dot(p, p + 45.32 + u_seed * 11.7);
|
|
1355
|
-
return fract(p.x * p.y);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
float noise(vec2 p) {
|
|
1359
|
-
vec2 i = floor(p);
|
|
1360
|
-
vec2 f = fract(p);
|
|
1361
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
1362
|
-
float a = hash21(i);
|
|
1363
|
-
float b = hash21(i + vec2(1.0, 0.0));
|
|
1364
|
-
float c = hash21(i + vec2(0.0, 1.0));
|
|
1365
|
-
float d = hash21(i + vec2(1.0, 1.0));
|
|
1366
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
float fbm(vec2 p) {
|
|
1370
|
-
float value = 0.0;
|
|
1371
|
-
float amplitude = 0.55;
|
|
1372
|
-
mat2 rotation = mat2(0.82, 0.57, -0.57, 0.82);
|
|
1373
|
-
for (int i = 0; i < 6; i++) {
|
|
1374
|
-
value += noise(p) * amplitude;
|
|
1375
|
-
p = rotation * p * 2.02 + 13.7;
|
|
1376
|
-
amplitude *= 0.48;
|
|
1377
|
-
}
|
|
1378
|
-
return value;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
float gaussian(float value, float center, float width) {
|
|
1382
|
-
float delta = (value - center) / max(width, 0.0001);
|
|
1383
|
-
return exp(-delta * delta);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
float profileMix(float model, float agent, float visual) {
|
|
1387
|
-
if (u_profile < 0.5) return model;
|
|
1388
|
-
if (u_profile < 1.5) return agent;
|
|
1389
|
-
return visual;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
float motionSample(float y, float t) {
|
|
1393
|
-
float phase = fract(t / max(u_motionDuration, 0.001));
|
|
1394
|
-
float captured = texture(u_motion, vec2(phase, 1.0 - clamp(y, 0.0, 1.0))).r;
|
|
1395
|
-
return (captured * 2.0 - 1.0) * u_motionScale;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
float edgeDisplacement(float y, float t) {
|
|
1399
|
-
return motionSample(y, t);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
float flowDisplacement(float y, float t) {
|
|
1403
|
-
return (
|
|
1404
|
-
motionSample(y - 0.024, t) * 0.08 +
|
|
1405
|
-
motionSample(y - 0.012, t) * 0.18 +
|
|
1406
|
-
motionSample(y, t) * 0.48 +
|
|
1407
|
-
motionSample(y + 0.012, t) * 0.18 +
|
|
1408
|
-
motionSample(y + 0.024, t) * 0.08
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
float ellipseRing(vec2 p, float radius, float width) {
|
|
1413
|
-
return gaussian(length(p), radius, width);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
void main() {
|
|
1417
|
-
vec2 uv = v_uv;
|
|
1418
|
-
float t = u_time;
|
|
1419
|
-
float edge = u_progress + edgeDisplacement(uv.y, t);
|
|
1420
|
-
float flowEdge = u_progress + flowDisplacement(uv.y, t);
|
|
1421
|
-
float d = uv.x - edge;
|
|
1422
|
-
float fd = uv.x - flowEdge;
|
|
1423
|
-
|
|
1424
|
-
vec3 rightBase = vec3(0.125, 0.129, 0.145);
|
|
1425
|
-
vec3 color = rightBase;
|
|
1426
|
-
|
|
1427
|
-
float leftMask = 1.0 - smoothstep(-0.001, 0.002, d);
|
|
1428
|
-
color = mix(color, u_dark, leftMask * profileMix(0.96, 0.92, 0.96));
|
|
1429
|
-
|
|
1430
|
-
vec2 flowP = vec2((fd + 0.10) * 6.2, uv.y * 1.95);
|
|
1431
|
-
float flowA = fbm(flowP + vec2(-t * 0.22, t * 0.27) + u_seed * 1.7);
|
|
1432
|
-
float flowB = fbm(flowP * 1.52 + vec2(t * 0.28, -t * 0.36) + 8.2 + u_seed);
|
|
1433
|
-
float flowC = fbm(flowP * 2.25 + vec2(-t * 0.41, t * 0.46) + 19.0);
|
|
1434
|
-
|
|
1435
|
-
float farCenter = profileMix(-0.060, -0.079, -0.045) + (flowA - 0.5) * profileMix(0.018, 0.022, 0.014);
|
|
1436
|
-
float midCenter = profileMix(-0.039, -0.052, -0.030) + (flowB - 0.5) * profileMix(0.013, 0.016, 0.010);
|
|
1437
|
-
float hotCenter = profileMix(-0.026, -0.029, -0.023) + (flowC - 0.5) * 0.010;
|
|
1438
|
-
|
|
1439
|
-
float farBand = gaussian(fd, farCenter, profileMix(0.035, 0.049, 0.030));
|
|
1440
|
-
float midBand = gaussian(fd, midCenter, profileMix(0.026, 0.034, 0.026));
|
|
1441
|
-
float hotBand = gaussian(fd, hotCenter, profileMix(0.023, 0.027, 0.026));
|
|
1442
|
-
float darkTrough = gaussian(fd, profileMix(-0.050, -0.058, -0.044) + (flowB - 0.5) * 0.010, profileMix(0.020, 0.025, 0.021));
|
|
1443
|
-
|
|
1444
|
-
float ringY = 0.47 + sin(t * 0.58 + u_seed * 2.4) * 0.12;
|
|
1445
|
-
vec2 ringP = vec2((fd + 0.086) / 0.078, (uv.y - ringY) / 0.25);
|
|
1446
|
-
ringP += vec2((flowB - 0.5) * 0.08, (flowA - 0.5) * 0.06);
|
|
1447
|
-
float ringTexture = fbm(ringP * 2.15 + vec2(t * 0.18, -t * 0.14) + u_seed * 1.9);
|
|
1448
|
-
float ring = ellipseRing(ringP, 0.66, 0.32) * (0.30 + 0.64 * ringTexture);
|
|
1449
|
-
float ringCore = gaussian(length(ringP), 0.25, 0.25);
|
|
1450
|
-
float ringPulse = smoothstep(0.58, 0.90, 0.5 + 0.5 * sin(t * 0.82 + u_seed * 4.1));
|
|
1451
|
-
float modelRing = ring * ringPulse * (1.0 - step(0.5, u_profile));
|
|
1452
|
-
float visualRing = ring * 0.16 * step(1.5, u_profile) * ringPulse;
|
|
1453
|
-
|
|
1454
|
-
float cloudGate = leftMask * smoothstep(-0.30, -0.008, fd);
|
|
1455
|
-
float textureA = smoothstep(0.24, 0.92, flowA * 0.72 + flowB * 0.42);
|
|
1456
|
-
float textureB = smoothstep(0.28, 0.94, flowB * 0.68 + flowC * 0.38);
|
|
1457
|
-
|
|
1458
|
-
vec3 hotColor = u_accentB;
|
|
1459
|
-
color += u_accentA * farBand * cloudGate * (0.07 + textureA * profileMix(0.42, 0.24, 0.40));
|
|
1460
|
-
color += u_accentB * midBand * cloudGate * (0.15 + textureB * profileMix(0.70, 0.46, 0.68));
|
|
1461
|
-
color += hotColor * hotBand * cloudGate * profileMix(0.88, 0.62, 0.84);
|
|
1462
|
-
float modelMask = 1.0 - step(0.5, u_profile);
|
|
1463
|
-
color += u_accentA * (modelRing + visualRing) * cloudGate * profileMix(0.54, 0.0, 0.34);
|
|
1464
|
-
color *= 1.0 - darkTrough * profileMix(0.44, 0.24, 0.24) * cloudGate;
|
|
1465
|
-
color *= 1.0 - ringCore * modelMask * ringPulse * 0.44 * cloudGate;
|
|
1466
|
-
|
|
1467
|
-
float broadHalo = exp(-abs(d) * 96.0);
|
|
1468
|
-
float innerHalo = exp(-abs(d) * 176.0);
|
|
1469
|
-
float colorCore = exp(-abs(d) * 360.0);
|
|
1470
|
-
float sharpCore = exp(-abs(d) * 760.0);
|
|
1471
|
-
float leftGate = 1.0 - smoothstep(-0.003, 0.005, d);
|
|
1472
|
-
|
|
1473
|
-
color += u_accentA * broadHalo * leftGate * profileMix(0.09, 0.04, 0.06);
|
|
1474
|
-
color += hotColor * innerHalo * leftGate * profileMix(0.76, 0.72, 0.78);
|
|
1475
|
-
color += u_glow * colorCore * profileMix(0.72, 0.34, 0.24);
|
|
1476
|
-
|
|
1477
|
-
float whiteStrength = profileMix(0.10, 0.0, 0.0);
|
|
1478
|
-
color += vec3(1.0, 0.99, 0.91) * sharpCore * whiteStrength;
|
|
1479
|
-
|
|
1480
|
-
float rightCut = smoothstep(0.001, 0.006, d);
|
|
1481
|
-
color = mix(color, rightBase, rightCut);
|
|
1482
|
-
|
|
1483
|
-
float effectX = (d * 1257.0 + 260.0) / 320.0;
|
|
1484
|
-
float atlasPhase = fract(t / 12.0) * u_effectFrames;
|
|
1485
|
-
float atlasFrameA = floor(atlasPhase);
|
|
1486
|
-
float atlasFrameB = mod(atlasFrameA + 1.0, u_effectFrames);
|
|
1487
|
-
float atlasMix = smoothstep(0.0, 1.0, fract(atlasPhase));
|
|
1488
|
-
float atlasXA = (atlasFrameA + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
1489
|
-
float atlasXB = (atlasFrameB + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
1490
|
-
vec3 referenceA = texture(u_effect, vec2(atlasXA, uv.y)).rgb;
|
|
1491
|
-
vec3 referenceB = texture(u_effect, vec2(atlasXB, uv.y)).rgb;
|
|
1492
|
-
vec3 referenceColor = mix(referenceA, referenceB, atlasMix);
|
|
1493
|
-
float stripMask = smoothstep(0.0, 0.018, effectX) * (1.0 - smoothstep(0.982, 1.0, effectX));
|
|
1494
|
-
float referenceLeft = 1.0 - smoothstep(-0.026, -0.012, d);
|
|
1495
|
-
// 参考图集只提供亮度结构,颜色始终由用户 colors(u_dark / u_accentA /
|
|
1496
|
-
// u_accentB / u_glow)决定:这样 setColors / colors 属性在 WebGL 路径下
|
|
1497
|
-
// 真实生效,改色有可见反馈,而不是被图集整体覆盖。
|
|
1498
|
-
float referenceLuma = dot(referenceColor, vec3(0.299, 0.587, 0.114));
|
|
1499
|
-
vec3 tintedColor = color * (0.42 + 0.86 * referenceLuma);
|
|
1500
|
-
color = mix(color, tintedColor, stripMask * referenceLeft * u_hasEffect);
|
|
1501
|
-
|
|
1502
|
-
outColor = vec4(clamp(color, 0.0, 1.0), 1.0);
|
|
1503
|
-
}`;
|
|
1504
|
-
|
|
1505
|
-
function compileShader(gl, type, source) {
|
|
1506
|
-
const shader = gl.createShader(type);
|
|
1507
|
-
gl.shaderSource(shader, source);
|
|
1508
|
-
gl.compileShader(shader);
|
|
1509
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1510
|
-
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
1511
|
-
gl.deleteShader(shader);
|
|
1512
|
-
throw new Error(message);
|
|
1513
|
-
}
|
|
1514
|
-
return shader;
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function createProgram(gl) {
|
|
1518
|
-
const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
1519
|
-
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
1520
|
-
const program = gl.createProgram();
|
|
1521
|
-
gl.attachShader(program, vertex);
|
|
1522
|
-
gl.attachShader(program, fragment);
|
|
1523
|
-
gl.linkProgram(program);
|
|
1524
|
-
gl.deleteShader(vertex);
|
|
1525
|
-
gl.deleteShader(fragment);
|
|
1526
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1527
|
-
const message = gl.getProgramInfoLog(program) || 'Unknown shader link error';
|
|
1528
|
-
gl.deleteProgram(program);
|
|
1529
|
-
throw new Error(message);
|
|
1530
|
-
}
|
|
1531
|
-
return program;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
class ProgressFlowRenderer {
|
|
1535
|
-
constructor(canvas, preset) {
|
|
1536
|
-
const gl = canvas.getContext('webgl2', {
|
|
1537
|
-
alpha: false,
|
|
1538
|
-
antialias: true,
|
|
1539
|
-
premultipliedAlpha: false,
|
|
1540
|
-
powerPreference: 'high-performance'
|
|
1541
|
-
});
|
|
1542
|
-
if (!gl) throw new Error('WebGL2 unavailable');
|
|
1543
|
-
|
|
1544
|
-
this.canvas = canvas;
|
|
1545
|
-
this.
|
|
1546
|
-
this.
|
|
1547
|
-
this.profile = preset.edgeStyle === 'tide' ? 3 : (PROFILE_INDEX[preset.id] ?? 2);
|
|
1548
|
-
this.seed = stringSeed$1(`${preset.id}-shader`) * 13.7 + 1.0;
|
|
1549
|
-
this.colors = preset.colors.map(hexToRgb01);
|
|
1550
|
-
this.motionData = getProgressMotionData(preset.id, preset.edgeStyle);
|
|
1551
|
-
const motionFactor = preset.edgeStyle === 'tide'
|
|
1552
|
-
? MOTION_SCALE_FACTORS.tide
|
|
1553
|
-
: (MOTION_SCALE_FACTORS[preset.id] || 1.04);
|
|
1554
|
-
this.motionScale = (PROGRESS_MOTION_MAX_PX * motionFactor) / 1257;
|
|
1555
|
-
|
|
1556
|
-
this.
|
|
1557
|
-
this.
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
gl.
|
|
1579
|
-
|
|
1580
|
-
gl.
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
gl.
|
|
1585
|
-
gl.
|
|
1586
|
-
gl.
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
gl.
|
|
1667
|
-
gl.
|
|
1668
|
-
|
|
1669
|
-
gl.
|
|
1670
|
-
gl.uniform1f(this.uniforms.
|
|
1671
|
-
|
|
1672
|
-
gl.
|
|
1673
|
-
gl.
|
|
1674
|
-
gl.
|
|
1675
|
-
gl.
|
|
1676
|
-
|
|
1677
|
-
gl.
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1467
|
+
const PROGRESS_MOTION_WIDTH = 240;
|
|
1468
|
+
const PROGRESS_MOTION_HEIGHT = 80;
|
|
1469
|
+
const PROGRESS_MOTION_DURATION = 12.0;
|
|
1470
|
+
const PROGRESS_MOTION_MAX_PX = 40.0;
|
|
1471
|
+
|
|
1472
|
+
const PROFILE_CONFIG = {
|
|
1473
|
+
'model-training': { seed: 0.37, broad: 0.58, middle: 0.25, detail: 0.13, lobe: 0.24 },
|
|
1474
|
+
'agent-migration': { seed: 1.71, broad: 0.72, middle: 0.10, detail: 0.03, lobe: 0.18 },
|
|
1475
|
+
'visual-training': { seed: 2.83, broad: 0.66, middle: 0.16, detail: 0.06, lobe: 0.23 },
|
|
1476
|
+
// ponytail: first-pass tide = asymmetric time warp on the same motion pipeline.
|
|
1477
|
+
// Refine (foam line / wash streaks) in the shader after visual QA.
|
|
1478
|
+
'tide': { seed: 4.12, broad: 0.82, middle: 0.07, detail: 0.02, lobe: 0.30, warp: 0.5 }
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
const CACHE$1 = Object.create(null);
|
|
1482
|
+
|
|
1483
|
+
function gaussian(value, center, width) {
|
|
1484
|
+
const delta = (value - center) / Math.max(width, 0.001);
|
|
1485
|
+
return Math.exp(-delta * delta);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function createMotionData(id, edgeStyle = 'flow') {
|
|
1489
|
+
const profile = edgeStyle === 'tide'
|
|
1490
|
+
? PROFILE_CONFIG.tide
|
|
1491
|
+
: (PROFILE_CONFIG[id] || PROFILE_CONFIG['visual-training']);
|
|
1492
|
+
const data = new Uint8Array(PROGRESS_MOTION_WIDTH * PROGRESS_MOTION_HEIGHT);
|
|
1493
|
+
|
|
1494
|
+
for (let x = 0; x < PROGRESS_MOTION_WIDTH; x += 1) {
|
|
1495
|
+
let time = (x / PROGRESS_MOTION_WIDTH) * Math.PI * 2;
|
|
1496
|
+
if (profile.warp) time += profile.warp * Math.sin(time * 2);
|
|
1497
|
+
const centerA = 0.28 + Math.sin(time * 0.53 + profile.seed) * 0.13;
|
|
1498
|
+
const centerB = 0.70 + Math.cos(time * 0.47 + profile.seed * 1.7) * 0.12;
|
|
1499
|
+
|
|
1500
|
+
for (let y = 0; y < PROGRESS_MOTION_HEIGHT; y += 1) {
|
|
1501
|
+
const ratio = y / Math.max(PROGRESS_MOTION_HEIGHT - 1, 1);
|
|
1502
|
+
const envelope = Math.pow(Math.max(Math.sin(Math.PI * ratio), 0), 0.48);
|
|
1503
|
+
const broad = Math.sin(ratio * Math.PI * 2 * 1.35 + time * 0.58 + profile.seed) * profile.broad;
|
|
1504
|
+
const middle = Math.sin(ratio * Math.PI * 2 * 3.2 - time * 0.91 + profile.seed * 2.1) * profile.middle;
|
|
1505
|
+
const detail = Math.sin(ratio * Math.PI * 2 * 6.1 + time * 1.31 + profile.seed * 3.2) * profile.detail;
|
|
1506
|
+
const lobes = (
|
|
1507
|
+
gaussian(ratio, centerA, 0.09) * Math.sin(time * 1.11 + profile.seed * 4.0) -
|
|
1508
|
+
gaussian(ratio, centerB, 0.10) * Math.cos(time * 0.97 + profile.seed * 3.3)
|
|
1509
|
+
) * profile.lobe;
|
|
1510
|
+
const normalized = Math.max(-1, Math.min(1, (broad + middle + detail + lobes) * envelope));
|
|
1511
|
+
data[y * PROGRESS_MOTION_WIDTH + x] = Math.round((normalized * 0.5 + 0.5) * 255);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return data;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function getProgressMotionData(id, edgeStyle = 'flow') {
|
|
1519
|
+
const key = `${id}:${edgeStyle}`;
|
|
1520
|
+
if (!CACHE$1[key]) CACHE$1[key] = createMotionData(id, edgeStyle);
|
|
1521
|
+
return CACHE$1[key];
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function hexToRgb01(hex) {
|
|
1525
|
+
const value = Number.parseInt(hex.replace('#', ''), 16);
|
|
1526
|
+
return [
|
|
1527
|
+
((value >> 16) & 255) / 255,
|
|
1528
|
+
((value >> 8) & 255) / 255,
|
|
1529
|
+
(value & 255) / 255
|
|
1530
|
+
];
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function stringSeed$1(value) {
|
|
1534
|
+
let hash = 2166136261;
|
|
1535
|
+
for (const character of value) {
|
|
1536
|
+
hash ^= character.charCodeAt(0);
|
|
1537
|
+
hash = Math.imul(hash, 16777619);
|
|
1538
|
+
}
|
|
1539
|
+
return (hash >>> 0) / 4294967295;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const PROFILE_INDEX = {
|
|
1543
|
+
'model-training': 0,
|
|
1544
|
+
'agent-migration': 1,
|
|
1545
|
+
'visual-training': 2,
|
|
1546
|
+
'tide': 3
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
const MOTION_SCALE_FACTORS = {
|
|
1550
|
+
'model-training': 1.05,
|
|
1551
|
+
'agent-migration': 1.04,
|
|
1552
|
+
'visual-training': 1.04,
|
|
1553
|
+
'tide': 1.18
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
const VERTEX_SHADER = `#version 300 es
|
|
1557
|
+
in vec2 a_position;
|
|
1558
|
+
out vec2 v_uv;
|
|
1559
|
+
void main() {
|
|
1560
|
+
v_uv = a_position * 0.5 + 0.5;
|
|
1561
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
1562
|
+
}`;
|
|
1563
|
+
|
|
1564
|
+
const FRAGMENT_SHADER = `#version 300 es
|
|
1565
|
+
precision highp float;
|
|
1566
|
+
|
|
1567
|
+
in vec2 v_uv;
|
|
1568
|
+
out vec4 outColor;
|
|
1569
|
+
|
|
1570
|
+
uniform vec2 u_resolution;
|
|
1571
|
+
uniform float u_time;
|
|
1572
|
+
uniform float u_progress;
|
|
1573
|
+
uniform float u_seed;
|
|
1574
|
+
uniform float u_profile;
|
|
1575
|
+
uniform sampler2D u_motion;
|
|
1576
|
+
uniform sampler2D u_effect;
|
|
1577
|
+
uniform float u_hasEffect;
|
|
1578
|
+
uniform float u_effectFrames;
|
|
1579
|
+
uniform float u_motionDuration;
|
|
1580
|
+
uniform float u_motionScale;
|
|
1581
|
+
uniform vec3 u_dark;
|
|
1582
|
+
uniform vec3 u_accentA;
|
|
1583
|
+
uniform vec3 u_accentB;
|
|
1584
|
+
uniform vec3 u_glow;
|
|
1585
|
+
|
|
1586
|
+
float hash21(vec2 p) {
|
|
1587
|
+
p = fract(p * vec2(123.34, 456.21));
|
|
1588
|
+
p += dot(p, p + 45.32 + u_seed * 11.7);
|
|
1589
|
+
return fract(p.x * p.y);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
float noise(vec2 p) {
|
|
1593
|
+
vec2 i = floor(p);
|
|
1594
|
+
vec2 f = fract(p);
|
|
1595
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
1596
|
+
float a = hash21(i);
|
|
1597
|
+
float b = hash21(i + vec2(1.0, 0.0));
|
|
1598
|
+
float c = hash21(i + vec2(0.0, 1.0));
|
|
1599
|
+
float d = hash21(i + vec2(1.0, 1.0));
|
|
1600
|
+
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
float fbm(vec2 p) {
|
|
1604
|
+
float value = 0.0;
|
|
1605
|
+
float amplitude = 0.55;
|
|
1606
|
+
mat2 rotation = mat2(0.82, 0.57, -0.57, 0.82);
|
|
1607
|
+
for (int i = 0; i < 6; i++) {
|
|
1608
|
+
value += noise(p) * amplitude;
|
|
1609
|
+
p = rotation * p * 2.02 + 13.7;
|
|
1610
|
+
amplitude *= 0.48;
|
|
1611
|
+
}
|
|
1612
|
+
return value;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
float gaussian(float value, float center, float width) {
|
|
1616
|
+
float delta = (value - center) / max(width, 0.0001);
|
|
1617
|
+
return exp(-delta * delta);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
float profileMix(float model, float agent, float visual) {
|
|
1621
|
+
if (u_profile < 0.5) return model;
|
|
1622
|
+
if (u_profile < 1.5) return agent;
|
|
1623
|
+
return visual;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
float motionSample(float y, float t) {
|
|
1627
|
+
float phase = fract(t / max(u_motionDuration, 0.001));
|
|
1628
|
+
float captured = texture(u_motion, vec2(phase, 1.0 - clamp(y, 0.0, 1.0))).r;
|
|
1629
|
+
return (captured * 2.0 - 1.0) * u_motionScale;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
float edgeDisplacement(float y, float t) {
|
|
1633
|
+
return motionSample(y, t);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
float flowDisplacement(float y, float t) {
|
|
1637
|
+
return (
|
|
1638
|
+
motionSample(y - 0.024, t) * 0.08 +
|
|
1639
|
+
motionSample(y - 0.012, t) * 0.18 +
|
|
1640
|
+
motionSample(y, t) * 0.48 +
|
|
1641
|
+
motionSample(y + 0.012, t) * 0.18 +
|
|
1642
|
+
motionSample(y + 0.024, t) * 0.08
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
float ellipseRing(vec2 p, float radius, float width) {
|
|
1647
|
+
return gaussian(length(p), radius, width);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
void main() {
|
|
1651
|
+
vec2 uv = v_uv;
|
|
1652
|
+
float t = u_time;
|
|
1653
|
+
float edge = u_progress + edgeDisplacement(uv.y, t);
|
|
1654
|
+
float flowEdge = u_progress + flowDisplacement(uv.y, t);
|
|
1655
|
+
float d = uv.x - edge;
|
|
1656
|
+
float fd = uv.x - flowEdge;
|
|
1657
|
+
|
|
1658
|
+
vec3 rightBase = vec3(0.125, 0.129, 0.145);
|
|
1659
|
+
vec3 color = rightBase;
|
|
1660
|
+
|
|
1661
|
+
float leftMask = 1.0 - smoothstep(-0.001, 0.002, d);
|
|
1662
|
+
color = mix(color, u_dark, leftMask * profileMix(0.96, 0.92, 0.96));
|
|
1663
|
+
|
|
1664
|
+
vec2 flowP = vec2((fd + 0.10) * 6.2, uv.y * 1.95);
|
|
1665
|
+
float flowA = fbm(flowP + vec2(-t * 0.22, t * 0.27) + u_seed * 1.7);
|
|
1666
|
+
float flowB = fbm(flowP * 1.52 + vec2(t * 0.28, -t * 0.36) + 8.2 + u_seed);
|
|
1667
|
+
float flowC = fbm(flowP * 2.25 + vec2(-t * 0.41, t * 0.46) + 19.0);
|
|
1668
|
+
|
|
1669
|
+
float farCenter = profileMix(-0.060, -0.079, -0.045) + (flowA - 0.5) * profileMix(0.018, 0.022, 0.014);
|
|
1670
|
+
float midCenter = profileMix(-0.039, -0.052, -0.030) + (flowB - 0.5) * profileMix(0.013, 0.016, 0.010);
|
|
1671
|
+
float hotCenter = profileMix(-0.026, -0.029, -0.023) + (flowC - 0.5) * 0.010;
|
|
1672
|
+
|
|
1673
|
+
float farBand = gaussian(fd, farCenter, profileMix(0.035, 0.049, 0.030));
|
|
1674
|
+
float midBand = gaussian(fd, midCenter, profileMix(0.026, 0.034, 0.026));
|
|
1675
|
+
float hotBand = gaussian(fd, hotCenter, profileMix(0.023, 0.027, 0.026));
|
|
1676
|
+
float darkTrough = gaussian(fd, profileMix(-0.050, -0.058, -0.044) + (flowB - 0.5) * 0.010, profileMix(0.020, 0.025, 0.021));
|
|
1677
|
+
|
|
1678
|
+
float ringY = 0.47 + sin(t * 0.58 + u_seed * 2.4) * 0.12;
|
|
1679
|
+
vec2 ringP = vec2((fd + 0.086) / 0.078, (uv.y - ringY) / 0.25);
|
|
1680
|
+
ringP += vec2((flowB - 0.5) * 0.08, (flowA - 0.5) * 0.06);
|
|
1681
|
+
float ringTexture = fbm(ringP * 2.15 + vec2(t * 0.18, -t * 0.14) + u_seed * 1.9);
|
|
1682
|
+
float ring = ellipseRing(ringP, 0.66, 0.32) * (0.30 + 0.64 * ringTexture);
|
|
1683
|
+
float ringCore = gaussian(length(ringP), 0.25, 0.25);
|
|
1684
|
+
float ringPulse = smoothstep(0.58, 0.90, 0.5 + 0.5 * sin(t * 0.82 + u_seed * 4.1));
|
|
1685
|
+
float modelRing = ring * ringPulse * (1.0 - step(0.5, u_profile));
|
|
1686
|
+
float visualRing = ring * 0.16 * step(1.5, u_profile) * ringPulse;
|
|
1687
|
+
|
|
1688
|
+
float cloudGate = leftMask * smoothstep(-0.30, -0.008, fd);
|
|
1689
|
+
float textureA = smoothstep(0.24, 0.92, flowA * 0.72 + flowB * 0.42);
|
|
1690
|
+
float textureB = smoothstep(0.28, 0.94, flowB * 0.68 + flowC * 0.38);
|
|
1691
|
+
|
|
1692
|
+
vec3 hotColor = u_accentB;
|
|
1693
|
+
color += u_accentA * farBand * cloudGate * (0.07 + textureA * profileMix(0.42, 0.24, 0.40));
|
|
1694
|
+
color += u_accentB * midBand * cloudGate * (0.15 + textureB * profileMix(0.70, 0.46, 0.68));
|
|
1695
|
+
color += hotColor * hotBand * cloudGate * profileMix(0.88, 0.62, 0.84);
|
|
1696
|
+
float modelMask = 1.0 - step(0.5, u_profile);
|
|
1697
|
+
color += u_accentA * (modelRing + visualRing) * cloudGate * profileMix(0.54, 0.0, 0.34);
|
|
1698
|
+
color *= 1.0 - darkTrough * profileMix(0.44, 0.24, 0.24) * cloudGate;
|
|
1699
|
+
color *= 1.0 - ringCore * modelMask * ringPulse * 0.44 * cloudGate;
|
|
1700
|
+
|
|
1701
|
+
float broadHalo = exp(-abs(d) * 96.0);
|
|
1702
|
+
float innerHalo = exp(-abs(d) * 176.0);
|
|
1703
|
+
float colorCore = exp(-abs(d) * 360.0);
|
|
1704
|
+
float sharpCore = exp(-abs(d) * 760.0);
|
|
1705
|
+
float leftGate = 1.0 - smoothstep(-0.003, 0.005, d);
|
|
1706
|
+
|
|
1707
|
+
color += u_accentA * broadHalo * leftGate * profileMix(0.09, 0.04, 0.06);
|
|
1708
|
+
color += hotColor * innerHalo * leftGate * profileMix(0.76, 0.72, 0.78);
|
|
1709
|
+
color += u_glow * colorCore * profileMix(0.72, 0.34, 0.24);
|
|
1710
|
+
|
|
1711
|
+
float whiteStrength = profileMix(0.10, 0.0, 0.0);
|
|
1712
|
+
color += vec3(1.0, 0.99, 0.91) * sharpCore * whiteStrength;
|
|
1713
|
+
|
|
1714
|
+
float rightCut = smoothstep(0.001, 0.006, d);
|
|
1715
|
+
color = mix(color, rightBase, rightCut);
|
|
1716
|
+
|
|
1717
|
+
float effectX = (d * 1257.0 + 260.0) / 320.0;
|
|
1718
|
+
float atlasPhase = fract(t / 12.0) * u_effectFrames;
|
|
1719
|
+
float atlasFrameA = floor(atlasPhase);
|
|
1720
|
+
float atlasFrameB = mod(atlasFrameA + 1.0, u_effectFrames);
|
|
1721
|
+
float atlasMix = smoothstep(0.0, 1.0, fract(atlasPhase));
|
|
1722
|
+
float atlasXA = (atlasFrameA + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
1723
|
+
float atlasXB = (atlasFrameB + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
1724
|
+
vec3 referenceA = texture(u_effect, vec2(atlasXA, uv.y)).rgb;
|
|
1725
|
+
vec3 referenceB = texture(u_effect, vec2(atlasXB, uv.y)).rgb;
|
|
1726
|
+
vec3 referenceColor = mix(referenceA, referenceB, atlasMix);
|
|
1727
|
+
float stripMask = smoothstep(0.0, 0.018, effectX) * (1.0 - smoothstep(0.982, 1.0, effectX));
|
|
1728
|
+
float referenceLeft = 1.0 - smoothstep(-0.026, -0.012, d);
|
|
1729
|
+
// 参考图集只提供亮度结构,颜色始终由用户 colors(u_dark / u_accentA /
|
|
1730
|
+
// u_accentB / u_glow)决定:这样 setColors / colors 属性在 WebGL 路径下
|
|
1731
|
+
// 真实生效,改色有可见反馈,而不是被图集整体覆盖。
|
|
1732
|
+
float referenceLuma = dot(referenceColor, vec3(0.299, 0.587, 0.114));
|
|
1733
|
+
vec3 tintedColor = color * (0.42 + 0.86 * referenceLuma);
|
|
1734
|
+
color = mix(color, tintedColor, stripMask * referenceLeft * u_hasEffect);
|
|
1735
|
+
|
|
1736
|
+
outColor = vec4(clamp(color, 0.0, 1.0), 1.0);
|
|
1737
|
+
}`;
|
|
1738
|
+
|
|
1739
|
+
function compileShader(gl, type, source) {
|
|
1740
|
+
const shader = gl.createShader(type);
|
|
1741
|
+
gl.shaderSource(shader, source);
|
|
1742
|
+
gl.compileShader(shader);
|
|
1743
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1744
|
+
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
1745
|
+
gl.deleteShader(shader);
|
|
1746
|
+
throw new Error(message);
|
|
1747
|
+
}
|
|
1748
|
+
return shader;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function createProgram(gl) {
|
|
1752
|
+
const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
1753
|
+
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
1754
|
+
const program = gl.createProgram();
|
|
1755
|
+
gl.attachShader(program, vertex);
|
|
1756
|
+
gl.attachShader(program, fragment);
|
|
1757
|
+
gl.linkProgram(program);
|
|
1758
|
+
gl.deleteShader(vertex);
|
|
1759
|
+
gl.deleteShader(fragment);
|
|
1760
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1761
|
+
const message = gl.getProgramInfoLog(program) || 'Unknown shader link error';
|
|
1762
|
+
gl.deleteProgram(program);
|
|
1763
|
+
throw new Error(message);
|
|
1764
|
+
}
|
|
1765
|
+
return program;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
class ProgressFlowRenderer {
|
|
1769
|
+
constructor(canvas, preset, options = {}) {
|
|
1770
|
+
const gl = canvas.getContext('webgl2', {
|
|
1771
|
+
alpha: false,
|
|
1772
|
+
antialias: true,
|
|
1773
|
+
premultipliedAlpha: false,
|
|
1774
|
+
powerPreference: options.powerPreference || 'high-performance'
|
|
1775
|
+
});
|
|
1776
|
+
if (!gl) throw new Error('WebGL2 unavailable');
|
|
1777
|
+
|
|
1778
|
+
this.canvas = canvas;
|
|
1779
|
+
this.options = options;
|
|
1780
|
+
this.gl = gl;
|
|
1781
|
+
this.profile = preset.edgeStyle === 'tide' ? 3 : (PROFILE_INDEX[preset.id] ?? 2);
|
|
1782
|
+
this.seed = stringSeed$1(`${preset.id}-shader`) * 13.7 + 1.0;
|
|
1783
|
+
this.colors = preset.colors.map(hexToRgb01);
|
|
1784
|
+
this.motionData = getProgressMotionData(preset.id, preset.edgeStyle);
|
|
1785
|
+
const motionFactor = preset.edgeStyle === 'tide'
|
|
1786
|
+
? MOTION_SCALE_FACTORS.tide
|
|
1787
|
+
: (MOTION_SCALE_FACTORS[preset.id] || 1.04);
|
|
1788
|
+
this.motionScale = (PROGRESS_MOTION_MAX_PX * motionFactor) / 1257;
|
|
1789
|
+
this.disposed = false;
|
|
1790
|
+
this.contextLost = false;
|
|
1791
|
+
this.setupResources();
|
|
1792
|
+
this.bindContextEvents();
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
setupResources() {
|
|
1796
|
+
const gl = this.gl;
|
|
1797
|
+
this.program = createProgram(gl);
|
|
1798
|
+
|
|
1799
|
+
this.position = gl.getAttribLocation(this.program, 'a_position');
|
|
1800
|
+
this.uniforms = {
|
|
1801
|
+
resolution: gl.getUniformLocation(this.program, 'u_resolution'),
|
|
1802
|
+
time: gl.getUniformLocation(this.program, 'u_time'),
|
|
1803
|
+
progress: gl.getUniformLocation(this.program, 'u_progress'),
|
|
1804
|
+
seed: gl.getUniformLocation(this.program, 'u_seed'),
|
|
1805
|
+
profile: gl.getUniformLocation(this.program, 'u_profile'),
|
|
1806
|
+
motion: gl.getUniformLocation(this.program, 'u_motion'),
|
|
1807
|
+
effect: gl.getUniformLocation(this.program, 'u_effect'),
|
|
1808
|
+
hasEffect: gl.getUniformLocation(this.program, 'u_hasEffect'),
|
|
1809
|
+
effectFrames: gl.getUniformLocation(this.program, 'u_effectFrames'),
|
|
1810
|
+
motionDuration: gl.getUniformLocation(this.program, 'u_motionDuration'),
|
|
1811
|
+
motionScale: gl.getUniformLocation(this.program, 'u_motionScale'),
|
|
1812
|
+
dark: gl.getUniformLocation(this.program, 'u_dark'),
|
|
1813
|
+
accentA: gl.getUniformLocation(this.program, 'u_accentA'),
|
|
1814
|
+
accentB: gl.getUniformLocation(this.program, 'u_accentB'),
|
|
1815
|
+
glow: gl.getUniformLocation(this.program, 'u_glow')
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
this.buffer = gl.createBuffer();
|
|
1819
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
1820
|
+
gl.bufferData(
|
|
1821
|
+
gl.ARRAY_BUFFER,
|
|
1822
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
|
|
1823
|
+
gl.STATIC_DRAW
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
this.motionTexture = gl.createTexture();
|
|
1827
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1828
|
+
gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
|
|
1829
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
1830
|
+
gl.texImage2D(
|
|
1831
|
+
gl.TEXTURE_2D,
|
|
1832
|
+
0,
|
|
1833
|
+
gl.R8,
|
|
1834
|
+
PROGRESS_MOTION_WIDTH,
|
|
1835
|
+
PROGRESS_MOTION_HEIGHT,
|
|
1836
|
+
0,
|
|
1837
|
+
gl.RED,
|
|
1838
|
+
gl.UNSIGNED_BYTE,
|
|
1839
|
+
this.motionData
|
|
1840
|
+
);
|
|
1841
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1842
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1843
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
1844
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1845
|
+
|
|
1846
|
+
this.effectTexture = gl.createTexture();
|
|
1847
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1848
|
+
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
1849
|
+
gl.texImage2D(
|
|
1850
|
+
gl.TEXTURE_2D,
|
|
1851
|
+
0,
|
|
1852
|
+
gl.RGB,
|
|
1853
|
+
1,
|
|
1854
|
+
1,
|
|
1855
|
+
0,
|
|
1856
|
+
gl.RGB,
|
|
1857
|
+
gl.UNSIGNED_BYTE,
|
|
1858
|
+
new Uint8Array([32, 33, 38])
|
|
1859
|
+
);
|
|
1860
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1861
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1862
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1863
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1864
|
+
this.effectUploaded = false;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
bindContextEvents() {
|
|
1868
|
+
this.onContextLost = (event) => {
|
|
1869
|
+
event.preventDefault();
|
|
1870
|
+
if (this.disposed) return;
|
|
1871
|
+
this.contextLost = true;
|
|
1872
|
+
if (typeof this.options.onContextLost === 'function') this.options.onContextLost(event);
|
|
1873
|
+
};
|
|
1874
|
+
this.onContextRestored = () => {
|
|
1875
|
+
if (this.disposed) return;
|
|
1876
|
+
this.setupResources();
|
|
1877
|
+
this.contextLost = false;
|
|
1878
|
+
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
1879
|
+
if (typeof this.options.onContextRestored === 'function') this.options.onContextRestored();
|
|
1880
|
+
};
|
|
1881
|
+
this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
|
|
1882
|
+
this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
resize(width, height, dpr) {
|
|
1886
|
+
const pixelWidth = Math.max(1, Math.round(width * dpr));
|
|
1887
|
+
const pixelHeight = Math.max(1, Math.round(height * dpr));
|
|
1888
|
+
if (this.canvas.width !== pixelWidth || this.canvas.height !== pixelHeight) {
|
|
1889
|
+
this.canvas.width = pixelWidth;
|
|
1890
|
+
this.canvas.height = pixelHeight;
|
|
1891
|
+
}
|
|
1892
|
+
this.gl.viewport(0, 0, pixelWidth, pixelHeight);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
draw(time, progress, effectImage = null) {
|
|
1896
|
+
if (this.disposed || this.contextLost) return;
|
|
1897
|
+
const gl = this.gl;
|
|
1898
|
+
gl.useProgram(this.program);
|
|
1899
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
1900
|
+
gl.enableVertexAttribArray(this.position);
|
|
1901
|
+
gl.vertexAttribPointer(this.position, 2, gl.FLOAT, false, 0, 0);
|
|
1902
|
+
|
|
1903
|
+
gl.uniform2f(this.uniforms.resolution, this.canvas.width, this.canvas.height);
|
|
1904
|
+
gl.uniform1f(this.uniforms.time, time);
|
|
1905
|
+
gl.uniform1f(this.uniforms.progress, progress / 100);
|
|
1906
|
+
gl.uniform1f(this.uniforms.seed, this.seed);
|
|
1907
|
+
gl.uniform1f(this.uniforms.profile, this.profile);
|
|
1908
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1909
|
+
gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
|
|
1910
|
+
gl.uniform1i(this.uniforms.motion, 0);
|
|
1911
|
+
gl.uniform1f(this.uniforms.motionDuration, PROGRESS_MOTION_DURATION);
|
|
1912
|
+
gl.uniform1f(this.uniforms.motionScale, this.motionScale);
|
|
1913
|
+
|
|
1914
|
+
let hasEffect = this.effectUploaded ? 1 : 0;
|
|
1915
|
+
const effectWidth = effectImage && (effectImage.naturalWidth || effectImage.width || 0);
|
|
1916
|
+
if (!this.effectUploaded && effectImage && effectWidth > 0) {
|
|
1917
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1918
|
+
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
1919
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
1920
|
+
try {
|
|
1921
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, effectImage);
|
|
1922
|
+
this.effectUploaded = true;
|
|
1923
|
+
hasEffect = 1;
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
console.warn('[画境观屿] 参考纹理图集上传失败,继续使用程序化降级。', error);
|
|
1926
|
+
}
|
|
1927
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
1928
|
+
}
|
|
1929
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1930
|
+
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
1931
|
+
gl.uniform1i(this.uniforms.effect, 1);
|
|
1932
|
+
gl.uniform1f(this.uniforms.hasEffect, hasEffect);
|
|
1933
|
+
gl.uniform1f(this.uniforms.effectFrames, 24);
|
|
1934
|
+
|
|
1935
|
+
gl.uniform3fv(this.uniforms.dark, this.colors[0]);
|
|
1936
|
+
gl.uniform3fv(this.uniforms.accentA, this.colors[1]);
|
|
1937
|
+
gl.uniform3fv(this.uniforms.accentB, this.colors[2]);
|
|
1938
|
+
gl.uniform3fv(this.uniforms.glow, this.colors[3]);
|
|
1939
|
+
|
|
1940
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
setColors(colors) {
|
|
1944
|
+
this.colors = colors.map(hexToRgb01);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
dispose() {
|
|
1948
|
+
this.disposed = true;
|
|
1949
|
+
const gl = this.gl;
|
|
1950
|
+
this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
|
|
1951
|
+
this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
1952
|
+
gl.deleteBuffer(this.buffer);
|
|
1953
|
+
gl.deleteTexture(this.motionTexture);
|
|
1954
|
+
gl.deleteTexture(this.effectTexture);
|
|
1955
|
+
gl.deleteProgram(this.program);
|
|
1956
|
+
const lose = gl.getExtension('WEBGL_lose_context');
|
|
1957
|
+
if (lose) lose.loseContext();
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function createProgressFlowRenderer(canvas, preset, options = {}) {
|
|
1962
|
+
try {
|
|
1963
|
+
return new ProgressFlowRenderer(canvas, preset, options);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
console.warn('[画境观屿] 进度流体 WebGL2 不可用,使用 Canvas 2D 降级。', error);
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const PROGRESS_REFERENCE_DURATION = 12;
|
|
1971
|
+
const PROGRESS_REFERENCE_FRAME_COUNT = 24;
|
|
1972
|
+
|
|
1973
|
+
const FRAME_WIDTH = 64;
|
|
1974
|
+
const FRAME_HEIGHT = 32;
|
|
1975
|
+
const CACHE = Object.create(null);
|
|
1976
|
+
|
|
1977
|
+
const PALETTES = {
|
|
1978
|
+
'model-training': ['#20131f', '#ff3f94', '#ff8a3d', '#fff06a'],
|
|
1979
|
+
'agent-migration': ['#111a31', '#245bff', '#00cfff', '#5dffe6'],
|
|
1980
|
+
'visual-training': ['#1f172b', '#7042ff', '#42f58d', '#c4ff8a'],
|
|
1981
|
+
'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff']
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
|
|
1985
|
+
context.save();
|
|
1986
|
+
context.translate(x, y);
|
|
1987
|
+
context.scale(1, radiusY / radiusX);
|
|
1988
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
1989
|
+
const alphaHex = (value) => {
|
|
1990
|
+
const text = Math.round(value).toString(16);
|
|
1725
1991
|
return text.length === 1 ? `0${text}` : text;
|
|
1726
1992
|
};
|
|
1727
1993
|
gradient.addColorStop(0, `${color}${alphaHex(alpha * 255)}`);
|
|
1728
1994
|
gradient.addColorStop(0.46, `${color}${alphaHex(alpha * 0.42 * 255)}`);
|
|
1729
|
-
gradient.addColorStop(1, `${color}00`);
|
|
1730
|
-
context.fillStyle = gradient;
|
|
1731
|
-
context.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
1732
|
-
context.restore();
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
function createAtlas(id) {
|
|
1736
|
-
const palette = PALETTES[id] || PALETTES['visual-training'];
|
|
1737
|
-
const canvas = document.createElement('canvas');
|
|
1738
|
-
canvas.width = FRAME_WIDTH * PROGRESS_REFERENCE_FRAME_COUNT;
|
|
1739
|
-
canvas.height = FRAME_HEIGHT;
|
|
1740
|
-
const context = canvas.getContext('2d');
|
|
1741
|
-
|
|
1742
|
-
for (let frame = 0; frame < PROGRESS_REFERENCE_FRAME_COUNT; frame += 1) {
|
|
1743
|
-
const phase = (frame / PROGRESS_REFERENCE_FRAME_COUNT) * Math.PI * 2;
|
|
1744
|
-
const left = frame * FRAME_WIDTH;
|
|
1745
|
-
context.save();
|
|
1746
|
-
context.translate(left, 0);
|
|
1747
|
-
context.fillStyle = palette[0];
|
|
1748
|
-
context.fillRect(0, 0, FRAME_WIDTH, FRAME_HEIGHT);
|
|
1749
|
-
context.globalCompositeOperation = 'screen';
|
|
1750
|
-
|
|
1751
|
-
drawCloud(context, 35 + Math.sin(phase * 0.83) * 5, 10 + Math.cos(phase * 0.61) * 5, 27, 18, palette[1], id === 'agent-migration' ? 0.32 : 0.42);
|
|
1752
|
-
drawCloud(context, 45 + Math.cos(phase * 0.72) * 4, 23 + Math.sin(phase * 0.54) * 4, 22, 15, palette[2], 0.44);
|
|
1753
|
-
drawCloud(context, 53 + Math.sin(phase * 1.07) * 2, 16 + Math.cos(phase * 0.89) * 6, 12, 13, palette[3], id === 'model-training' ? 0.30 : 0.20);
|
|
1754
|
-
|
|
1755
|
-
context.globalCompositeOperation = 'source-over';
|
|
1756
|
-
const trough = context.createRadialGradient(41, 16, 1, 41, 16, 17);
|
|
1757
|
-
trough.addColorStop(0, 'rgba(5,6,11,0.64)');
|
|
1758
|
-
trough.addColorStop(0.58, 'rgba(6,7,12,0.26)');
|
|
1759
|
-
trough.addColorStop(1, 'rgba(6,7,12,0)');
|
|
1760
|
-
context.fillStyle = trough;
|
|
1761
|
-
context.fillRect(20, 0, 44, FRAME_HEIGHT);
|
|
1762
|
-
context.restore();
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
* @param {
|
|
1783
|
-
* @
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1995
|
+
gradient.addColorStop(1, `${color}00`);
|
|
1996
|
+
context.fillStyle = gradient;
|
|
1997
|
+
context.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
1998
|
+
context.restore();
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function createAtlas(id) {
|
|
2002
|
+
const palette = PALETTES[id] || PALETTES['visual-training'];
|
|
2003
|
+
const canvas = document.createElement('canvas');
|
|
2004
|
+
canvas.width = FRAME_WIDTH * PROGRESS_REFERENCE_FRAME_COUNT;
|
|
2005
|
+
canvas.height = FRAME_HEIGHT;
|
|
2006
|
+
const context = canvas.getContext('2d');
|
|
2007
|
+
|
|
2008
|
+
for (let frame = 0; frame < PROGRESS_REFERENCE_FRAME_COUNT; frame += 1) {
|
|
2009
|
+
const phase = (frame / PROGRESS_REFERENCE_FRAME_COUNT) * Math.PI * 2;
|
|
2010
|
+
const left = frame * FRAME_WIDTH;
|
|
2011
|
+
context.save();
|
|
2012
|
+
context.translate(left, 0);
|
|
2013
|
+
context.fillStyle = palette[0];
|
|
2014
|
+
context.fillRect(0, 0, FRAME_WIDTH, FRAME_HEIGHT);
|
|
2015
|
+
context.globalCompositeOperation = 'screen';
|
|
2016
|
+
|
|
2017
|
+
drawCloud(context, 35 + Math.sin(phase * 0.83) * 5, 10 + Math.cos(phase * 0.61) * 5, 27, 18, palette[1], id === 'agent-migration' ? 0.32 : 0.42);
|
|
2018
|
+
drawCloud(context, 45 + Math.cos(phase * 0.72) * 4, 23 + Math.sin(phase * 0.54) * 4, 22, 15, palette[2], 0.44);
|
|
2019
|
+
drawCloud(context, 53 + Math.sin(phase * 1.07) * 2, 16 + Math.cos(phase * 0.89) * 6, 12, 13, palette[3], id === 'model-training' ? 0.30 : 0.20);
|
|
2020
|
+
|
|
2021
|
+
context.globalCompositeOperation = 'source-over';
|
|
2022
|
+
const trough = context.createRadialGradient(41, 16, 1, 41, 16, 17);
|
|
2023
|
+
trough.addColorStop(0, 'rgba(5,6,11,0.64)');
|
|
2024
|
+
trough.addColorStop(0.58, 'rgba(6,7,12,0.26)');
|
|
2025
|
+
trough.addColorStop(1, 'rgba(6,7,12,0)');
|
|
2026
|
+
context.fillStyle = trough;
|
|
2027
|
+
context.fillRect(20, 0, 44, FRAME_HEIGHT);
|
|
2028
|
+
context.restore();
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// A canvas is a valid TexImageSource. Keeping it directly avoids a PNG
|
|
2032
|
+
// encode -> base64 allocation -> Image decode round trip at startup.
|
|
2033
|
+
return { image: canvas, ready: true };
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function getProgressReferenceAtlas(id) {
|
|
2037
|
+
if (!CACHE[id]) CACHE[id] = createAtlas(id);
|
|
2038
|
+
return CACHE[id];
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* Attach a WebGL2 fluid overlay to a progress capsule root.
|
|
2043
|
+
*
|
|
2044
|
+
* @param {object} params
|
|
2045
|
+
* @param {HTMLElement} params.root progress capsule root element
|
|
2046
|
+
* @param {HTMLCanvasElement} params.canvas 2D fallback canvas (kept beneath)
|
|
2047
|
+
* @param {object} params.preset progress preset
|
|
2048
|
+
* @param {() => number} params.getProgress reads the current progress value
|
|
2049
|
+
* @returns {{ update(flowTime: number): void, setDprCap(cap: number): void, dispose(): void } | null}
|
|
2050
|
+
*/
|
|
2051
|
+
function attachProgressFlowOverlay({
|
|
2052
|
+
root,
|
|
2053
|
+
canvas,
|
|
2054
|
+
preset,
|
|
2055
|
+
getProgress,
|
|
2056
|
+
dprCap = 2,
|
|
2057
|
+
powerPreference = 'high-performance',
|
|
2058
|
+
onContextLost,
|
|
2059
|
+
onContextRestored
|
|
2060
|
+
}) {
|
|
2061
|
+
const overlay = document.createElement('canvas');
|
|
2062
|
+
overlay.className = 'hj-progress-canvas hj-progress-overlay';
|
|
2063
|
+
overlay.setAttribute('aria-hidden', 'true');
|
|
2064
|
+
canvas.insertAdjacentElement('afterend', overlay);
|
|
2065
|
+
|
|
2066
|
+
const renderer = createProgressFlowRenderer(overlay, preset, {
|
|
2067
|
+
powerPreference,
|
|
2068
|
+
onContextLost,
|
|
2069
|
+
onContextRestored
|
|
2070
|
+
});
|
|
2071
|
+
if (!renderer) {
|
|
2072
|
+
overlay.remove();
|
|
2073
|
+
return null;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const atlas = getProgressReferenceAtlas(preset.id);
|
|
2077
|
+
root.classList.add('has-webgl-progress');
|
|
2078
|
+
|
|
2079
|
+
const resize = () => {
|
|
2080
|
+
const bounds = root.getBoundingClientRect();
|
|
2081
|
+
const width = Math.max(1, bounds.width);
|
|
2082
|
+
const height = Math.max(1, bounds.height);
|
|
2083
|
+
const dpr = Math.min(window.devicePixelRatio || 1, dprCap);
|
|
2084
|
+
overlay.style.width = `${width}px`;
|
|
2085
|
+
overlay.style.height = `${height}px`;
|
|
2086
|
+
renderer.resize(width, height, dpr);
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
let resizeObserver = null;
|
|
2090
|
+
let onWindowResize = null;
|
|
2091
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
2092
|
+
resizeObserver = new ResizeObserver(resize);
|
|
2093
|
+
resizeObserver.observe(root);
|
|
2094
|
+
} else {
|
|
2095
|
+
onWindowResize = resize;
|
|
2096
|
+
window.addEventListener('resize', onWindowResize);
|
|
2097
|
+
}
|
|
2098
|
+
resize();
|
|
2099
|
+
|
|
2100
|
+
return {
|
|
2101
|
+
update(flowTime) {
|
|
2102
|
+
const effectTime = flowTime % PROGRESS_REFERENCE_DURATION;
|
|
2103
|
+
const progress = getProgress();
|
|
2104
|
+
renderer.draw(
|
|
2105
|
+
effectTime,
|
|
2106
|
+
Number.isFinite(progress) ? progress : preset.initialProgress,
|
|
2107
|
+
atlas.ready ? atlas.image : null
|
|
2108
|
+
);
|
|
2109
|
+
},
|
|
2110
|
+
setColors(colors) {
|
|
2111
|
+
renderer.setColors(colors);
|
|
2112
|
+
},
|
|
2113
|
+
setDprCap(cap) {
|
|
2114
|
+
dprCap = cap;
|
|
2115
|
+
resize();
|
|
2116
|
+
},
|
|
2117
|
+
dispose() {
|
|
2118
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
2119
|
+
if (onWindowResize) window.removeEventListener('resize', onWindowResize);
|
|
2120
|
+
renderer.dispose();
|
|
2121
|
+
overlay.remove();
|
|
2122
|
+
root.classList.remove('has-webgl-progress');
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function normalizeRange(min, max, fallbackMin = 0, fallbackMax = 100) {
|
|
2128
|
+
let nextMin = toFiniteNumber(min);
|
|
2129
|
+
let nextMax = toFiniteNumber(max);
|
|
2130
|
+
if (nextMin === null) nextMin = fallbackMin;
|
|
2131
|
+
if (nextMax === null) nextMax = fallbackMax;
|
|
2132
|
+
if (nextMin > nextMax) {
|
|
2133
|
+
const swap = nextMin;
|
|
2134
|
+
nextMin = nextMax;
|
|
2135
|
+
nextMax = swap;
|
|
2136
|
+
}
|
|
2137
|
+
return [nextMin, nextMax];
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
function clampProgressValue(value, min, max) {
|
|
2141
|
+
const number = toFiniteNumber(value);
|
|
2142
|
+
if (number === null) return null;
|
|
2143
|
+
return Math.min(Math.max(number, min), max);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function normalizeProgressStep(step, fallback = 'any') {
|
|
2147
|
+
if (step === 'any' || step == null) return 'any';
|
|
2148
|
+
const increment = toFiniteNumber(step);
|
|
2149
|
+
return increment !== null && increment > 0 ? increment : fallback;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function snapProgressValue(value, min, max, step = 'any') {
|
|
2153
|
+
const clamped = clampProgressValue(value, min, max);
|
|
2154
|
+
if (clamped === null) return null;
|
|
2155
|
+
if (step === 'any' || step == null) return clamped;
|
|
2156
|
+
const increment = normalizeProgressStep(step);
|
|
2157
|
+
if (increment === 'any') return clamped;
|
|
2158
|
+
const snapped = min + Math.round((clamped - min) / increment) * increment;
|
|
2159
|
+
return Math.min(Math.max(Number(snapped.toFixed(12)), min), max);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function progressRatio(value, min, max) {
|
|
2163
|
+
const range = max - min;
|
|
2164
|
+
if (!Number.isFinite(range) || range <= 0) return 0;
|
|
2165
|
+
return Math.min(Math.max((value - min) / range, 0), 1);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
2169
|
+
const SUPPORTS_CTX_FILTER = typeof CanvasRenderingContext2D !== 'undefined' && 'filter' in CanvasRenderingContext2D.prototype;
|
|
2170
|
+
|
|
2171
|
+
function stringSeed(value) {
|
|
2172
|
+
let hash = 2166136261;
|
|
2173
|
+
for (const character of value) {
|
|
2174
|
+
hash ^= character.charCodeAt(0);
|
|
2175
|
+
hash = Math.imul(hash, 16777619);
|
|
2176
|
+
}
|
|
2177
|
+
return (hash >>> 0) / 4294967295;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
function interpolate(template, values) {
|
|
2181
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => (values[key] !== undefined ? values[key] : ''));
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
const FLOW_PROFILES = {
|
|
2185
|
+
'model-training': {
|
|
2186
|
+
cycles: [0.98, 3.05, 5.45],
|
|
2187
|
+
amplitudes: [11.2, 11.0, 5.2],
|
|
2188
|
+
speeds: [0.31, -0.53, 0.78],
|
|
2189
|
+
bulgeAmplitude: 8.2,
|
|
2190
|
+
timeScale: 1.0,
|
|
2191
|
+
glowWidth: 5.8,
|
|
2192
|
+
haloWidth: 20,
|
|
2193
|
+
whiteAlpha: 0.72,
|
|
2194
|
+
whiteWidth: 1.15,
|
|
2195
|
+
cloudWidth: 0.17,
|
|
2196
|
+
autoRange: [25, 66]
|
|
2197
|
+
},
|
|
2198
|
+
'agent-migration': {
|
|
2199
|
+
cycles: [0.62, 1.55, 2.95],
|
|
2200
|
+
amplitudes: [16.0, 7.8, 2.3],
|
|
2201
|
+
speeds: [0.25, -0.4, 0.60],
|
|
2202
|
+
bulgeAmplitude: 8.4,
|
|
2203
|
+
timeScale: 0.82,
|
|
2204
|
+
glowWidth: 6.3,
|
|
2205
|
+
haloWidth: 21,
|
|
2206
|
+
whiteAlpha: 0.18,
|
|
2207
|
+
whiteWidth: 0.45,
|
|
2208
|
+
cloudWidth: 0.18,
|
|
2209
|
+
autoRange: [24, 62]
|
|
2210
|
+
},
|
|
2211
|
+
'visual-training': {
|
|
2212
|
+
cycles: [0.88, 2.45, 4.35],
|
|
2213
|
+
amplitudes: [12.8, 10.2, 4.3],
|
|
2214
|
+
speeds: [0.28, -0.47, 0.69],
|
|
2215
|
+
bulgeAmplitude: 7.3,
|
|
2216
|
+
timeScale: 0.91,
|
|
2217
|
+
glowWidth: 6.0,
|
|
2218
|
+
haloWidth: 21,
|
|
2219
|
+
whiteAlpha: 0.30,
|
|
2220
|
+
whiteWidth: 0.55,
|
|
2221
|
+
cloudWidth: 0.175,
|
|
2222
|
+
autoRange: [20, 75]
|
|
2223
|
+
},
|
|
2224
|
+
// ponytail: first-pass tide = asymmetric surge on the 2D fallback path.
|
|
2225
|
+
// Tune surge/amplitudes after visual QA against the WebGL overlay.
|
|
2226
|
+
'tide': {
|
|
2227
|
+
cycles: [0.72, 1.9, 4.2],
|
|
2228
|
+
amplitudes: [19.0, 6.5, 1.5],
|
|
2229
|
+
speeds: [0.42, -0.5, 0.66],
|
|
2230
|
+
bulgeAmplitude: 9.5,
|
|
2231
|
+
timeScale: 0.85,
|
|
2232
|
+
glowWidth: 6.2,
|
|
2233
|
+
haloWidth: 21,
|
|
2234
|
+
whiteAlpha: 0.5,
|
|
2235
|
+
whiteWidth: 1.0,
|
|
2236
|
+
cloudWidth: 0.18,
|
|
2237
|
+
autoRange: [18, 70],
|
|
2238
|
+
surge: 0.55
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
|
|
2242
|
+
class ProgressCapsuleController {
|
|
1927
2243
|
constructor({ root, canvas, valueElement, preset, emitter, options, copy, dirty }) {
|
|
1928
|
-
this.root = root;
|
|
1929
|
-
this.canvas = canvas;
|
|
1930
|
-
this.valueElement = valueElement;
|
|
1931
|
-
this.preset = preset;
|
|
1932
|
-
this.emitter = emitter;
|
|
2244
|
+
this.root = root;
|
|
2245
|
+
this.canvas = canvas;
|
|
2246
|
+
this.valueElement = valueElement;
|
|
2247
|
+
this.preset = preset;
|
|
2248
|
+
this.emitter = emitter;
|
|
1933
2249
|
this.options = options;
|
|
1934
2250
|
this.copy = copy;
|
|
1935
2251
|
this.dirty = dirty;
|
|
1936
2252
|
this.valueSuffix = options.valueSuffix ?? copy.valueSuffix;
|
|
1937
|
-
this.profile = preset.edgeStyle === 'tide'
|
|
1938
|
-
? FLOW_PROFILES.tide
|
|
1939
|
-
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
1940
|
-
this.min = options.min
|
|
1941
|
-
this.
|
|
1942
|
-
this.
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
this.
|
|
1946
|
-
this.
|
|
1947
|
-
this.
|
|
1948
|
-
this.
|
|
1949
|
-
this.
|
|
1950
|
-
this.
|
|
1951
|
-
this.
|
|
1952
|
-
this.
|
|
1953
|
-
this.
|
|
1954
|
-
|
|
1955
|
-
this.
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
this.
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
this.
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
this.
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2253
|
+
this.profile = preset.edgeStyle === 'tide'
|
|
2254
|
+
? FLOW_PROFILES.tide
|
|
2255
|
+
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
2256
|
+
[this.min, this.max] = normalizeRange(options.min, options.max);
|
|
2257
|
+
this.step = normalizeProgressStep(options.step);
|
|
2258
|
+
this.precision = Number.isInteger(Number(options.precision))
|
|
2259
|
+
? clamp(Number(options.precision), 0, 20)
|
|
2260
|
+
: 0;
|
|
2261
|
+
this.formatValue = typeof options.formatValue === 'function' ? options.formatValue : null;
|
|
2262
|
+
this.direction = options.direction === 'rtl' ? 'rtl' : 'ltr';
|
|
2263
|
+
this.dprCap = effectiveDprCap(options.quality, options.renderScale);
|
|
2264
|
+
this.ctx = canvas.getContext('2d');
|
|
2265
|
+
this.value = snapProgressValue(options.value ?? preset.initialProgress, this.min, this.max, this.step);
|
|
2266
|
+
if (this.value === null) this.value = clamp(preset.initialProgress, this.min, this.max);
|
|
2267
|
+
this.dragging = false;
|
|
2268
|
+
this.webglActive = false;
|
|
2269
|
+
this.flowTime = stringSeed(preset.id) * 31;
|
|
2270
|
+
this.seed = stringSeed(`${preset.id}-reference`) * Math.PI * 2;
|
|
2271
|
+
this.randomState = Math.floor(stringSeed(`${preset.id}-auto`) * 0x7fffffff) || 1;
|
|
2272
|
+
this.dpr = 1;
|
|
2273
|
+
this.width = 0;
|
|
2274
|
+
this.height = 0;
|
|
2275
|
+
this.handlers = {};
|
|
2276
|
+
|
|
2277
|
+
this.onResize = () => {
|
|
2278
|
+
this.resizeCanvas();
|
|
2279
|
+
if (typeof this.options.onResize === 'function') this.options.onResize();
|
|
2280
|
+
};
|
|
2281
|
+
this.resizeObserver = typeof ResizeObserver !== 'undefined'
|
|
2282
|
+
? new ResizeObserver(this.onResize)
|
|
2283
|
+
: null;
|
|
2284
|
+
if (this.resizeObserver) this.resizeObserver.observe(this.root);
|
|
2285
|
+
else window.addEventListener('resize', this.onResize);
|
|
2286
|
+
|
|
2287
|
+
this.suppressEvents = true;
|
|
2288
|
+
this.setProgress(this.value, 'init');
|
|
2289
|
+
this.suppressEvents = false;
|
|
2290
|
+
this.bindEvents();
|
|
2291
|
+
this.resizeCanvas();
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
random() {
|
|
2295
|
+
this.randomState = (Math.imul(this.randomState, 1664525) + 1013904223) >>> 0;
|
|
2296
|
+
return this.randomState / 4294967296;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
displayText() {
|
|
2300
|
+
if (this.formatValue) {
|
|
2301
|
+
return String(this.formatValue(this.value, {
|
|
2302
|
+
min: this.min,
|
|
2303
|
+
max: this.max,
|
|
2304
|
+
suffix: this.valueSuffix
|
|
2305
|
+
}));
|
|
2306
|
+
}
|
|
2307
|
+
const number = this.precision > 0 ? this.value.toFixed(this.precision) : String(Math.round(this.value));
|
|
2308
|
+
return `${number}${this.valueSuffix}`;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
syncValueDom() {
|
|
2312
|
+
const text = this.displayText();
|
|
2313
|
+
this.root.style.setProperty('--progress', this.value.toFixed(2));
|
|
2314
|
+
this.root.style.setProperty('--progress-ratio', progressRatio(this.value, this.min, this.max).toFixed(4));
|
|
2315
|
+
this.root.setAttribute('aria-valuenow', String(this.value));
|
|
2316
|
+
this.root.setAttribute('aria-valuetext', text);
|
|
2317
|
+
if (this.valueElement) this.valueElement.textContent = text;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
setProgress(nextValue, source = 'auto') {
|
|
2321
|
+
const next = snapProgressValue(nextValue, this.min, this.max, this.step);
|
|
2322
|
+
if (next === null) return false;
|
|
2323
|
+
this.value = next;
|
|
2324
|
+
this.syncValueDom();
|
|
1980
2325
|
if (!this.suppressEvents) {
|
|
1981
2326
|
if (this.dirty) this.dirty.value = true;
|
|
1982
2327
|
this.emitter.emit('change', { value: this.value, source });
|
|
1983
2328
|
}
|
|
2329
|
+
return true;
|
|
1984
2330
|
}
|
|
1985
2331
|
|
|
1986
2332
|
setValueSuffix(suffix) {
|
|
2333
|
+
if (this.dirty) this.dirty.valueSuffix = true;
|
|
1987
2334
|
this.valueSuffix = String(suffix == null ? '' : suffix);
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2335
|
+
this.syncValueDom();
|
|
2336
|
+
return this;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
setStep(step) {
|
|
2340
|
+
const nextStep = normalizeProgressStep(step, this.step);
|
|
2341
|
+
if (nextStep === this.step) return this;
|
|
2342
|
+
if (this.dirty) this.dirty.step = true;
|
|
2343
|
+
this.step = nextStep;
|
|
2344
|
+
const next = snapProgressValue(this.value, this.min, this.max, this.step);
|
|
2345
|
+
if (next !== null && next !== this.value) this.setProgress(next, 'prop');
|
|
2346
|
+
return this;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
setPrecision(precision) {
|
|
2350
|
+
const value = Number(precision);
|
|
2351
|
+
if (!Number.isInteger(value) || value < 0 || value > 20) return this;
|
|
2352
|
+
if (this.dirty) this.dirty.precision = true;
|
|
2353
|
+
this.precision = value;
|
|
2354
|
+
this.syncValueDom();
|
|
2355
|
+
return this;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
setFormatValue(formatter) {
|
|
2359
|
+
if (formatter != null && typeof formatter !== 'function') return this;
|
|
2360
|
+
if (this.dirty) this.dirty.formatValue = true;
|
|
2361
|
+
this.formatValue = formatter || null;
|
|
2362
|
+
this.syncValueDom();
|
|
2363
|
+
return this;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
setDirection(direction) {
|
|
2367
|
+
if (this.dirty) this.dirty.direction = true;
|
|
2368
|
+
this.direction = direction === 'rtl' ? 'rtl' : 'ltr';
|
|
2369
|
+
this.root.dataset.direction = this.direction;
|
|
1991
2370
|
return this;
|
|
1992
2371
|
}
|
|
1993
2372
|
|
|
1994
2373
|
setShowValue(show) {
|
|
2374
|
+
if (this.dirty) this.dirty.showValue = true;
|
|
1995
2375
|
if (this.valueElement) this.valueElement.hidden = show === false;
|
|
1996
2376
|
return this;
|
|
1997
2377
|
}
|
|
1998
|
-
|
|
1999
|
-
resizeCanvas() {
|
|
2000
|
-
const bounds = this.root.getBoundingClientRect();
|
|
2001
|
-
this.dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
|
|
2002
|
-
this.width = Math.max(1, bounds.width);
|
|
2003
|
-
this.height = Math.max(1, bounds.height);
|
|
2004
|
-
this.canvas.width = Math.round(this.width * this.dpr);
|
|
2005
|
-
this.canvas.height = Math.round(this.height * this.dpr);
|
|
2006
|
-
this.canvas.style.width = `${this.width}px`;
|
|
2007
|
-
this.canvas.style.height = `${this.height}px`;
|
|
2008
|
-
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
edgeEnvelope(yRatio) {
|
|
2012
|
-
const edge = Math.sin(Math.PI * clamp(yRatio, 0, 1));
|
|
2013
|
-
return Math.pow(Math.max(edge, 0), 0.48);
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
localBulge(yRatio, time, index) {
|
|
2017
|
-
const direction = index === 0 ? 1 : -1;
|
|
2018
|
-
const center = 0.28 + index * 0.40 + Math.sin(time * (0.19 + index * 0.035) + this.seed * (1.1 + index)) * 0.13;
|
|
2019
|
-
const width = 0.075 + index * 0.016 + Math.sin(time * 0.13 + this.seed * 2.1) * 0.012;
|
|
2020
|
-
const distance = (yRatio - center) / Math.max(width, 0.035);
|
|
2021
|
-
const gaussian = Math.exp(-0.5 * distance * distance);
|
|
2022
|
-
return gaussian * Math.sin(time * (0.71 + index * 0.09) + this.seed * (2.7 + index)) * this.profile.bulgeAmplitude * direction;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
edgeOffset(y, time, phase = 0, amplitudeScale = 1) {
|
|
2026
|
-
const yRatio = this.height > 0 ? y / this.height : 0;
|
|
2027
|
-
const envelope = this.edgeEnvelope(yRatio);
|
|
2028
|
-
const scaledTime = time * this.profile.timeScale;
|
|
2029
|
-
const phaseTime = this.profile.surge
|
|
2030
|
-
? scaledTime + this.profile.surge * Math.sin(scaledTime * 2)
|
|
2031
|
-
: scaledTime;
|
|
2032
|
-
let offset = 0;
|
|
2033
|
-
|
|
2034
|
-
for (let index = 0; index < this.profile.cycles.length; index += 1) {
|
|
2035
|
-
const cycle = this.profile.cycles[index];
|
|
2036
|
-
const amplitude = this.profile.amplitudes[index];
|
|
2037
|
-
const speed = this.profile.speeds[index];
|
|
2038
|
-
const amplitudeMotion = 0.74 + 0.26 * Math.sin(
|
|
2039
|
-
phaseTime * (0.17 + index * 0.045) + this.seed * (index + 2.4)
|
|
2040
|
-
);
|
|
2041
|
-
offset += Math.sin(
|
|
2042
|
-
yRatio * Math.PI * 2 * cycle + phaseTime * speed * Math.PI * 2 + this.seed * (index + 1) + phase
|
|
2043
|
-
) * amplitude * amplitudeMotion;
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
offset += this.localBulge(yRatio, phaseTime + phase, 0);
|
|
2047
|
-
offset += this.localBulge(yRatio, phaseTime - phase * 0.7, 1);
|
|
2048
|
-
return offset * envelope * amplitudeScale;
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
createEdgePath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
|
|
2052
|
-
const step = Math.max(1.8, this.height / 92);
|
|
2053
|
-
ctx.beginPath();
|
|
2054
|
-
for (let y = 0; y <= this.height + step; y += step) {
|
|
2055
|
-
const x = baseX + this.edgeOffset(y, time, phase, amplitudeScale);
|
|
2056
|
-
if (y === 0) ctx.moveTo(x, y);
|
|
2057
|
-
else ctx.lineTo(x, y);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
createFillPath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
|
|
2062
|
-
const step = Math.max(1.8, this.height / 92);
|
|
2063
|
-
ctx.beginPath();
|
|
2064
|
-
ctx.moveTo(0, 0);
|
|
2065
|
-
ctx.lineTo(baseX + this.edgeOffset(0, time, phase, amplitudeScale), 0);
|
|
2066
|
-
for (let y = step; y <= this.height + step; y += step) {
|
|
2067
|
-
ctx.lineTo(baseX + this.edgeOffset(y, time, phase, amplitudeScale), y);
|
|
2068
|
-
}
|
|
2069
|
-
ctx.lineTo(0, this.height);
|
|
2070
|
-
ctx.closePath();
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
drawEllipticalGlow(x, y, radiusX, radiusY, color, alpha) {
|
|
2074
|
-
const ctx = this.ctx;
|
|
2075
|
-
ctx.save();
|
|
2076
|
-
ctx.translate(x, y);
|
|
2077
|
-
ctx.scale(1, radiusY / radiusX);
|
|
2078
|
-
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
2079
|
-
gradient.addColorStop(0, hexToRgba(color, alpha));
|
|
2080
|
-
gradient.addColorStop(0.42, hexToRgba(color, alpha * 0.48));
|
|
2081
|
-
gradient.addColorStop(1, hexToRgba(color, 0));
|
|
2082
|
-
ctx.globalCompositeOperation = 'screen';
|
|
2083
|
-
ctx.fillStyle = gradient;
|
|
2084
|
-
ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
2085
|
-
ctx.restore();
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
drawDarkEllipticalShadow(x, y, radiusX, radiusY, alpha) {
|
|
2089
|
-
const ctx = this.ctx;
|
|
2090
|
-
ctx.save();
|
|
2091
|
-
ctx.translate(x, y);
|
|
2092
|
-
ctx.scale(1, radiusY / radiusX);
|
|
2093
|
-
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
2094
|
-
gradient.addColorStop(0, `rgba(6, 6, 11, ${alpha})`);
|
|
2095
|
-
gradient.addColorStop(0.54, `rgba(8, 8, 14, ${alpha * 0.62})`);
|
|
2096
|
-
gradient.addColorStop(1, 'rgba(8, 8, 14, 0)');
|
|
2097
|
-
ctx.globalCompositeOperation = 'source-over';
|
|
2098
|
-
ctx.fillStyle = gradient;
|
|
2099
|
-
ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
2100
|
-
ctx.restore();
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
drawColorClouds(shoreline, time, accentA, accentB, glow) {
|
|
2104
|
-
this.ctx;
|
|
2105
|
-
const width = this.width;
|
|
2106
|
-
const height = this.height;
|
|
2107
|
-
const scale = this.profile.cloudWidth;
|
|
2108
|
-
const t = time * this.profile.timeScale;
|
|
2109
|
-
|
|
2110
|
-
const upperY = height * (0.28 + Math.sin(t * 0.24 + this.seed) * 0.13);
|
|
2111
|
-
const lowerY = height * (0.70 + Math.cos(t * 0.21 + this.seed * 1.7) * 0.12);
|
|
2112
|
-
const middleY = height * (0.49 + Math.sin(t * 0.31 + this.seed * 2.3) * 0.15);
|
|
2113
|
-
|
|
2114
|
-
const farX = shoreline - width * 0.095;
|
|
2115
|
-
const farRx = Math.max(52, width * scale);
|
|
2116
|
-
const farRy = height * 0.42;
|
|
2117
|
-
this.drawEllipticalGlow(farX, upperY, farRx, farRy, accentA, 0.38);
|
|
2118
|
-
this.drawDarkEllipticalShadow(
|
|
2119
|
-
farX + farRx * 0.16,
|
|
2120
|
-
upperY,
|
|
2121
|
-
farRx * 0.58,
|
|
2122
|
-
farRy * 0.66,
|
|
2123
|
-
0.74
|
|
2124
|
-
);
|
|
2125
|
-
|
|
2126
|
-
const lowerX = shoreline - width * 0.072;
|
|
2127
|
-
const lowerRx = Math.max(44, width * scale * 0.82);
|
|
2128
|
-
const lowerRy = height * 0.36;
|
|
2129
|
-
this.drawEllipticalGlow(lowerX, lowerY, lowerRx, lowerRy, accentB, 0.31);
|
|
2130
|
-
this.drawDarkEllipticalShadow(
|
|
2131
|
-
lowerX + lowerRx * 0.14,
|
|
2132
|
-
lowerY,
|
|
2133
|
-
lowerRx * 0.54,
|
|
2134
|
-
lowerRy * 0.62,
|
|
2135
|
-
0.64
|
|
2136
|
-
);
|
|
2137
|
-
|
|
2138
|
-
this.drawEllipticalGlow(
|
|
2139
|
-
shoreline - width * 0.034,
|
|
2140
|
-
middleY,
|
|
2141
|
-
Math.max(30, width * scale * 0.48),
|
|
2142
|
-
height * 0.27,
|
|
2143
|
-
glow,
|
|
2144
|
-
0.20
|
|
2145
|
-
);
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
drawPathBand({ baseX, time, phase, amplitudeScale, color, alpha, blur, width, composite = 'screen' }) {
|
|
2149
|
-
const ctx = this.ctx;
|
|
2150
|
-
this.createEdgePath(ctx, baseX, time, phase, amplitudeScale);
|
|
2151
|
-
ctx.save();
|
|
2152
|
-
ctx.globalCompositeOperation = composite;
|
|
2153
|
-
ctx.globalAlpha = alpha;
|
|
2154
|
-
// Safari < 18 and some old WebViews ignore ctx.filter; setting it is a
|
|
2155
|
-
// no-op there, so only assign when supported to keep intent explicit.
|
|
2156
|
-
if (SUPPORTS_CTX_FILTER) ctx.filter = `blur(${blur}px)`;
|
|
2157
|
-
ctx.strokeStyle = color;
|
|
2158
|
-
ctx.lineWidth = width;
|
|
2159
|
-
ctx.stroke();
|
|
2160
|
-
ctx.restore();
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
setRange(min, max) {
|
|
2164
|
-
if (min > max) {
|
|
2165
|
-
const swap = min;
|
|
2166
|
-
min = max;
|
|
2167
|
-
max = swap;
|
|
2378
|
+
|
|
2379
|
+
resizeCanvas() {
|
|
2380
|
+
const bounds = this.root.getBoundingClientRect();
|
|
2381
|
+
this.dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
|
|
2382
|
+
this.width = Math.max(1, bounds.width);
|
|
2383
|
+
this.height = Math.max(1, bounds.height);
|
|
2384
|
+
this.canvas.width = Math.round(this.width * this.dpr);
|
|
2385
|
+
this.canvas.height = Math.round(this.height * this.dpr);
|
|
2386
|
+
this.canvas.style.width = `${this.width}px`;
|
|
2387
|
+
this.canvas.style.height = `${this.height}px`;
|
|
2388
|
+
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
edgeEnvelope(yRatio) {
|
|
2392
|
+
const edge = Math.sin(Math.PI * clamp(yRatio, 0, 1));
|
|
2393
|
+
return Math.pow(Math.max(edge, 0), 0.48);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
localBulge(yRatio, time, index) {
|
|
2397
|
+
const direction = index === 0 ? 1 : -1;
|
|
2398
|
+
const center = 0.28 + index * 0.40 + Math.sin(time * (0.19 + index * 0.035) + this.seed * (1.1 + index)) * 0.13;
|
|
2399
|
+
const width = 0.075 + index * 0.016 + Math.sin(time * 0.13 + this.seed * 2.1) * 0.012;
|
|
2400
|
+
const distance = (yRatio - center) / Math.max(width, 0.035);
|
|
2401
|
+
const gaussian = Math.exp(-0.5 * distance * distance);
|
|
2402
|
+
return gaussian * Math.sin(time * (0.71 + index * 0.09) + this.seed * (2.7 + index)) * this.profile.bulgeAmplitude * direction;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
edgeOffset(y, time, phase = 0, amplitudeScale = 1) {
|
|
2406
|
+
const yRatio = this.height > 0 ? y / this.height : 0;
|
|
2407
|
+
const envelope = this.edgeEnvelope(yRatio);
|
|
2408
|
+
const scaledTime = time * this.profile.timeScale;
|
|
2409
|
+
const phaseTime = this.profile.surge
|
|
2410
|
+
? scaledTime + this.profile.surge * Math.sin(scaledTime * 2)
|
|
2411
|
+
: scaledTime;
|
|
2412
|
+
let offset = 0;
|
|
2413
|
+
|
|
2414
|
+
for (let index = 0; index < this.profile.cycles.length; index += 1) {
|
|
2415
|
+
const cycle = this.profile.cycles[index];
|
|
2416
|
+
const amplitude = this.profile.amplitudes[index];
|
|
2417
|
+
const speed = this.profile.speeds[index];
|
|
2418
|
+
const amplitudeMotion = 0.74 + 0.26 * Math.sin(
|
|
2419
|
+
phaseTime * (0.17 + index * 0.045) + this.seed * (index + 2.4)
|
|
2420
|
+
);
|
|
2421
|
+
offset += Math.sin(
|
|
2422
|
+
yRatio * Math.PI * 2 * cycle + phaseTime * speed * Math.PI * 2 + this.seed * (index + 1) + phase
|
|
2423
|
+
) * amplitude * amplitudeMotion;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
offset += this.localBulge(yRatio, phaseTime + phase, 0);
|
|
2427
|
+
offset += this.localBulge(yRatio, phaseTime - phase * 0.7, 1);
|
|
2428
|
+
return offset * envelope * amplitudeScale;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
createEdgePath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
|
|
2432
|
+
const step = Math.max(1.8, this.height / 92);
|
|
2433
|
+
ctx.beginPath();
|
|
2434
|
+
for (let y = 0; y <= this.height + step; y += step) {
|
|
2435
|
+
const x = baseX + this.edgeOffset(y, time, phase, amplitudeScale);
|
|
2436
|
+
if (y === 0) ctx.moveTo(x, y);
|
|
2437
|
+
else ctx.lineTo(x, y);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
createFillPath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
|
|
2442
|
+
const step = Math.max(1.8, this.height / 92);
|
|
2443
|
+
ctx.beginPath();
|
|
2444
|
+
ctx.moveTo(0, 0);
|
|
2445
|
+
ctx.lineTo(baseX + this.edgeOffset(0, time, phase, amplitudeScale), 0);
|
|
2446
|
+
for (let y = step; y <= this.height + step; y += step) {
|
|
2447
|
+
ctx.lineTo(baseX + this.edgeOffset(y, time, phase, amplitudeScale), y);
|
|
2168
2448
|
}
|
|
2169
|
-
this.
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2449
|
+
ctx.lineTo(0, this.height);
|
|
2450
|
+
ctx.closePath();
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
drawEllipticalGlow(x, y, radiusX, radiusY, color, alpha) {
|
|
2454
|
+
const ctx = this.ctx;
|
|
2455
|
+
ctx.save();
|
|
2456
|
+
ctx.translate(x, y);
|
|
2457
|
+
ctx.scale(1, radiusY / radiusX);
|
|
2458
|
+
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
2459
|
+
gradient.addColorStop(0, hexToRgba(color, alpha));
|
|
2460
|
+
gradient.addColorStop(0.42, hexToRgba(color, alpha * 0.48));
|
|
2461
|
+
gradient.addColorStop(1, hexToRgba(color, 0));
|
|
2462
|
+
ctx.globalCompositeOperation = 'screen';
|
|
2463
|
+
ctx.fillStyle = gradient;
|
|
2464
|
+
ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
2465
|
+
ctx.restore();
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
drawDarkEllipticalShadow(x, y, radiusX, radiusY, alpha) {
|
|
2469
|
+
const ctx = this.ctx;
|
|
2470
|
+
ctx.save();
|
|
2471
|
+
ctx.translate(x, y);
|
|
2472
|
+
ctx.scale(1, radiusY / radiusX);
|
|
2473
|
+
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
2474
|
+
gradient.addColorStop(0, `rgba(6, 6, 11, ${alpha})`);
|
|
2475
|
+
gradient.addColorStop(0.54, `rgba(8, 8, 14, ${alpha * 0.62})`);
|
|
2476
|
+
gradient.addColorStop(1, 'rgba(8, 8, 14, 0)');
|
|
2477
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
2478
|
+
ctx.fillStyle = gradient;
|
|
2479
|
+
ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
2480
|
+
ctx.restore();
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
drawColorClouds(shoreline, time, accentA, accentB, glow) {
|
|
2484
|
+
this.ctx;
|
|
2485
|
+
const width = this.width;
|
|
2486
|
+
const height = this.height;
|
|
2487
|
+
const scale = this.profile.cloudWidth;
|
|
2488
|
+
const t = time * this.profile.timeScale;
|
|
2489
|
+
|
|
2490
|
+
const upperY = height * (0.28 + Math.sin(t * 0.24 + this.seed) * 0.13);
|
|
2491
|
+
const lowerY = height * (0.70 + Math.cos(t * 0.21 + this.seed * 1.7) * 0.12);
|
|
2492
|
+
const middleY = height * (0.49 + Math.sin(t * 0.31 + this.seed * 2.3) * 0.15);
|
|
2493
|
+
|
|
2494
|
+
const farX = shoreline - width * 0.095;
|
|
2495
|
+
const farRx = Math.max(52, width * scale);
|
|
2496
|
+
const farRy = height * 0.42;
|
|
2497
|
+
this.drawEllipticalGlow(farX, upperY, farRx, farRy, accentA, 0.38);
|
|
2498
|
+
this.drawDarkEllipticalShadow(
|
|
2499
|
+
farX + farRx * 0.16,
|
|
2500
|
+
upperY,
|
|
2501
|
+
farRx * 0.58,
|
|
2502
|
+
farRy * 0.66,
|
|
2503
|
+
0.74
|
|
2504
|
+
);
|
|
2505
|
+
|
|
2506
|
+
const lowerX = shoreline - width * 0.072;
|
|
2507
|
+
const lowerRx = Math.max(44, width * scale * 0.82);
|
|
2508
|
+
const lowerRy = height * 0.36;
|
|
2509
|
+
this.drawEllipticalGlow(lowerX, lowerY, lowerRx, lowerRy, accentB, 0.31);
|
|
2510
|
+
this.drawDarkEllipticalShadow(
|
|
2511
|
+
lowerX + lowerRx * 0.14,
|
|
2512
|
+
lowerY,
|
|
2513
|
+
lowerRx * 0.54,
|
|
2514
|
+
lowerRy * 0.62,
|
|
2515
|
+
0.64
|
|
2516
|
+
);
|
|
2517
|
+
|
|
2518
|
+
this.drawEllipticalGlow(
|
|
2519
|
+
shoreline - width * 0.034,
|
|
2520
|
+
middleY,
|
|
2521
|
+
Math.max(30, width * scale * 0.48),
|
|
2522
|
+
height * 0.27,
|
|
2523
|
+
glow,
|
|
2524
|
+
0.20
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
drawPathBand({ baseX, time, phase, amplitudeScale, color, alpha, blur, width, composite = 'screen' }) {
|
|
2529
|
+
const ctx = this.ctx;
|
|
2530
|
+
this.createEdgePath(ctx, baseX, time, phase, amplitudeScale);
|
|
2531
|
+
ctx.save();
|
|
2532
|
+
ctx.globalCompositeOperation = composite;
|
|
2533
|
+
ctx.globalAlpha = alpha;
|
|
2534
|
+
// Safari < 18 and some old WebViews ignore ctx.filter; setting it is a
|
|
2535
|
+
// no-op there, so only assign when supported to keep intent explicit.
|
|
2536
|
+
if (SUPPORTS_CTX_FILTER) ctx.filter = `blur(${blur}px)`;
|
|
2537
|
+
ctx.strokeStyle = color;
|
|
2538
|
+
ctx.lineWidth = width;
|
|
2539
|
+
ctx.stroke();
|
|
2540
|
+
ctx.restore();
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
setRange(min, max) {
|
|
2544
|
+
const nextRange = normalizeRange(min, max, this.min, this.max);
|
|
2545
|
+
this.min = nextRange[0];
|
|
2546
|
+
this.max = nextRange[1];
|
|
2547
|
+
this.root.setAttribute('aria-valuemin', String(this.min));
|
|
2548
|
+
this.root.setAttribute('aria-valuemax', String(this.max));
|
|
2549
|
+
const next = snapProgressValue(this.value, this.min, this.max, this.step);
|
|
2550
|
+
if (next !== this.value) this.setProgress(next, 'prop');
|
|
2551
|
+
else this.syncValueDom();
|
|
2552
|
+
return this;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2175
2555
|
drawReferenceFlow() {
|
|
2176
|
-
const ctx = this.ctx;
|
|
2177
|
-
const width = this.width;
|
|
2178
|
-
const height = this.height;
|
|
2179
|
-
if (!ctx || width <= 0 || height <= 0) return;
|
|
2180
|
-
|
|
2556
|
+
const ctx = this.ctx;
|
|
2557
|
+
const width = this.width;
|
|
2558
|
+
const height = this.height;
|
|
2559
|
+
if (!ctx || width <= 0 || height <= 0) return;
|
|
2560
|
+
|
|
2181
2561
|
const range = Math.max(this.max - this.min, 1);
|
|
2182
2562
|
const shoreline = width * Math.min(Math.max((this.value - this.min) / range, 0), 1);
|
|
2183
|
-
const time = this.flowTime;
|
|
2184
|
-
const [dark, accentA, accentB, glow] = this.preset.colors;
|
|
2185
|
-
|
|
2186
|
-
ctx.clearRect(0, 0, width, height);
|
|
2187
|
-
ctx.fillStyle = '#202126';
|
|
2188
|
-
ctx.fillRect(0, 0, width, height);
|
|
2189
|
-
|
|
2190
|
-
this.createFillPath(ctx, shoreline, time, 0, 1);
|
|
2191
|
-
const bodyGradient = ctx.createLinearGradient(0, 0, Math.max(shoreline, 1), 0);
|
|
2192
|
-
bodyGradient.addColorStop(0, dark);
|
|
2193
|
-
bodyGradient.addColorStop(0.74, dark);
|
|
2194
|
-
bodyGradient.addColorStop(0.89, hexToRgba(dark, 0.99));
|
|
2195
|
-
bodyGradient.addColorStop(0.955, hexToRgba(accentA, 0.09));
|
|
2196
|
-
bodyGradient.addColorStop(0.992, hexToRgba(accentB, 0.54));
|
|
2197
|
-
bodyGradient.addColorStop(1, hexToRgba(glow, 0.78));
|
|
2198
|
-
ctx.fillStyle = bodyGradient;
|
|
2199
|
-
ctx.fill();
|
|
2200
|
-
|
|
2201
|
-
this.drawColorClouds(shoreline, time, accentA, accentB, glow);
|
|
2202
|
-
|
|
2203
|
-
this.drawPathBand({
|
|
2204
|
-
baseX: shoreline - width * 0.105,
|
|
2205
|
-
time,
|
|
2206
|
-
phase: 1.42,
|
|
2207
|
-
amplitudeScale: 1.18,
|
|
2208
|
-
color: accentA,
|
|
2209
|
-
alpha: 0.22,
|
|
2210
|
-
blur: 21,
|
|
2211
|
-
width: 54
|
|
2212
|
-
});
|
|
2213
|
-
this.drawPathBand({
|
|
2214
|
-
baseX: shoreline - width * 0.073,
|
|
2215
|
-
time,
|
|
2216
|
-
phase: -0.92,
|
|
2217
|
-
amplitudeScale: 1.06,
|
|
2218
|
-
color: accentB,
|
|
2219
|
-
alpha: 0.30,
|
|
2220
|
-
blur: 15,
|
|
2221
|
-
width: 42
|
|
2222
|
-
});
|
|
2223
|
-
this.drawPathBand({
|
|
2224
|
-
baseX: shoreline - width * 0.047,
|
|
2225
|
-
time,
|
|
2226
|
-
phase: 0.42,
|
|
2227
|
-
amplitudeScale: 0.94,
|
|
2228
|
-
color: 'rgba(5, 5, 10, 0.92)',
|
|
2229
|
-
alpha: 0.72,
|
|
2230
|
-
blur: 12,
|
|
2231
|
-
width: 34,
|
|
2232
|
-
composite: 'source-over'
|
|
2233
|
-
});
|
|
2234
|
-
this.drawPathBand({
|
|
2235
|
-
baseX: shoreline - width * 0.025,
|
|
2236
|
-
time,
|
|
2237
|
-
phase: -0.28,
|
|
2238
|
-
amplitudeScale: 0.96,
|
|
2239
|
-
color: accentB,
|
|
2240
|
-
alpha: 0.66,
|
|
2241
|
-
blur: 8,
|
|
2242
|
-
width: 28
|
|
2243
|
-
});
|
|
2244
|
-
|
|
2245
|
-
ctx.save();
|
|
2246
|
-
this.createFillPath(ctx, shoreline, time, 0, 1);
|
|
2247
|
-
ctx.clip();
|
|
2248
|
-
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
2249
|
-
|
|
2250
|
-
ctx.save();
|
|
2251
|
-
ctx.globalCompositeOperation = 'screen';
|
|
2252
|
-
ctx.strokeStyle = hexToRgba(accentA, 0.20);
|
|
2253
|
-
ctx.lineWidth = this.profile.haloWidth;
|
|
2254
|
-
ctx.shadowColor = accentA;
|
|
2255
|
-
ctx.shadowBlur = this.profile.haloWidth * 0.72;
|
|
2256
|
-
ctx.stroke();
|
|
2257
|
-
ctx.restore();
|
|
2258
|
-
|
|
2259
|
-
ctx.save();
|
|
2260
|
-
ctx.globalCompositeOperation = 'screen';
|
|
2261
|
-
ctx.strokeStyle = hexToRgba(accentB, 0.78);
|
|
2262
|
-
ctx.lineWidth = this.profile.glowWidth + 4.2;
|
|
2263
|
-
ctx.shadowColor = accentB;
|
|
2264
|
-
ctx.shadowBlur = 8;
|
|
2265
|
-
ctx.stroke();
|
|
2266
|
-
ctx.restore();
|
|
2267
|
-
|
|
2268
|
-
ctx.save();
|
|
2269
|
-
ctx.globalCompositeOperation = 'screen';
|
|
2270
|
-
ctx.strokeStyle = hexToRgba(glow, 0.88);
|
|
2271
|
-
ctx.lineWidth = this.profile.glowWidth;
|
|
2272
|
-
ctx.shadowColor = glow;
|
|
2273
|
-
ctx.shadowBlur = 5;
|
|
2274
|
-
ctx.stroke();
|
|
2275
|
-
ctx.restore();
|
|
2276
|
-
|
|
2277
|
-
ctx.restore();
|
|
2278
|
-
|
|
2279
|
-
ctx.save();
|
|
2280
|
-
ctx.globalCompositeOperation = 'screen';
|
|
2281
|
-
ctx.strokeStyle = hexToRgba(glow, 0.84);
|
|
2282
|
-
ctx.lineWidth = 2.15;
|
|
2283
|
-
ctx.shadowColor = glow;
|
|
2284
|
-
ctx.shadowBlur = 2.5;
|
|
2285
|
-
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
2286
|
-
ctx.stroke();
|
|
2287
|
-
ctx.restore();
|
|
2288
|
-
|
|
2289
|
-
if (this.profile.whiteAlpha > 0.05) {
|
|
2290
|
-
ctx.save();
|
|
2291
|
-
ctx.globalCompositeOperation = 'screen';
|
|
2292
|
-
ctx.strokeStyle = `rgba(255,255,245,${this.profile.whiteAlpha})`;
|
|
2293
|
-
ctx.lineWidth = this.profile.whiteWidth;
|
|
2294
|
-
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
2295
|
-
ctx.stroke();
|
|
2296
|
-
ctx.restore();
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2563
|
+
const time = this.flowTime;
|
|
2564
|
+
const [dark, accentA, accentB, glow] = this.preset.colors;
|
|
2565
|
+
|
|
2566
|
+
ctx.clearRect(0, 0, width, height);
|
|
2567
|
+
ctx.fillStyle = '#202126';
|
|
2568
|
+
ctx.fillRect(0, 0, width, height);
|
|
2569
|
+
|
|
2570
|
+
this.createFillPath(ctx, shoreline, time, 0, 1);
|
|
2571
|
+
const bodyGradient = ctx.createLinearGradient(0, 0, Math.max(shoreline, 1), 0);
|
|
2572
|
+
bodyGradient.addColorStop(0, dark);
|
|
2573
|
+
bodyGradient.addColorStop(0.74, dark);
|
|
2574
|
+
bodyGradient.addColorStop(0.89, hexToRgba(dark, 0.99));
|
|
2575
|
+
bodyGradient.addColorStop(0.955, hexToRgba(accentA, 0.09));
|
|
2576
|
+
bodyGradient.addColorStop(0.992, hexToRgba(accentB, 0.54));
|
|
2577
|
+
bodyGradient.addColorStop(1, hexToRgba(glow, 0.78));
|
|
2578
|
+
ctx.fillStyle = bodyGradient;
|
|
2579
|
+
ctx.fill();
|
|
2580
|
+
|
|
2581
|
+
this.drawColorClouds(shoreline, time, accentA, accentB, glow);
|
|
2582
|
+
|
|
2583
|
+
this.drawPathBand({
|
|
2584
|
+
baseX: shoreline - width * 0.105,
|
|
2585
|
+
time,
|
|
2586
|
+
phase: 1.42,
|
|
2587
|
+
amplitudeScale: 1.18,
|
|
2588
|
+
color: accentA,
|
|
2589
|
+
alpha: 0.22,
|
|
2590
|
+
blur: 21,
|
|
2591
|
+
width: 54
|
|
2592
|
+
});
|
|
2593
|
+
this.drawPathBand({
|
|
2594
|
+
baseX: shoreline - width * 0.073,
|
|
2595
|
+
time,
|
|
2596
|
+
phase: -0.92,
|
|
2597
|
+
amplitudeScale: 1.06,
|
|
2598
|
+
color: accentB,
|
|
2599
|
+
alpha: 0.30,
|
|
2600
|
+
blur: 15,
|
|
2601
|
+
width: 42
|
|
2602
|
+
});
|
|
2603
|
+
this.drawPathBand({
|
|
2604
|
+
baseX: shoreline - width * 0.047,
|
|
2605
|
+
time,
|
|
2606
|
+
phase: 0.42,
|
|
2607
|
+
amplitudeScale: 0.94,
|
|
2608
|
+
color: 'rgba(5, 5, 10, 0.92)',
|
|
2609
|
+
alpha: 0.72,
|
|
2610
|
+
blur: 12,
|
|
2611
|
+
width: 34,
|
|
2612
|
+
composite: 'source-over'
|
|
2613
|
+
});
|
|
2614
|
+
this.drawPathBand({
|
|
2615
|
+
baseX: shoreline - width * 0.025,
|
|
2616
|
+
time,
|
|
2617
|
+
phase: -0.28,
|
|
2618
|
+
amplitudeScale: 0.96,
|
|
2619
|
+
color: accentB,
|
|
2620
|
+
alpha: 0.66,
|
|
2621
|
+
blur: 8,
|
|
2622
|
+
width: 28
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
ctx.save();
|
|
2626
|
+
this.createFillPath(ctx, shoreline, time, 0, 1);
|
|
2627
|
+
ctx.clip();
|
|
2628
|
+
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
2629
|
+
|
|
2630
|
+
ctx.save();
|
|
2631
|
+
ctx.globalCompositeOperation = 'screen';
|
|
2632
|
+
ctx.strokeStyle = hexToRgba(accentA, 0.20);
|
|
2633
|
+
ctx.lineWidth = this.profile.haloWidth;
|
|
2634
|
+
ctx.shadowColor = accentA;
|
|
2635
|
+
ctx.shadowBlur = this.profile.haloWidth * 0.72;
|
|
2636
|
+
ctx.stroke();
|
|
2637
|
+
ctx.restore();
|
|
2638
|
+
|
|
2639
|
+
ctx.save();
|
|
2640
|
+
ctx.globalCompositeOperation = 'screen';
|
|
2641
|
+
ctx.strokeStyle = hexToRgba(accentB, 0.78);
|
|
2642
|
+
ctx.lineWidth = this.profile.glowWidth + 4.2;
|
|
2643
|
+
ctx.shadowColor = accentB;
|
|
2644
|
+
ctx.shadowBlur = 8;
|
|
2645
|
+
ctx.stroke();
|
|
2646
|
+
ctx.restore();
|
|
2647
|
+
|
|
2648
|
+
ctx.save();
|
|
2649
|
+
ctx.globalCompositeOperation = 'screen';
|
|
2650
|
+
ctx.strokeStyle = hexToRgba(glow, 0.88);
|
|
2651
|
+
ctx.lineWidth = this.profile.glowWidth;
|
|
2652
|
+
ctx.shadowColor = glow;
|
|
2653
|
+
ctx.shadowBlur = 5;
|
|
2654
|
+
ctx.stroke();
|
|
2655
|
+
ctx.restore();
|
|
2656
|
+
|
|
2657
|
+
ctx.restore();
|
|
2658
|
+
|
|
2659
|
+
ctx.save();
|
|
2660
|
+
ctx.globalCompositeOperation = 'screen';
|
|
2661
|
+
ctx.strokeStyle = hexToRgba(glow, 0.84);
|
|
2662
|
+
ctx.lineWidth = 2.15;
|
|
2663
|
+
ctx.shadowColor = glow;
|
|
2664
|
+
ctx.shadowBlur = 2.5;
|
|
2665
|
+
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
2666
|
+
ctx.stroke();
|
|
2667
|
+
ctx.restore();
|
|
2668
|
+
|
|
2669
|
+
if (this.profile.whiteAlpha > 0.05) {
|
|
2670
|
+
ctx.save();
|
|
2671
|
+
ctx.globalCompositeOperation = 'screen';
|
|
2672
|
+
ctx.strokeStyle = `rgba(255,255,245,${this.profile.whiteAlpha})`;
|
|
2673
|
+
ctx.lineWidth = this.profile.whiteWidth;
|
|
2674
|
+
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
2675
|
+
ctx.stroke();
|
|
2676
|
+
ctx.restore();
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2300
2680
|
updateFromPointer(event) {
|
|
2301
2681
|
const bounds = this.root.getBoundingClientRect();
|
|
2302
|
-
|
|
2682
|
+
let ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
|
|
2683
|
+
if (this.direction === 'rtl') ratio = 1 - ratio;
|
|
2303
2684
|
this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
|
|
2304
2685
|
}
|
|
2305
|
-
|
|
2686
|
+
|
|
2687
|
+
onKeyDown(event) {
|
|
2688
|
+
const range = this.max - this.min;
|
|
2689
|
+
const configuredStep = Number(this.step);
|
|
2690
|
+
const step = Number.isFinite(configuredStep) && configuredStep > 0
|
|
2691
|
+
? configuredStep
|
|
2692
|
+
: Math.max(range / 100, 1e-7);
|
|
2693
|
+
const direction = this.direction === 'rtl' ? -1 : 1;
|
|
2694
|
+
const aliases = { Left: 'ArrowLeft', Right: 'ArrowRight', Up: 'ArrowUp', Down: 'ArrowDown' };
|
|
2695
|
+
const keyCodes = {
|
|
2696
|
+
35: 'End', 36: 'Home', 33: 'PageUp', 34: 'PageDown',
|
|
2697
|
+
37: 'ArrowLeft', 38: 'ArrowUp', 39: 'ArrowRight', 40: 'ArrowDown'
|
|
2698
|
+
};
|
|
2699
|
+
const key = aliases[event.key] || event.key || keyCodes[event.keyCode];
|
|
2700
|
+
let next = null;
|
|
2701
|
+
if (key === 'ArrowRight') next = this.value + step * direction;
|
|
2702
|
+
else if (key === 'ArrowLeft') next = this.value - step * direction;
|
|
2703
|
+
else if (key === 'ArrowUp') next = this.value + step;
|
|
2704
|
+
else if (key === 'ArrowDown') next = this.value - step;
|
|
2705
|
+
else if (key === 'PageUp') next = this.value + step * 10;
|
|
2706
|
+
else if (key === 'PageDown') next = this.value - step * 10;
|
|
2707
|
+
else if (key === 'Home') next = this.min;
|
|
2708
|
+
else if (key === 'End') next = this.max;
|
|
2709
|
+
if (next === null) return;
|
|
2710
|
+
event.preventDefault();
|
|
2711
|
+
this.setProgress(next, 'keyboard');
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2306
2714
|
beginDrag(event) {
|
|
2307
2715
|
if (event.button !== undefined && event.button !== 0) return;
|
|
2308
2716
|
event.preventDefault();
|
|
@@ -2342,85 +2750,101 @@ class ProgressCapsuleController {
|
|
|
2342
2750
|
this.pendingPointer = null;
|
|
2343
2751
|
if (pending) this.updateFromPointer(pending);
|
|
2344
2752
|
this.root.classList.remove('is-dragging');
|
|
2345
|
-
try {
|
|
2346
|
-
if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
|
|
2347
|
-
this.root.releasePointerCapture(event.pointerId);
|
|
2348
|
-
}
|
|
2349
|
-
} catch {}
|
|
2350
|
-
this.emitter.emit('dragend', { value: this.value });
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
bindEvents() {
|
|
2354
|
-
if (this.options.draggable !== false) {
|
|
2355
|
-
this.handlers.pointerdown = (event) => this.beginDrag(event);
|
|
2356
|
-
this.handlers.pointermove = (event) => this.moveDrag(event);
|
|
2357
|
-
this.handlers.pointerup = (event) => this.endDrag(event);
|
|
2358
|
-
this.handlers.pointercancel = (event) => this.endDrag(event);
|
|
2359
|
-
this.root.addEventListener('pointerdown', this.handlers.pointerdown);
|
|
2360
|
-
this.root.addEventListener('pointermove', this.handlers.pointermove);
|
|
2361
|
-
this.root.addEventListener('pointerup', this.handlers.pointerup);
|
|
2362
|
-
this.root.addEventListener('pointercancel', this.handlers.pointercancel);
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
this.
|
|
2385
|
-
|
|
2386
|
-
|
|
2753
|
+
try {
|
|
2754
|
+
if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
|
|
2755
|
+
this.root.releasePointerCapture(event.pointerId);
|
|
2756
|
+
}
|
|
2757
|
+
} catch {}
|
|
2758
|
+
this.emitter.emit('dragend', { value: this.value });
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
bindEvents() {
|
|
2762
|
+
if (this.options.draggable !== false) {
|
|
2763
|
+
this.handlers.pointerdown = (event) => this.beginDrag(event);
|
|
2764
|
+
this.handlers.pointermove = (event) => this.moveDrag(event);
|
|
2765
|
+
this.handlers.pointerup = (event) => this.endDrag(event);
|
|
2766
|
+
this.handlers.pointercancel = (event) => this.endDrag(event);
|
|
2767
|
+
this.root.addEventListener('pointerdown', this.handlers.pointerdown);
|
|
2768
|
+
this.root.addEventListener('pointermove', this.handlers.pointermove);
|
|
2769
|
+
this.root.addEventListener('pointerup', this.handlers.pointerup);
|
|
2770
|
+
this.root.addEventListener('pointercancel', this.handlers.pointercancel);
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
if (this.options.keyboard !== false) {
|
|
2774
|
+
this.handlers.keydown = (event) => this.onKeyDown(event);
|
|
2775
|
+
this.root.addEventListener('keydown', this.handlers.keydown);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
update(delta, paused) {
|
|
2781
|
+
if (!paused) this.flowTime += delta;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
draw() {
|
|
2785
|
+
// Performance fix: when the WebGL overlay is active the 2D layer is
|
|
2786
|
+
// hidden behind it, so drawing it every frame would be wasted CPU.
|
|
2787
|
+
if (this.webglActive) return;
|
|
2788
|
+
this.drawReferenceFlow();
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
randomize() {
|
|
2792
|
+
this.flowTime = this.random() * 40;
|
|
2793
|
+
return this.flowTime;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
setColors(colors) {
|
|
2797
|
+
if (!Array.isArray(colors) || colors.length !== 4) return;
|
|
2798
|
+
this.preset.colors = [...colors];
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2387
2801
|
dispose() {
|
|
2388
2802
|
if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
|
|
2389
2803
|
if (this.resizeObserver) this.resizeObserver.disconnect();
|
|
2390
|
-
else window.removeEventListener('resize', this.
|
|
2391
|
-
for (const name of Object.keys(this.handlers)) {
|
|
2392
|
-
const handler = this.handlers[name];
|
|
2393
|
-
this.root.removeEventListener(name, handler);
|
|
2394
|
-
}
|
|
2395
|
-
this.handlers = {};
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
/**
|
|
2804
|
+
else window.removeEventListener('resize', this.onResize);
|
|
2805
|
+
for (const name of Object.keys(this.handlers)) {
|
|
2806
|
+
const handler = this.handlers[name];
|
|
2807
|
+
this.root.removeEventListener(name, handler);
|
|
2808
|
+
}
|
|
2809
|
+
this.handlers = {};
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2400
2814
|
* Mount a fluid progress capsule into `container`.
|
|
2401
2815
|
*
|
|
2402
|
-
* Options: preset (literary name), width, height, value, min, max,
|
|
2403
|
-
* draggable,
|
|
2816
|
+
* Options: preset (literary name), width, height, value/modelValue, min, max,
|
|
2817
|
+
* step, draggable, keyboard, direction, precision/formatValue, colors,
|
|
2818
|
+
* edgeStyle, textRatio (0-100, text region
|
|
2404
2819
|
* width in percent), text (HTML string or DOM nodes for the text slot),
|
|
2405
2820
|
* colorContent (HTML string or DOM nodes for the color slot),
|
|
2406
|
-
* showValue (show/hide the right-side percentage), quality,
|
|
2407
|
-
* respectReducedMotion, cssVars.
|
|
2821
|
+
* showValue (show/hide the right-side percentage), quality, renderScale,
|
|
2822
|
+
* powerPreference, fps, paused/static, respectReducedMotion, cssVars.
|
|
2408
2823
|
*/
|
|
2409
2824
|
function createProgressCapsule(container, options = {}) {
|
|
2410
|
-
if (!container || typeof container.appendChild !== 'function') {
|
|
2411
|
-
throw new Error('createProgressCapsule: container element is required');
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2825
|
+
if (!container || typeof container.appendChild !== 'function') {
|
|
2826
|
+
throw new Error('createProgressCapsule: container element is required');
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2414
2829
|
const preset = { ...getPreset('progress', options.preset ?? '星火') };
|
|
2415
2830
|
const merged = normalizeOptions(DEFAULTS.progress, preset, options);
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
preset.edgeStyle =
|
|
2831
|
+
[merged.min, merged.max] = normalizeRange(merged.min, merged.max);
|
|
2832
|
+
if (merged.modelValue !== undefined) merged.value = merged.modelValue;
|
|
2833
|
+
const normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
|
|
2834
|
+
if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
2835
|
+
const initialEdgeStyle = merged.edgeStyle === 'tide' || merged.edgeStyle === 'flow'
|
|
2836
|
+
? merged.edgeStyle
|
|
2837
|
+
: preset.edgeStyle;
|
|
2838
|
+
preset.edgeStyle = initialEdgeStyle;
|
|
2839
|
+
merged.edgeStyle = initialEdgeStyle;
|
|
2840
|
+
merged.colors = [...preset.colors];
|
|
2841
|
+
const optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
|
|
2842
|
+
let colorOverride = optionColors.length === 4 && optionColors.every(Boolean)
|
|
2843
|
+
? [...optionColors]
|
|
2844
|
+
: null;
|
|
2845
|
+
let edgeStyleOverride = options.edgeStyle === 'flow' || options.edgeStyle === 'tide'
|
|
2846
|
+
? merged.edgeStyle
|
|
2847
|
+
: null;
|
|
2424
2848
|
const copy = COPY;
|
|
2425
2849
|
let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
|
|
2426
2850
|
// Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
|
|
@@ -2429,19 +2853,32 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2429
2853
|
const readonly = merged.readonly === true || merged.readonly === '' || merged.readonly === 'true';
|
|
2430
2854
|
const locked = disabled || readonly;
|
|
2431
2855
|
const effectiveDraggable = locked ? false : merged.draggable !== false;
|
|
2856
|
+
const effectiveKeyboard = locked ? false : merged.keyboard !== false;
|
|
2432
2857
|
const dirty = {
|
|
2433
2858
|
preset: false,
|
|
2434
2859
|
colors: false,
|
|
2435
2860
|
textRatio: false,
|
|
2436
2861
|
cssVars: false,
|
|
2437
2862
|
edgeStyle: false,
|
|
2438
|
-
value: false
|
|
2863
|
+
value: false,
|
|
2864
|
+
step: false,
|
|
2865
|
+
precision: false,
|
|
2866
|
+
formatValue: false,
|
|
2867
|
+
direction: false,
|
|
2868
|
+
showValue: false,
|
|
2869
|
+
valueSuffix: false,
|
|
2870
|
+
quality: false,
|
|
2871
|
+
renderScale: false,
|
|
2872
|
+
paused: false,
|
|
2873
|
+
static: false,
|
|
2874
|
+
fps: false
|
|
2439
2875
|
};
|
|
2440
2876
|
|
|
2441
2877
|
const root = document.createElement('div');
|
|
2442
2878
|
root.className = 'hj-capsule-root hj-progress-root';
|
|
2443
2879
|
root.setAttribute('role', 'slider');
|
|
2444
2880
|
root.setAttribute('data-draggable', String(effectiveDraggable));
|
|
2881
|
+
root.dataset.direction = merged.direction === 'rtl' ? 'rtl' : 'ltr';
|
|
2445
2882
|
if (disabled) {
|
|
2446
2883
|
root.setAttribute('aria-disabled', 'true');
|
|
2447
2884
|
root.classList.add('is-disabled');
|
|
@@ -2450,10 +2887,11 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2450
2887
|
root.setAttribute('aria-readonly', 'true');
|
|
2451
2888
|
root.classList.add('is-readonly');
|
|
2452
2889
|
}
|
|
2453
|
-
|
|
2454
|
-
root.setAttribute('aria-
|
|
2455
|
-
root.setAttribute('aria-
|
|
2456
|
-
root.setAttribute('aria-
|
|
2890
|
+
root.setAttribute('tabindex', locked ? '-1' : (effectiveKeyboard ? '0' : '-1'));
|
|
2891
|
+
root.setAttribute('aria-orientation', 'horizontal');
|
|
2892
|
+
root.setAttribute('aria-valuemin', String(merged.min ?? 0));
|
|
2893
|
+
root.setAttribute('aria-valuemax', String(merged.max ?? 100));
|
|
2894
|
+
root.setAttribute('aria-valuenow', String(preset.initialProgress));
|
|
2457
2895
|
const updateAria = () => {
|
|
2458
2896
|
root.setAttribute(
|
|
2459
2897
|
'aria-label',
|
|
@@ -2461,10 +2899,10 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2461
2899
|
);
|
|
2462
2900
|
};
|
|
2463
2901
|
updateAria();
|
|
2464
|
-
const canvas = document.createElement('canvas');
|
|
2465
|
-
canvas.className = 'hj-progress-canvas';
|
|
2466
|
-
canvas.setAttribute('aria-hidden', 'true');
|
|
2467
|
-
|
|
2902
|
+
const canvas = document.createElement('canvas');
|
|
2903
|
+
canvas.className = 'hj-progress-canvas';
|
|
2904
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
2905
|
+
|
|
2468
2906
|
// Slot containers are always present but transparent by default; they only
|
|
2469
2907
|
// provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
|
|
2470
2908
|
const fillContent = (layer, content) => {
|
|
@@ -2493,17 +2931,31 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2493
2931
|
valueElement.className = 'hj-progress-value';
|
|
2494
2932
|
valueElement.setAttribute('aria-hidden', 'true');
|
|
2495
2933
|
valueElement.hidden = merged.showValue === false;
|
|
2496
|
-
|
|
2934
|
+
|
|
2497
2935
|
root.appendChild(canvas);
|
|
2498
2936
|
root.appendChild(textLayer);
|
|
2499
2937
|
root.appendChild(visualLayer);
|
|
2500
2938
|
root.appendChild(valueElement);
|
|
2501
2939
|
container.appendChild(root);
|
|
2502
|
-
|
|
2940
|
+
|
|
2503
2941
|
const emitter = createEmitter();
|
|
2504
|
-
let
|
|
2942
|
+
let manuallyPaused = merged.paused === true;
|
|
2943
|
+
let reducedPaused = false;
|
|
2944
|
+
let staticMode = merged.static === true;
|
|
2945
|
+
let contextLost = false;
|
|
2505
2946
|
let disposed = false;
|
|
2506
|
-
|
|
2947
|
+
let renderOnce = () => {};
|
|
2948
|
+
const onContextLost = () => {
|
|
2949
|
+
contextLost = true;
|
|
2950
|
+
emitter.emit('contextlost', {});
|
|
2951
|
+
};
|
|
2952
|
+
const onContextRestored = () => {
|
|
2953
|
+
contextLost = false;
|
|
2954
|
+
emitter.emit('contextrestored', {});
|
|
2955
|
+
renderOnce();
|
|
2956
|
+
wakeScheduler();
|
|
2957
|
+
};
|
|
2958
|
+
|
|
2507
2959
|
const applySize = () => {
|
|
2508
2960
|
root.style.width = parseSize(merged.width);
|
|
2509
2961
|
root.style.height = parseSize(merged.height);
|
|
@@ -2513,28 +2965,39 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2513
2965
|
root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
|
|
2514
2966
|
}
|
|
2515
2967
|
};
|
|
2516
|
-
applySize();
|
|
2517
|
-
|
|
2968
|
+
applySize();
|
|
2969
|
+
|
|
2518
2970
|
const controller = new ProgressCapsuleController({
|
|
2519
2971
|
root,
|
|
2520
2972
|
canvas,
|
|
2521
2973
|
valueElement,
|
|
2522
2974
|
preset,
|
|
2523
2975
|
emitter,
|
|
2524
|
-
options: {
|
|
2976
|
+
options: {
|
|
2977
|
+
...merged,
|
|
2978
|
+
draggable: effectiveDraggable,
|
|
2979
|
+
keyboard: effectiveKeyboard,
|
|
2980
|
+
onResize: () => {
|
|
2981
|
+
if (manuallyPaused || reducedPaused || staticMode) renderOnce();
|
|
2982
|
+
}
|
|
2983
|
+
},
|
|
2525
2984
|
copy,
|
|
2526
2985
|
dirty
|
|
2527
2986
|
});
|
|
2528
|
-
|
|
2529
|
-
let overlay = null;
|
|
2530
|
-
if (merged.renderer !== 'canvas2d') {
|
|
2531
|
-
overlay = attachProgressFlowOverlay({
|
|
2532
|
-
root,
|
|
2533
|
-
canvas,
|
|
2534
|
-
preset,
|
|
2535
|
-
getProgress: () => controller.value
|
|
2536
|
-
|
|
2537
|
-
|
|
2987
|
+
|
|
2988
|
+
let overlay = null;
|
|
2989
|
+
if (merged.renderer !== 'canvas2d') {
|
|
2990
|
+
overlay = attachProgressFlowOverlay({
|
|
2991
|
+
root,
|
|
2992
|
+
canvas,
|
|
2993
|
+
preset,
|
|
2994
|
+
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
2995
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
2996
|
+
powerPreference: merged.powerPreference,
|
|
2997
|
+
onContextLost,
|
|
2998
|
+
onContextRestored
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
2538
3001
|
if (overlay) controller.webglActive = true;
|
|
2539
3002
|
else if (merged.renderer !== 'canvas2d') {
|
|
2540
3003
|
nextTick(() => {
|
|
@@ -2542,66 +3005,99 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2542
3005
|
emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
|
|
2543
3006
|
});
|
|
2544
3007
|
}
|
|
2545
|
-
|
|
2546
|
-
const visibility = createVisibilityGuard(root);
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
(
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
);
|
|
2559
|
-
|
|
3008
|
+
|
|
3009
|
+
const visibility = createVisibilityGuard(root, wakeScheduler);
|
|
3010
|
+
const motionPreference = createReducedMotionPreference(
|
|
3011
|
+
merged.respectReducedMotion,
|
|
3012
|
+
(matches) => {
|
|
3013
|
+
reducedPaused = matches;
|
|
3014
|
+
if (matches) renderOnce();
|
|
3015
|
+
else wakeScheduler();
|
|
3016
|
+
}
|
|
3017
|
+
);
|
|
3018
|
+
reducedPaused = motionPreference.matches();
|
|
3019
|
+
const isMotionPaused = () => manuallyPaused || reducedPaused || staticMode || contextLost;
|
|
3020
|
+
let flowTime = 0;
|
|
3021
|
+
const frameGate = createFrameGate(merged.fps);
|
|
3022
|
+
renderOnce = () => {
|
|
3023
|
+
controller.draw();
|
|
3024
|
+
if (overlay) overlay.update(flowTime);
|
|
3025
|
+
};
|
|
3026
|
+
renderOnce();
|
|
3027
|
+
const offPausedChange = emitter.on('change', () => {
|
|
3028
|
+
if (isMotionPaused()) renderOnce();
|
|
3029
|
+
});
|
|
3030
|
+
const unsubscribe = subscribeScheduler(
|
|
3031
|
+
(delta, now) => {
|
|
3032
|
+
flowTime += delta;
|
|
3033
|
+
controller.update(delta, false);
|
|
3034
|
+
if (frameGate.shouldDraw(delta)) {
|
|
3035
|
+
controller.draw();
|
|
3036
|
+
if (overlay) overlay.update(flowTime);
|
|
3037
|
+
}
|
|
3038
|
+
},
|
|
3039
|
+
() => isMotionPaused() || !visibility.isVisible()
|
|
3040
|
+
);
|
|
3041
|
+
|
|
2560
3042
|
nextTick(() => {
|
|
2561
3043
|
if (disposed) return;
|
|
2562
3044
|
emitter.emit('ready', { preset: { ...preset } });
|
|
2563
3045
|
});
|
|
2564
|
-
|
|
3046
|
+
|
|
2565
3047
|
const syncDom = () => {
|
|
2566
3048
|
updateAria();
|
|
2567
3049
|
};
|
|
2568
|
-
|
|
2569
|
-
return {
|
|
2570
|
-
element: root,
|
|
2571
|
-
canvas,
|
|
2572
|
-
preset,
|
|
2573
|
-
on: emitter.on,
|
|
2574
|
-
off: emitter.off,
|
|
2575
|
-
setValue(value, source = 'prop') {
|
|
2576
|
-
controller.setProgress(value, source);
|
|
2577
|
-
return this;
|
|
2578
|
-
},
|
|
2579
|
-
getValue() {
|
|
2580
|
-
return controller.value;
|
|
2581
|
-
},
|
|
2582
|
-
setRange(min, max) {
|
|
2583
|
-
controller.setRange(min, max);
|
|
2584
|
-
|
|
2585
|
-
|
|
3050
|
+
|
|
3051
|
+
return {
|
|
3052
|
+
element: root,
|
|
3053
|
+
canvas,
|
|
3054
|
+
preset,
|
|
3055
|
+
on: emitter.on,
|
|
3056
|
+
off: emitter.off,
|
|
3057
|
+
setValue(value, source = 'prop') {
|
|
3058
|
+
controller.setProgress(value, source);
|
|
3059
|
+
return this;
|
|
3060
|
+
},
|
|
3061
|
+
getValue() {
|
|
3062
|
+
return controller.value;
|
|
3063
|
+
},
|
|
3064
|
+
setRange(min, max) {
|
|
3065
|
+
controller.setRange(min, max);
|
|
3066
|
+
if (isMotionPaused()) renderOnce();
|
|
3067
|
+
return this;
|
|
3068
|
+
},
|
|
2586
3069
|
setPreset(ref) {
|
|
2587
3070
|
const next = getPreset('progress', ref);
|
|
2588
3071
|
dirty.preset = true;
|
|
2589
3072
|
Object.assign(preset, next);
|
|
3073
|
+
if (colorOverride) preset.colors = [...colorOverride];
|
|
3074
|
+
if (edgeStyleOverride) preset.edgeStyle = edgeStyleOverride;
|
|
2590
3075
|
const nextColors = preset.colors.map(normalizeColor);
|
|
2591
3076
|
if (nextColors.every(Boolean)) preset.colors = nextColors;
|
|
2592
3077
|
controller.preset = preset;
|
|
2593
|
-
controller.profile =
|
|
3078
|
+
controller.profile = preset.edgeStyle === 'tide'
|
|
2594
3079
|
? FLOW_PROFILES.tide
|
|
2595
|
-
: (FLOW_PROFILES[
|
|
3080
|
+
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
2596
3081
|
controller.flowTime = stringSeed(next.id) * 31;
|
|
2597
3082
|
controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
|
|
2598
3083
|
syncDom();
|
|
2599
3084
|
if (overlay) {
|
|
2600
3085
|
overlay.dispose();
|
|
2601
|
-
|
|
3086
|
+
contextLost = false;
|
|
3087
|
+
overlay = attachProgressFlowOverlay({
|
|
3088
|
+
root,
|
|
3089
|
+
canvas,
|
|
3090
|
+
preset,
|
|
3091
|
+
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
3092
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
3093
|
+
powerPreference: merged.powerPreference,
|
|
3094
|
+
onContextLost,
|
|
3095
|
+
onContextRestored
|
|
3096
|
+
});
|
|
2602
3097
|
}
|
|
2603
3098
|
controller.webglActive = Boolean(overlay);
|
|
2604
3099
|
controller.resizeCanvas();
|
|
3100
|
+
if (isMotionPaused()) renderOnce();
|
|
2605
3101
|
emitter.emit('presetchange', { preset: { ...preset } });
|
|
2606
3102
|
return this;
|
|
2607
3103
|
},
|
|
@@ -2609,16 +3105,28 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2609
3105
|
const value = String(edgeStyle || '').toLowerCase();
|
|
2610
3106
|
if (value !== 'flow' && value !== 'tide') return this;
|
|
2611
3107
|
dirty.edgeStyle = true;
|
|
3108
|
+
edgeStyleOverride = value;
|
|
2612
3109
|
preset.edgeStyle = value;
|
|
2613
3110
|
controller.profile = value === 'tide'
|
|
2614
3111
|
? FLOW_PROFILES.tide
|
|
2615
3112
|
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
2616
3113
|
if (overlay) {
|
|
2617
3114
|
overlay.dispose();
|
|
2618
|
-
|
|
3115
|
+
contextLost = false;
|
|
3116
|
+
overlay = attachProgressFlowOverlay({
|
|
3117
|
+
root,
|
|
3118
|
+
canvas,
|
|
3119
|
+
preset,
|
|
3120
|
+
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
3121
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
3122
|
+
powerPreference: merged.powerPreference,
|
|
3123
|
+
onContextLost,
|
|
3124
|
+
onContextRestored
|
|
3125
|
+
});
|
|
2619
3126
|
}
|
|
2620
3127
|
controller.webglActive = Boolean(overlay);
|
|
2621
3128
|
controller.resizeCanvas();
|
|
3129
|
+
if (isMotionPaused()) renderOnce();
|
|
2622
3130
|
return this;
|
|
2623
3131
|
},
|
|
2624
3132
|
setText(content) {
|
|
@@ -2653,6 +3161,22 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2653
3161
|
controller.setValueSuffix(suffix);
|
|
2654
3162
|
return this;
|
|
2655
3163
|
},
|
|
3164
|
+
setStep(step) {
|
|
3165
|
+
controller.setStep(step);
|
|
3166
|
+
return this;
|
|
3167
|
+
},
|
|
3168
|
+
setPrecision(precision) {
|
|
3169
|
+
controller.setPrecision(precision);
|
|
3170
|
+
return this;
|
|
3171
|
+
},
|
|
3172
|
+
setFormatValue(formatter) {
|
|
3173
|
+
controller.setFormatValue(formatter);
|
|
3174
|
+
return this;
|
|
3175
|
+
},
|
|
3176
|
+
setDirection(direction) {
|
|
3177
|
+
controller.setDirection(direction);
|
|
3178
|
+
return this;
|
|
3179
|
+
},
|
|
2656
3180
|
setShowValue(show) {
|
|
2657
3181
|
controller.setShowValue(show);
|
|
2658
3182
|
return this;
|
|
@@ -2661,11 +3185,14 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2661
3185
|
const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
|
|
2662
3186
|
if (next.length !== 4 || next.some((color) => !color)) return this;
|
|
2663
3187
|
dirty.colors = true;
|
|
3188
|
+
colorOverride = [...next];
|
|
3189
|
+
preset.colors = [...next];
|
|
2664
3190
|
controller.setColors(next);
|
|
2665
3191
|
if (overlay) overlay.setColors(next);
|
|
2666
3192
|
controller.resizeCanvas();
|
|
3193
|
+
if (isMotionPaused()) renderOnce();
|
|
2667
3194
|
return this;
|
|
2668
|
-
},
|
|
3195
|
+
},
|
|
2669
3196
|
setSize(width, height) {
|
|
2670
3197
|
if (width !== undefined) {
|
|
2671
3198
|
parseSize(width);
|
|
@@ -2676,111 +3203,208 @@ function createProgressCapsule(container, options = {}) {
|
|
|
2676
3203
|
merged.height = height;
|
|
2677
3204
|
}
|
|
2678
3205
|
applySize();
|
|
2679
|
-
controller.resizeCanvas();
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
return this;
|
|
2693
|
-
},
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
return this;
|
|
2699
|
-
},
|
|
3206
|
+
controller.resizeCanvas();
|
|
3207
|
+
if (isMotionPaused()) renderOnce();
|
|
3208
|
+
return this;
|
|
3209
|
+
},
|
|
3210
|
+
randomize() {
|
|
3211
|
+
flowTime = controller.randomize();
|
|
3212
|
+
if (isMotionPaused()) renderOnce();
|
|
3213
|
+
return this;
|
|
3214
|
+
},
|
|
3215
|
+
pause() {
|
|
3216
|
+
dirty.paused = true;
|
|
3217
|
+
manuallyPaused = true;
|
|
3218
|
+
renderOnce();
|
|
3219
|
+
return this;
|
|
3220
|
+
},
|
|
3221
|
+
resume() {
|
|
3222
|
+
dirty.paused = true;
|
|
3223
|
+
manuallyPaused = false;
|
|
3224
|
+
wakeScheduler();
|
|
3225
|
+
return this;
|
|
3226
|
+
},
|
|
3227
|
+
setPaused(value) {
|
|
3228
|
+
return value ? this.pause() : this.resume();
|
|
3229
|
+
},
|
|
3230
|
+
setStatic(value) {
|
|
3231
|
+
dirty.static = true;
|
|
3232
|
+
staticMode = value === true;
|
|
3233
|
+
merged.static = staticMode;
|
|
3234
|
+
if (staticMode) renderOnce();
|
|
3235
|
+
else wakeScheduler();
|
|
3236
|
+
return this;
|
|
3237
|
+
},
|
|
3238
|
+
setFps(fps) {
|
|
3239
|
+
dirty.fps = true;
|
|
3240
|
+
merged.fps = frameGate.setFps(fps);
|
|
3241
|
+
wakeScheduler();
|
|
3242
|
+
return this;
|
|
3243
|
+
},
|
|
3244
|
+
setQuality(quality) {
|
|
3245
|
+
dirty.quality = true;
|
|
3246
|
+
merged.quality = quality;
|
|
3247
|
+
controller.dprCap = effectiveDprCap(quality, merged.renderScale);
|
|
3248
|
+
controller.resizeCanvas();
|
|
3249
|
+
if (overlay) overlay.setDprCap(controller.dprCap);
|
|
3250
|
+
if (isMotionPaused()) renderOnce();
|
|
3251
|
+
return this;
|
|
3252
|
+
},
|
|
3253
|
+
setRenderScale(renderScale) {
|
|
3254
|
+
const value = Number(renderScale);
|
|
3255
|
+
if (!Number.isFinite(value)) return this;
|
|
3256
|
+
dirty.renderScale = true;
|
|
3257
|
+
merged.renderScale = Math.min(1, Math.max(0.25, value));
|
|
3258
|
+
controller.dprCap = effectiveDprCap(merged.quality, merged.renderScale);
|
|
3259
|
+
controller.resizeCanvas();
|
|
3260
|
+
if (overlay) overlay.setDprCap(controller.dprCap);
|
|
3261
|
+
if (isMotionPaused()) renderOnce();
|
|
3262
|
+
return this;
|
|
3263
|
+
},
|
|
2700
3264
|
dispose() {
|
|
2701
3265
|
disposed = true;
|
|
2702
3266
|
unsubscribe();
|
|
2703
|
-
|
|
2704
|
-
|
|
3267
|
+
offPausedChange();
|
|
3268
|
+
visibility.dispose();
|
|
3269
|
+
motionPreference.dispose();
|
|
3270
|
+
if (overlay) overlay.dispose();
|
|
2705
3271
|
controller.dispose();
|
|
2706
3272
|
root.remove();
|
|
2707
3273
|
},
|
|
2708
3274
|
get textRatio() { return merged.textRatio; },
|
|
2709
3275
|
get cssVars() { return merged.cssVars; },
|
|
3276
|
+
get min() { return controller.min; },
|
|
3277
|
+
get max() { return controller.max; },
|
|
3278
|
+
get step() { return controller.step; },
|
|
3279
|
+
get precision() { return controller.precision; },
|
|
3280
|
+
get formatValue() { return controller.formatValue; },
|
|
3281
|
+
get direction() { return controller.direction; },
|
|
3282
|
+
get showValue() { return !valueElement.hidden; },
|
|
3283
|
+
get valueSuffix() { return controller.valueSuffix; },
|
|
3284
|
+
get quality() { return merged.quality; },
|
|
3285
|
+
get renderScale() { return merged.renderScale; },
|
|
3286
|
+
get paused() { return manuallyPaused; },
|
|
3287
|
+
get static() { return staticMode; },
|
|
3288
|
+
get fps() { return frameGate.getFps(); },
|
|
2710
3289
|
dirty
|
|
2711
3290
|
};
|
|
2712
3291
|
}
|
|
2713
3292
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
3293
|
+
const HOST_STYLES = new WeakMap();
|
|
3294
|
+
|
|
3295
|
+
function acquireHostStyles(host) {
|
|
3296
|
+
const active = HOST_STYLES.get(host);
|
|
3297
|
+
if (active) {
|
|
3298
|
+
active.count += 1;
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
const state = {
|
|
3302
|
+
count: 1,
|
|
3303
|
+
position: host.style.position,
|
|
3304
|
+
isolation: host.style.isolation
|
|
3305
|
+
};
|
|
3306
|
+
const computed = getComputedStyle(host);
|
|
3307
|
+
if (computed.position === 'static' || computed.position === '') host.style.position = 'relative';
|
|
3308
|
+
host.style.isolation = 'isolate';
|
|
3309
|
+
HOST_STYLES.set(host, state);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
function releaseHostStyles(host) {
|
|
3313
|
+
const state = HOST_STYLES.get(host);
|
|
3314
|
+
if (!state) return;
|
|
3315
|
+
state.count -= 1;
|
|
3316
|
+
if (state.count > 0) return;
|
|
3317
|
+
host.style.position = state.position;
|
|
3318
|
+
host.style.isolation = state.isolation;
|
|
3319
|
+
HOST_STYLES.delete(host);
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
/**
|
|
3323
|
+
* Mount the cosmic (nebula) material as a background layer inside `host`.
|
|
3324
|
+
*
|
|
3325
|
+
* The layer is absolutely positioned and defaults to `z-index: -1`, so it
|
|
3326
|
+
* paints behind the host's in-flow content: text/buttons/images inside the
|
|
3327
|
+
* host sit on top with zero extra CSS. Host gets `position: relative;
|
|
3328
|
+
* isolation: isolate` automatically so the layer can never escape its box
|
|
3329
|
+
* (or break stacking outside it).
|
|
3330
|
+
*
|
|
2727
3331
|
* Options: preset (literary name), colors, seed, speed, quality,
|
|
2728
|
-
* renderer,
|
|
2729
|
-
*
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3332
|
+
* renderer, renderScale, powerPreference, fps, paused/static, mouseColor,
|
|
3333
|
+
* respectReducedMotion, opacity (0-1), fallbackColor (true=preset base
|
|
3334
|
+
* color, a hex string, or false to disable).
|
|
3335
|
+
*/
|
|
3336
|
+
function createColorBackground(host, options = {}) {
|
|
3337
|
+
if (!host || typeof host.appendChild !== 'function') {
|
|
3338
|
+
throw new Error('createColorBackground: host element is required');
|
|
3339
|
+
}
|
|
3340
|
+
|
|
2736
3341
|
const preset = { ...getPreset('capsule', options.preset ?? '初光') };
|
|
2737
3342
|
const merged = normalizeOptions(
|
|
2738
|
-
{
|
|
2739
|
-
quality: DEFAULTS.capsule.quality,
|
|
2740
|
-
renderer: DEFAULTS.capsule.renderer,
|
|
2741
|
-
respectReducedMotion: DEFAULTS.capsule.respectReducedMotion,
|
|
2742
|
-
mouseColor: true,
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
3343
|
+
{
|
|
3344
|
+
quality: DEFAULTS.capsule.quality,
|
|
3345
|
+
renderer: DEFAULTS.capsule.renderer,
|
|
3346
|
+
respectReducedMotion: DEFAULTS.capsule.respectReducedMotion,
|
|
3347
|
+
mouseColor: true,
|
|
3348
|
+
renderScale: 1,
|
|
3349
|
+
powerPreference: 'high-performance',
|
|
3350
|
+
fps: DEFAULTS.capsule.fps,
|
|
3351
|
+
paused: false,
|
|
3352
|
+
static: false,
|
|
3353
|
+
opacity: 1,
|
|
3354
|
+
fallbackColor: true
|
|
3355
|
+
},
|
|
3356
|
+
preset,
|
|
2747
3357
|
options
|
|
2748
3358
|
);
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
const
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
syncLayerVars()
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3359
|
+
merged.opacity = Number.isFinite(Number(merged.opacity))
|
|
3360
|
+
? Math.min(1, Math.max(0, Number(merged.opacity)))
|
|
3361
|
+
: 1;
|
|
3362
|
+
const normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
|
|
3363
|
+
if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
3364
|
+
const initialSeed = toFiniteNumber(merged.seed);
|
|
3365
|
+
const initialSpeed = toFiniteNumber(merged.speed);
|
|
3366
|
+
if (initialSeed !== null) preset.seed = initialSeed;
|
|
3367
|
+
if (initialSpeed !== null) preset.speed = initialSpeed;
|
|
3368
|
+
merged.colors = [...preset.colors];
|
|
3369
|
+
merged.seed = preset.seed;
|
|
3370
|
+
merged.speed = preset.speed;
|
|
3371
|
+
const optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
|
|
3372
|
+
let colorOverride = optionColors.length === 4 && optionColors.every(Boolean)
|
|
3373
|
+
? [...optionColors]
|
|
3374
|
+
: null;
|
|
3375
|
+
let seedOverride = options.seed !== undefined ? toFiniteNumber(options.seed) : null;
|
|
3376
|
+
let speedOverride = options.speed !== undefined ? toFiniteNumber(options.speed) : null;
|
|
3377
|
+
|
|
3378
|
+
acquireHostStyles(host);
|
|
3379
|
+
|
|
3380
|
+
const layer = document.createElement('div');
|
|
3381
|
+
layer.className = 'dlc-color-layer';
|
|
3382
|
+
|
|
3383
|
+
const syncLayerVars = () => {
|
|
3384
|
+
layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
|
|
3385
|
+
const fallback =
|
|
3386
|
+
merged.fallbackColor === false || merged.fallbackColor == null
|
|
3387
|
+
? 'transparent'
|
|
3388
|
+
: typeof merged.fallbackColor === 'string'
|
|
3389
|
+
? merged.fallbackColor
|
|
3390
|
+
: preset.colors[0];
|
|
3391
|
+
layer.style.setProperty('--dlc-color-bg', fallback);
|
|
3392
|
+
};
|
|
3393
|
+
syncLayerVars();
|
|
3394
|
+
|
|
3395
|
+
const canvas = document.createElement('canvas');
|
|
3396
|
+
canvas.className = 'dlc-color-canvas';
|
|
3397
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
3398
|
+
layer.appendChild(canvas);
|
|
3399
|
+
host.appendChild(layer);
|
|
3400
|
+
|
|
2781
3401
|
const emitter = createEmitter();
|
|
2782
|
-
let
|
|
3402
|
+
let manuallyPaused = merged.paused === true;
|
|
3403
|
+
let reducedPaused = false;
|
|
3404
|
+
let staticMode = merged.static === true;
|
|
3405
|
+
let contextLost = false;
|
|
2783
3406
|
let disposed = false;
|
|
3407
|
+
let renderOnce = () => {};
|
|
2784
3408
|
const dirty = {
|
|
2785
3409
|
preset: false,
|
|
2786
3410
|
seed: false,
|
|
@@ -2788,70 +3412,113 @@ function createColorBackground(host, options = {}) {
|
|
|
2788
3412
|
colors: false,
|
|
2789
3413
|
opacity: false,
|
|
2790
3414
|
fallbackColor: false,
|
|
2791
|
-
mouseColor: false
|
|
3415
|
+
mouseColor: false,
|
|
3416
|
+
quality: false,
|
|
3417
|
+
renderScale: false,
|
|
3418
|
+
paused: false,
|
|
3419
|
+
static: false,
|
|
3420
|
+
fps: false
|
|
2792
3421
|
};
|
|
2793
|
-
|
|
2794
|
-
let renderer;
|
|
2795
|
-
const useWebgl = merged.renderer !== 'canvas2d';
|
|
2796
|
-
if (useWebgl) {
|
|
2797
|
-
try {
|
|
2798
|
-
renderer = new CosmicRenderer(canvas, merged, {
|
|
2799
|
-
dprCap:
|
|
2800
|
-
mouseColor: merged.mouseColor !== false,
|
|
2801
|
-
eventTarget: host
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3422
|
+
|
|
3423
|
+
let renderer;
|
|
3424
|
+
const useWebgl = merged.renderer !== 'canvas2d';
|
|
3425
|
+
if (useWebgl) {
|
|
3426
|
+
try {
|
|
3427
|
+
renderer = new CosmicRenderer(canvas, merged, {
|
|
3428
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
3429
|
+
mouseColor: merged.mouseColor !== false,
|
|
3430
|
+
eventTarget: host,
|
|
3431
|
+
powerPreference: merged.powerPreference,
|
|
3432
|
+
onContextLost: () => {
|
|
3433
|
+
contextLost = true;
|
|
3434
|
+
emitter.emit('contextlost', {});
|
|
3435
|
+
},
|
|
3436
|
+
onContextRestored: () => {
|
|
3437
|
+
contextLost = false;
|
|
3438
|
+
emitter.emit('contextrestored', {});
|
|
3439
|
+
renderOnce();
|
|
3440
|
+
wakeScheduler();
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
renderer = new FallbackRenderer(canvas, merged, {
|
|
3445
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale)
|
|
3446
|
+
});
|
|
2805
3447
|
nextTick(() => {
|
|
2806
3448
|
if (disposed) return;
|
|
2807
3449
|
emitter.emit('error', {
|
|
2808
3450
|
message: String(error && error.message ? error.message : error)
|
|
2809
3451
|
});
|
|
2810
3452
|
});
|
|
2811
|
-
}
|
|
2812
|
-
} else {
|
|
2813
|
-
renderer = new FallbackRenderer(canvas, merged
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
);
|
|
2833
|
-
|
|
3453
|
+
}
|
|
3454
|
+
} else {
|
|
3455
|
+
renderer = new FallbackRenderer(canvas, merged, {
|
|
3456
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale)
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
renderer.resize();
|
|
3460
|
+
|
|
3461
|
+
let animationTime = 0;
|
|
3462
|
+
const frameGate = createFrameGate(merged.fps);
|
|
3463
|
+
renderOnce = () => renderer.draw(animationTime);
|
|
3464
|
+
|
|
3465
|
+
const resize = () => {
|
|
3466
|
+
renderer.resize();
|
|
3467
|
+
if (manuallyPaused || reducedPaused || staticMode) renderOnce();
|
|
3468
|
+
};
|
|
3469
|
+
const resizeObserver =
|
|
3470
|
+
typeof ResizeObserver !== 'undefined'
|
|
3471
|
+
? new ResizeObserver(resize)
|
|
3472
|
+
: null;
|
|
3473
|
+
if (resizeObserver) resizeObserver.observe(host);
|
|
3474
|
+
else window.addEventListener('resize', resize);
|
|
3475
|
+
|
|
3476
|
+
const visibility = createVisibilityGuard(layer, wakeScheduler);
|
|
3477
|
+
const motionPreference = createReducedMotionPreference(
|
|
3478
|
+
merged.respectReducedMotion,
|
|
3479
|
+
(matches) => {
|
|
3480
|
+
reducedPaused = matches;
|
|
3481
|
+
if (matches) renderOnce();
|
|
3482
|
+
else wakeScheduler();
|
|
3483
|
+
}
|
|
3484
|
+
);
|
|
3485
|
+
reducedPaused = motionPreference.matches();
|
|
3486
|
+
const isMotionPaused = () => manuallyPaused || reducedPaused || staticMode || contextLost;
|
|
3487
|
+
|
|
3488
|
+
renderOnce();
|
|
3489
|
+
const unsubscribe = subscribeScheduler(
|
|
3490
|
+
(delta) => {
|
|
3491
|
+
animationTime += delta;
|
|
3492
|
+
if (frameGate.shouldDraw(delta)) renderer.draw(animationTime);
|
|
3493
|
+
},
|
|
3494
|
+
() => isMotionPaused() || !visibility.isVisible()
|
|
3495
|
+
);
|
|
3496
|
+
|
|
2834
3497
|
nextTick(() => {
|
|
2835
3498
|
if (disposed) return;
|
|
2836
3499
|
emitter.emit('ready', { preset: { ...preset } });
|
|
2837
3500
|
});
|
|
2838
|
-
|
|
2839
|
-
return {
|
|
2840
|
-
element: host,
|
|
2841
|
-
layer,
|
|
2842
|
-
canvas,
|
|
2843
|
-
preset,
|
|
2844
|
-
on: emitter.on,
|
|
2845
|
-
off: emitter.off,
|
|
3501
|
+
|
|
3502
|
+
return {
|
|
3503
|
+
element: host,
|
|
3504
|
+
layer,
|
|
3505
|
+
canvas,
|
|
3506
|
+
preset,
|
|
3507
|
+
on: emitter.on,
|
|
3508
|
+
off: emitter.off,
|
|
2846
3509
|
setPreset(ref) {
|
|
2847
3510
|
const next = getPreset('capsule', ref);
|
|
2848
3511
|
dirty.preset = true;
|
|
2849
3512
|
Object.assign(preset, next);
|
|
3513
|
+
if (colorOverride) preset.colors = [...colorOverride];
|
|
3514
|
+
if (seedOverride !== null) preset.seed = seedOverride;
|
|
3515
|
+
if (speedOverride !== null) preset.speed = speedOverride;
|
|
2850
3516
|
const nextColors = preset.colors.map(normalizeColor);
|
|
2851
3517
|
if (nextColors.every(Boolean)) preset.colors = nextColors;
|
|
2852
3518
|
renderer.setPreset({ ...preset });
|
|
2853
3519
|
renderer.resize();
|
|
2854
3520
|
syncLayerVars();
|
|
3521
|
+
if (isMotionPaused()) renderOnce();
|
|
2855
3522
|
emitter.emit('presetchange', { preset: { ...preset } });
|
|
2856
3523
|
return this;
|
|
2857
3524
|
},
|
|
@@ -2859,25 +3526,31 @@ function createColorBackground(host, options = {}) {
|
|
|
2859
3526
|
const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
|
|
2860
3527
|
if (next.length !== 4 || next.some((color) => !color)) return this;
|
|
2861
3528
|
dirty.colors = true;
|
|
3529
|
+
colorOverride = [...next];
|
|
2862
3530
|
preset.colors = next;
|
|
2863
3531
|
renderer.setPreset({ ...preset, colors: next });
|
|
2864
3532
|
syncLayerVars();
|
|
3533
|
+
if (isMotionPaused()) renderOnce();
|
|
2865
3534
|
return this;
|
|
2866
3535
|
},
|
|
2867
3536
|
setSeed(seed) {
|
|
2868
|
-
const value =
|
|
2869
|
-
if (
|
|
3537
|
+
const value = toFiniteNumber(seed);
|
|
3538
|
+
if (value === null) return this;
|
|
2870
3539
|
dirty.seed = true;
|
|
3540
|
+
seedOverride = value;
|
|
2871
3541
|
preset.seed = value;
|
|
2872
3542
|
renderer.setPreset({ ...preset });
|
|
3543
|
+
if (isMotionPaused()) renderOnce();
|
|
2873
3544
|
return this;
|
|
2874
3545
|
},
|
|
2875
3546
|
setSpeed(speed) {
|
|
2876
|
-
const value =
|
|
2877
|
-
if (
|
|
3547
|
+
const value = toFiniteNumber(speed);
|
|
3548
|
+
if (value === null) return this;
|
|
2878
3549
|
dirty.speed = true;
|
|
3550
|
+
speedOverride = value;
|
|
2879
3551
|
preset.speed = value;
|
|
2880
3552
|
renderer.setPreset({ ...preset });
|
|
3553
|
+
if (isMotionPaused()) renderOnce();
|
|
2881
3554
|
return this;
|
|
2882
3555
|
},
|
|
2883
3556
|
setFallbackColor(value) {
|
|
@@ -2892,45 +3565,88 @@ function createColorBackground(host, options = {}) {
|
|
|
2892
3565
|
if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
|
|
2893
3566
|
return this;
|
|
2894
3567
|
},
|
|
2895
|
-
setQuality(quality) {
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3568
|
+
setQuality(quality) {
|
|
3569
|
+
dirty.quality = true;
|
|
3570
|
+
merged.quality = quality;
|
|
3571
|
+
if (typeof renderer.setDprCap === 'function') {
|
|
3572
|
+
renderer.setDprCap(effectiveDprCap(quality, merged.renderScale));
|
|
3573
|
+
}
|
|
3574
|
+
if (isMotionPaused()) renderOnce();
|
|
3575
|
+
return this;
|
|
3576
|
+
},
|
|
3577
|
+
setRenderScale(renderScale) {
|
|
3578
|
+
const value = Number(renderScale);
|
|
3579
|
+
if (!Number.isFinite(value)) return this;
|
|
3580
|
+
dirty.renderScale = true;
|
|
3581
|
+
merged.renderScale = Math.min(1, Math.max(0.25, value));
|
|
3582
|
+
if (typeof renderer.setDprCap === 'function') {
|
|
3583
|
+
renderer.setDprCap(effectiveDprCap(merged.quality, merged.renderScale));
|
|
3584
|
+
}
|
|
3585
|
+
if (isMotionPaused()) renderOnce();
|
|
3586
|
+
return this;
|
|
3587
|
+
},
|
|
2900
3588
|
setOpacity(value) {
|
|
2901
3589
|
const opacity = Number(value);
|
|
2902
3590
|
if (!Number.isFinite(opacity)) return this;
|
|
2903
3591
|
dirty.opacity = true;
|
|
2904
3592
|
merged.opacity = Math.min(1, Math.max(0, opacity));
|
|
2905
|
-
layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
|
|
2906
|
-
return this;
|
|
2907
|
-
},
|
|
3593
|
+
layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
|
|
3594
|
+
return this;
|
|
3595
|
+
},
|
|
2908
3596
|
randomize() {
|
|
2909
3597
|
this.setSeed(Math.random() * 100);
|
|
2910
3598
|
return this;
|
|
2911
3599
|
},
|
|
2912
|
-
pause() {
|
|
2913
|
-
paused = true;
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
3600
|
+
pause() {
|
|
3601
|
+
dirty.paused = true;
|
|
3602
|
+
manuallyPaused = true;
|
|
3603
|
+
renderOnce();
|
|
3604
|
+
return this;
|
|
3605
|
+
},
|
|
3606
|
+
resume() {
|
|
3607
|
+
dirty.paused = true;
|
|
3608
|
+
manuallyPaused = false;
|
|
3609
|
+
wakeScheduler();
|
|
3610
|
+
return this;
|
|
3611
|
+
},
|
|
3612
|
+
setPaused(value) {
|
|
3613
|
+
return value ? this.pause() : this.resume();
|
|
3614
|
+
},
|
|
3615
|
+
setStatic(value) {
|
|
3616
|
+
dirty.static = true;
|
|
3617
|
+
staticMode = value === true;
|
|
3618
|
+
merged.static = staticMode;
|
|
3619
|
+
if (staticMode) renderOnce();
|
|
3620
|
+
else wakeScheduler();
|
|
3621
|
+
return this;
|
|
3622
|
+
},
|
|
3623
|
+
setFps(fps) {
|
|
3624
|
+
dirty.fps = true;
|
|
3625
|
+
merged.fps = frameGate.setFps(fps);
|
|
3626
|
+
wakeScheduler();
|
|
3627
|
+
return this;
|
|
3628
|
+
},
|
|
2920
3629
|
dispose() {
|
|
2921
3630
|
disposed = true;
|
|
2922
3631
|
unsubscribe();
|
|
2923
|
-
visibility.dispose();
|
|
2924
|
-
|
|
2925
|
-
|
|
3632
|
+
visibility.dispose();
|
|
3633
|
+
motionPreference.dispose();
|
|
3634
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
3635
|
+
else window.removeEventListener('resize', resize);
|
|
2926
3636
|
renderer.dispose();
|
|
2927
3637
|
layer.remove();
|
|
3638
|
+
releaseHostStyles(host);
|
|
2928
3639
|
},
|
|
2929
3640
|
get opacity() { return merged.opacity; },
|
|
2930
3641
|
get fallbackColor() { return merged.fallbackColor; },
|
|
2931
3642
|
get mouseColor() { return merged.mouseColor; },
|
|
3643
|
+
get quality() { return merged.quality; },
|
|
3644
|
+
get renderScale() { return merged.renderScale; },
|
|
3645
|
+
get paused() { return manuallyPaused; },
|
|
3646
|
+
get static() { return staticMode; },
|
|
3647
|
+
get fps() { return frameGate.getFps(); },
|
|
2932
3648
|
dirty
|
|
2933
3649
|
};
|
|
2934
3650
|
}
|
|
2935
3651
|
|
|
2936
|
-
export { ALL_PRESETS, COPY, CosmicRenderer, DEFAULTS, FallbackRenderer, PRESETS, PROGRESS_PRESETS, ProgressCapsuleController, ProgressFlowRenderer, QUALITY_TIERS, createCapsule, createColorBackground, createProgressCapsule, dprCapFor, getPreset, hexToRgb01$1 as hexToRgb01, hexToRgba, normalizeColor, parseSize, pauseAll, resumeAll, validateColors };
|
|
3652
|
+
export { ALL_PRESETS, COPY, CosmicRenderer, DEFAULTS, FallbackRenderer, PRESETS, PROGRESS_PRESETS, ProgressCapsuleController, ProgressFlowRenderer, QUALITY_TIERS, createCapsule, createColorBackground, createProgressCapsule, dprCapFor, effectiveDprCap, getPreset, hexToRgb01$1 as hexToRgb01, hexToRgba, normalizeColor, parseSize, pauseAll, resumeAll, validateColors };
|