@5even7/dlc-ui 0.2.17 → 0.2.19
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 +147 -123
- package/LICENSE +21 -21
- package/README.md +338 -316
- package/THIRD_PARTY_NOTICES.md +57 -0
- package/dist/avatar-creator.cjs +7221 -0
- package/dist/avatar-creator.mjs +9278 -0
- package/dist/capsule.cjs +189 -68
- package/dist/capsule.mjs +1208 -1103
- package/dist/color.cjs +186 -67
- package/dist/color.mjs +1157 -1052
- package/dist/emo.cjs +1439 -410
- package/dist/emo.mjs +1405 -496
- package/dist/index.cjs +7808 -310
- package/dist/index.mjs +13435 -3802
- package/dist/index.umd.js +7786 -288
- package/dist/index.umd.min.js +1 -1
- package/dist/progress.cjs +195 -76
- package/dist/progress.mjs +2293 -2188
- package/dist/vue2.cjs +7827 -376
- package/dist/vue2.mjs +13841 -4267
- package/dist/vue3.cjs +7827 -376
- package/dist/vue3.mjs +13841 -4267
- package/package.json +146 -135
- package/styles/avatar-creator.css +13 -0
- package/styles/base.css +32 -32
- package/styles/color.css +18 -18
- package/styles/emo.css +89 -33
- package/styles/progress.css +96 -96
- package/styles/theme.css +1 -0
- package/types/avatar-creator.d.ts +124 -0
- package/types/capsule.d.ts +15 -15
- package/types/color.d.ts +15 -15
- package/types/emo.d.ts +37 -14
- package/types/index.d.ts +581 -417
- package/types/progress.d.ts +16 -16
- package/types/vue2.d.ts +11 -10
- package/types/vue3.d.ts +169 -144
package/dist/progress.mjs
CHANGED
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny event emitter. Accepts any event name so the API stays open for
|
|
3
|
-
* future events (hover, click, custom) without breaking changes.
|
|
4
|
-
*/
|
|
5
|
-
function createEmitter() {
|
|
6
|
-
// Plain object storage (no Map/Set) so the legacy build has no API
|
|
7
|
-
// dependencies beyond what IE11 provides.
|
|
8
|
-
const listeners = Object.create(null);
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
on(event, fn) {
|
|
12
|
-
if (!listeners[event]) listeners[event] = [];
|
|
13
|
-
listeners[event].push(fn);
|
|
14
|
-
return () => {
|
|
15
|
-
const current = listeners[event];
|
|
16
|
-
if (current) {
|
|
17
|
-
const index = current.indexOf(fn);
|
|
18
|
-
if (index !== -1) current.splice(index, 1);
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
off(event, fn) {
|
|
23
|
-
const current = listeners[event];
|
|
24
|
-
if (!current) return false;
|
|
25
|
-
const index = current.indexOf(fn);
|
|
26
|
-
if (index === -1) return false;
|
|
27
|
-
current.splice(index, 1);
|
|
28
|
-
return true;
|
|
29
|
-
},
|
|
30
|
-
emit(event, ...args) {
|
|
31
|
-
const current = listeners[event];
|
|
32
|
-
if (!current) return;
|
|
33
|
-
for (const fn of current.slice()) fn(...args);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Tiny event emitter. Accepts any event name so the API stays open for
|
|
3
|
+
* future events (hover, click, custom) without breaking changes.
|
|
4
|
+
*/
|
|
5
|
+
function createEmitter() {
|
|
6
|
+
// Plain object storage (no Map/Set) so the legacy build has no API
|
|
7
|
+
// dependencies beyond what IE11 provides.
|
|
8
|
+
const listeners = Object.create(null);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
on(event, fn) {
|
|
12
|
+
if (!listeners[event]) listeners[event] = [];
|
|
13
|
+
listeners[event].push(fn);
|
|
14
|
+
return () => {
|
|
15
|
+
const current = listeners[event];
|
|
16
|
+
if (current) {
|
|
17
|
+
const index = current.indexOf(fn);
|
|
18
|
+
if (index !== -1) current.splice(index, 1);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
off(event, fn) {
|
|
23
|
+
const current = listeners[event];
|
|
24
|
+
if (!current) return false;
|
|
25
|
+
const index = current.indexOf(fn);
|
|
26
|
+
if (index === -1) return false;
|
|
27
|
+
current.splice(index, 1);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
emit(event, ...args) {
|
|
31
|
+
const current = listeners[event];
|
|
32
|
+
if (!current) return;
|
|
33
|
+
for (const fn of current.slice()) fn(...args);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
36
|
}
|
|
37
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;
|
|
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
57
|
}
|
|
58
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);
|
|
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;
|
|
82
70
|
}
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 };
|
|
72
|
+
function dprCapFor(quality) {
|
|
73
|
+
if (quality === 'auto') return autoDprCap();
|
|
74
|
+
const tier = QUALITY_TIERS[quality] || QUALITY_TIERS.medium;
|
|
75
|
+
return tier.dpr;
|
|
102
76
|
}
|
|
103
77
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
const PROGRESS_PRESETS = [
|
|
105
|
+
{
|
|
106
|
+
id: 'model-training',
|
|
107
|
+
code: 'NC-10',
|
|
108
|
+
name: '星火',
|
|
109
|
+
subtitle: 'SHA 4.5 + 100 2026 TKN',
|
|
110
|
+
group: 'progress',
|
|
111
|
+
initialProgress: 43,
|
|
112
|
+
edgeStyle: 'flow',
|
|
113
|
+
colors: ['#2B1025', '#FF3F94', '#FF8A3D', '#FFF06A']
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'agent-migration',
|
|
117
|
+
code: 'NC-11',
|
|
118
|
+
name: '迁流',
|
|
119
|
+
subtitle: 'TRANSFERRING PROTOCOL',
|
|
120
|
+
group: 'progress',
|
|
121
|
+
initialProgress: 33,
|
|
122
|
+
edgeStyle: 'flow',
|
|
123
|
+
colors: ['#101C37', '#245BFF', '#00CFFF', '#5DFFE6']
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'visual-training',
|
|
127
|
+
code: 'NC-12',
|
|
128
|
+
name: '幻境',
|
|
129
|
+
subtitle: 'GENERATING POWER ++',
|
|
130
|
+
group: 'progress',
|
|
131
|
+
initialProgress: 58,
|
|
132
|
+
edgeStyle: 'flow',
|
|
133
|
+
colors: ['#21142D', '#7042FF', '#42F58D', '#C4FF8A']
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'tide',
|
|
137
|
+
code: 'NC-13',
|
|
138
|
+
name: '汐潮',
|
|
139
|
+
subtitle: 'MOON PULL / COAST',
|
|
140
|
+
group: 'progress',
|
|
141
|
+
initialProgress: 30,
|
|
142
|
+
edgeStyle: 'tide',
|
|
143
|
+
colors: ['#0A2239', '#2E9BFF', '#7FE3FF', '#EAF9FF']
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'botanic-lab',
|
|
147
|
+
code: 'NC-14',
|
|
148
|
+
name: '森息',
|
|
149
|
+
subtitle: 'MOSS CULTURE / ROOTING',
|
|
150
|
+
group: 'progress',
|
|
151
|
+
initialProgress: 46,
|
|
152
|
+
edgeStyle: 'flow',
|
|
153
|
+
colors: ['#102A23', '#287A58', '#9BD65B', '#F2FFC4']
|
|
154
|
+
}
|
|
155
155
|
];
|
|
156
156
|
|
|
157
157
|
/**
|
|
@@ -174,31 +174,31 @@ function getPreset(kind, ref) {
|
|
|
174
174
|
return found;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
const DEFAULTS = {
|
|
178
|
-
progress: {
|
|
179
|
-
width: 454,
|
|
180
|
-
height: 104,
|
|
181
|
-
min: 0,
|
|
182
|
-
max: 100,
|
|
183
|
-
step: 'any',
|
|
184
|
-
draggable: true,
|
|
185
|
-
keyboard: true,
|
|
186
|
-
disabled: false,
|
|
187
|
-
readonly: false,
|
|
188
|
-
direction: 'ltr',
|
|
189
|
-
precision: 0,
|
|
190
|
-
valueSuffix: '%',
|
|
191
|
-
edgeStyle: 'flow',
|
|
192
|
-
quality: 'auto',
|
|
193
|
-
renderer: 'auto',
|
|
194
|
-
renderScale: 1,
|
|
195
|
-
powerPreference: 'high-performance',
|
|
196
|
-
fps: 60,
|
|
197
|
-
paused: false,
|
|
198
|
-
static: false,
|
|
199
|
-
textRatio: 54,
|
|
200
|
-
showValue: true,
|
|
201
|
-
respectReducedMotion: true
|
|
177
|
+
const DEFAULTS = {
|
|
178
|
+
progress: {
|
|
179
|
+
width: 454,
|
|
180
|
+
height: 104,
|
|
181
|
+
min: 0,
|
|
182
|
+
max: 100,
|
|
183
|
+
step: 'any',
|
|
184
|
+
draggable: true,
|
|
185
|
+
keyboard: true,
|
|
186
|
+
disabled: false,
|
|
187
|
+
readonly: false,
|
|
188
|
+
direction: 'ltr',
|
|
189
|
+
precision: 0,
|
|
190
|
+
valueSuffix: '%',
|
|
191
|
+
edgeStyle: 'flow',
|
|
192
|
+
quality: 'auto',
|
|
193
|
+
renderer: 'auto',
|
|
194
|
+
renderScale: 1,
|
|
195
|
+
powerPreference: 'high-performance',
|
|
196
|
+
fps: 60,
|
|
197
|
+
paused: false,
|
|
198
|
+
static: false,
|
|
199
|
+
textRatio: 54,
|
|
200
|
+
showValue: true,
|
|
201
|
+
respectReducedMotion: true
|
|
202
202
|
}};
|
|
203
203
|
|
|
204
204
|
/**
|
|
@@ -209,16 +209,22 @@ const DEFAULTS = {
|
|
|
209
209
|
const COPY = {
|
|
210
210
|
brandName: '画境观屿',
|
|
211
211
|
valueSuffix: '%',
|
|
212
|
-
progressAria: '{brand} {code} 加载进度',
|
|
213
|
-
capsuleAria: '打开 {name} 沉浸预览'
|
|
212
|
+
progressAria: '{brand} {code} 加载进度',
|
|
213
|
+
capsuleAria: '打开 {name} 沉浸预览'
|
|
214
214
|
};
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
217
|
* Color helpers. `normalizeColor` converts any supported CSS color
|
|
218
|
-
* (hex / named / rgb() / rgba()) into a 6-digit hex string,
|
|
219
|
-
* keep working with #rrggbb only. rgba() alpha is
|
|
220
|
-
* the WebGL shader has no per-color alpha channel, and
|
|
221
|
-
* opaque by design — use component-level `opacity` for
|
|
218
|
+
* (hex / named / rgb() / rgba() / hsl() / hsla()) into a 6-digit hex string,
|
|
219
|
+
* so renderers can keep working with #rrggbb only. rgba()/hsla() alpha is
|
|
220
|
+
* intentionally dropped: the WebGL shader has no per-color alpha channel, and
|
|
221
|
+
* the component is opaque by design — use component-level `opacity` for
|
|
222
|
+
* transparency.
|
|
223
|
+
*
|
|
224
|
+
* `normalizeColorWithAlpha` keeps the alpha channel: it returns a 6-digit hex
|
|
225
|
+
* when alpha is 1, an 8-digit #rrggbbaa hex when alpha < 1, and the literal
|
|
226
|
+
* string 'transparent' for `transparent`. Use it for renderers (e.g. SVG)
|
|
227
|
+
* that can represent per-color transparency.
|
|
222
228
|
*/
|
|
223
229
|
|
|
224
230
|
const NAMED_COLORS = {
|
|
@@ -376,46 +382,145 @@ function clampChannel(value) {
|
|
|
376
382
|
return Math.min(255, Math.max(0, Math.round(value)));
|
|
377
383
|
}
|
|
378
384
|
|
|
379
|
-
function
|
|
380
|
-
|
|
385
|
+
function clampUnit(value) {
|
|
386
|
+
return Math.min(1, Math.max(0, value));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function hexPair(value) {
|
|
390
|
+
const text = clampChannel(value).toString(16);
|
|
391
|
+
return text.length === 1 ? `0${text}` : text;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Parse the argument list of rgb()/rgba()/hsl()/hsla() (comma syntax or
|
|
396
|
+
* modern space + slash syntax). Returns { parts, alpha } where alpha is 1 when
|
|
397
|
+
* omitted. Returns null when the argument list cannot be parsed.
|
|
398
|
+
*/
|
|
399
|
+
function parseFunctionArgs(args) {
|
|
400
|
+
const parts = args
|
|
401
|
+
.split(/[,\s]+/)
|
|
402
|
+
.map((part) => part.trim())
|
|
403
|
+
.filter((part) => part !== '' && part !== '/');
|
|
381
404
|
if (parts.length < 3) return null;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
};
|
|
396
|
-
|
|
405
|
+
let alpha = 1;
|
|
406
|
+
if (parts.length >= 4) {
|
|
407
|
+
const alphaText = parts.pop().trim();
|
|
408
|
+
if (alphaText.endsWith('%')) {
|
|
409
|
+
const percent = parseFloat(alphaText);
|
|
410
|
+
if (!Number.isFinite(percent)) return null;
|
|
411
|
+
alpha = clampUnit(percent / 100);
|
|
412
|
+
} else {
|
|
413
|
+
const number = Number(alphaText);
|
|
414
|
+
if (!Number.isFinite(number)) return null;
|
|
415
|
+
alpha = clampUnit(number);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { parts, alpha };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function parseChannel(value) {
|
|
422
|
+
if (typeof value !== 'string' || value === '') return null;
|
|
423
|
+
const text = value.trim();
|
|
424
|
+
if (text.endsWith('%')) return clampChannel((parseFloat(text) / 100) * 255);
|
|
425
|
+
const number = Number(text);
|
|
426
|
+
return Number.isFinite(number) ? clampChannel(number) : null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parsePercent(value) {
|
|
430
|
+
if (typeof value !== 'string' || value === '') return null;
|
|
431
|
+
const text = value.trim();
|
|
432
|
+
if (!text.endsWith('%')) return null;
|
|
433
|
+
const number = parseFloat(text);
|
|
434
|
+
return Number.isFinite(number) ? Math.min(100, Math.max(0, number)) : null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseHue(value) {
|
|
438
|
+
if (typeof value !== 'string' || value === '') return null;
|
|
439
|
+
const number = parseFloat(value);
|
|
440
|
+
return Number.isFinite(number) ? number : null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Standard HSL -> RGB. h in degrees, s/l in 0..100, returns r/g/b in 0..255. */
|
|
444
|
+
function hslToRgb(h, s, l) {
|
|
445
|
+
const hue = ((h % 360) + 360) % 360 / 360;
|
|
446
|
+
const saturation = s / 100;
|
|
447
|
+
const lightness = l / 100;
|
|
448
|
+
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
|
|
449
|
+
const section = hue * 6;
|
|
450
|
+
const x = chroma * (1 - Math.abs((section % 2) - 1));
|
|
451
|
+
let red = 0;
|
|
452
|
+
let green = 0;
|
|
453
|
+
let blue = 0;
|
|
454
|
+
if (section < 1) { red = chroma; green = x; }
|
|
455
|
+
else if (section < 2) { red = x; green = chroma; }
|
|
456
|
+
else if (section < 3) { green = chroma; blue = x; }
|
|
457
|
+
else if (section < 4) { green = x; blue = chroma; }
|
|
458
|
+
else if (section < 5) { red = x; blue = chroma; }
|
|
459
|
+
else { red = chroma; blue = x; }
|
|
460
|
+
const match = lightness - chroma / 2;
|
|
461
|
+
return [
|
|
462
|
+
Math.round((red + match) * 255),
|
|
463
|
+
Math.round((green + match) * 255),
|
|
464
|
+
Math.round((blue + match) * 255)
|
|
465
|
+
];
|
|
397
466
|
}
|
|
398
467
|
|
|
399
468
|
/**
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
* including percentages. Alpha in rgba() is ignored.
|
|
469
|
+
* Parse any supported CSS color. When keepAlpha is true the alpha channel is
|
|
470
|
+
* preserved (8-digit hex for alpha < 1); when false it is dropped. 'transparent'
|
|
471
|
+
* is only preserved when keepAlpha is true.
|
|
404
472
|
*/
|
|
405
|
-
function
|
|
473
|
+
function parseColor(value, keepAlpha) {
|
|
406
474
|
if (typeof value !== 'string') return null;
|
|
407
475
|
const input = value.trim();
|
|
408
476
|
if (!input) return null;
|
|
409
477
|
const lower = input.toLowerCase();
|
|
410
478
|
if (NAMED_COLORS[lower]) return NAMED_COLORS[lower];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
479
|
+
// #rgb / #rgba / #rrggbb / #rrggbbaa,以及无 # 前缀的 6/8 位 hex(dicebear 习惯)
|
|
480
|
+
const hexMatch = input.match(/^#?([0-9a-f]{3,8})$/i);
|
|
481
|
+
if (hexMatch) {
|
|
482
|
+
let hex = hexMatch[1].toLowerCase();
|
|
483
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
484
|
+
hex = hex.split('').map((c) => c + c).join('');
|
|
485
|
+
}
|
|
486
|
+
return `#${hex.slice(0, 6)}`;
|
|
487
|
+
}
|
|
488
|
+
const funcMatch = input.match(/^(rgba?|hsla?)\(([^)]*)\)$/i);
|
|
489
|
+
if (!funcMatch) return null;
|
|
490
|
+
const kind = funcMatch[1].toLowerCase();
|
|
491
|
+
const parsed = parseFunctionArgs(funcMatch[2]);
|
|
492
|
+
if (!parsed) return null;
|
|
493
|
+
const { parts, alpha } = parsed;
|
|
494
|
+
let red;
|
|
495
|
+
let green;
|
|
496
|
+
let blue;
|
|
497
|
+
if (kind === 'rgb' || kind === 'rgba') {
|
|
498
|
+
if (parts.length !== 3) return null;
|
|
499
|
+
red = parseChannel(parts[0]);
|
|
500
|
+
green = parseChannel(parts[1]);
|
|
501
|
+
blue = parseChannel(parts[2]);
|
|
502
|
+
if (red === null || green === null || blue === null) return null;
|
|
503
|
+
} else {
|
|
504
|
+
if (parts.length !== 3) return null;
|
|
505
|
+
const hue = parseHue(parts[0]);
|
|
506
|
+
const saturation = parsePercent(parts[1]);
|
|
507
|
+
const lightness = parsePercent(parts[2]);
|
|
508
|
+
if (hue === null || saturation === null || lightness === null) return null;
|
|
509
|
+
[red, green, blue] = hslToRgb(hue, saturation, lightness);
|
|
510
|
+
}
|
|
511
|
+
const hex = `#${hexPair(red)}${hexPair(green)}${hexPair(blue)}`;
|
|
512
|
+
return hex;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Convert any supported CSS color into `#rrggbb`. Returns null when the
|
|
517
|
+
* value cannot be parsed. Supports: #rgb / #rgba / #rrggbb / #rrggbbaa
|
|
518
|
+
* (case-insensitive), bare 6/8-digit hex, CSS named colors, rgb() / rgba() /
|
|
519
|
+
* hsl() / hsla() with comma or modern space syntax, including percentages.
|
|
520
|
+
* Alpha is ignored.
|
|
521
|
+
*/
|
|
522
|
+
function normalizeColor(value) {
|
|
523
|
+
return parseColor(value);
|
|
419
524
|
}
|
|
420
525
|
|
|
421
526
|
function hexToRgba(color, alpha = 1) {
|
|
@@ -428,781 +533,781 @@ function hexToRgba(color, alpha = 1) {
|
|
|
428
533
|
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
|
429
534
|
}
|
|
430
535
|
|
|
431
|
-
/**
|
|
432
|
-
* Document-level shared rAF scheduler. Every component instance subscribes
|
|
433
|
-
* its own frame callback; the whole page runs ONE animation loop (like the
|
|
434
|
-
* original demo), which avoids jank from many competing rAF loops.
|
|
435
|
-
*/
|
|
436
|
-
const subscribers = [];
|
|
437
|
-
let running = false;
|
|
438
|
-
let rafId = 0;
|
|
439
|
-
let last = 0;
|
|
440
|
-
|
|
441
|
-
function tick(now) {
|
|
442
|
-
if (!running) return;
|
|
443
|
-
const activeItems = [];
|
|
444
|
-
{
|
|
445
|
-
for (const item of subscribers.slice()) {
|
|
446
|
-
try {
|
|
447
|
-
if (!item.isPaused()) activeItems.push(item);
|
|
448
|
-
} catch {}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (activeItems.length === 0) {
|
|
452
|
-
running = false;
|
|
453
|
-
rafId = 0;
|
|
454
|
-
last = 0;
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
const delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
|
|
458
|
-
last = now;
|
|
459
|
-
// Schedule the next frame BEFORE running callbacks so one throwing
|
|
460
|
-
// subscriber can never kill the whole animation loop.
|
|
461
|
-
rafId = requestAnimationFrame(tick);
|
|
462
|
-
for (const item of activeItems) {
|
|
463
|
-
try {
|
|
464
|
-
item.onFrame(delta, now);
|
|
465
|
-
} catch (error) {
|
|
466
|
-
console.warn('[dlc-ui] frame error:', error);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
if (subscribers.length === 0) {
|
|
470
|
-
cancelAnimationFrame(rafId);
|
|
471
|
-
running = false;
|
|
472
|
-
rafId = 0;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function start() {
|
|
477
|
-
if (running) return;
|
|
478
|
-
running = true;
|
|
479
|
-
last = 0;
|
|
480
|
-
rafId = requestAnimationFrame(tick);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function wakeScheduler() {
|
|
484
|
-
start();
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function subscribeScheduler(onFrame, isPaused) {
|
|
488
|
-
const item = { onFrame, isPaused };
|
|
489
|
-
subscribers.push(item);
|
|
490
|
-
start();
|
|
491
|
-
return () => {
|
|
492
|
-
const index = subscribers.indexOf(item);
|
|
493
|
-
if (index !== -1) subscribers.splice(index, 1);
|
|
494
|
-
if (subscribers.length === 0 && rafId) {
|
|
495
|
-
cancelAnimationFrame(rafId);
|
|
496
|
-
running = false;
|
|
497
|
-
rafId = 0;
|
|
498
|
-
}
|
|
499
|
-
};
|
|
536
|
+
/**
|
|
537
|
+
* Document-level shared rAF scheduler. Every component instance subscribes
|
|
538
|
+
* its own frame callback; the whole page runs ONE animation loop (like the
|
|
539
|
+
* original demo), which avoids jank from many competing rAF loops.
|
|
540
|
+
*/
|
|
541
|
+
const subscribers = [];
|
|
542
|
+
let running = false;
|
|
543
|
+
let rafId = 0;
|
|
544
|
+
let last = 0;
|
|
545
|
+
|
|
546
|
+
function tick(now) {
|
|
547
|
+
if (!running) return;
|
|
548
|
+
const activeItems = [];
|
|
549
|
+
{
|
|
550
|
+
for (const item of subscribers.slice()) {
|
|
551
|
+
try {
|
|
552
|
+
if (!item.isPaused()) activeItems.push(item);
|
|
553
|
+
} catch {}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (activeItems.length === 0) {
|
|
557
|
+
running = false;
|
|
558
|
+
rafId = 0;
|
|
559
|
+
last = 0;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
|
|
563
|
+
last = now;
|
|
564
|
+
// Schedule the next frame BEFORE running callbacks so one throwing
|
|
565
|
+
// subscriber can never kill the whole animation loop.
|
|
566
|
+
rafId = requestAnimationFrame(tick);
|
|
567
|
+
for (const item of activeItems) {
|
|
568
|
+
try {
|
|
569
|
+
item.onFrame(delta, now);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.warn('[dlc-ui] frame error:', error);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (subscribers.length === 0) {
|
|
575
|
+
cancelAnimationFrame(rafId);
|
|
576
|
+
running = false;
|
|
577
|
+
rafId = 0;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function start() {
|
|
582
|
+
if (running) return;
|
|
583
|
+
running = true;
|
|
584
|
+
last = 0;
|
|
585
|
+
rafId = requestAnimationFrame(tick);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function wakeScheduler() {
|
|
589
|
+
start();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function subscribeScheduler(onFrame, isPaused) {
|
|
593
|
+
const item = { onFrame, isPaused };
|
|
594
|
+
subscribers.push(item);
|
|
595
|
+
start();
|
|
596
|
+
return () => {
|
|
597
|
+
const index = subscribers.indexOf(item);
|
|
598
|
+
if (index !== -1) subscribers.splice(index, 1);
|
|
599
|
+
if (subscribers.length === 0 && rafId) {
|
|
600
|
+
cancelAnimationFrame(rafId);
|
|
601
|
+
running = false;
|
|
602
|
+
rafId = 0;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Gates drawing on "element intersects viewport AND the page tab is visible".
|
|
609
|
+
* Falls back to always-visible when IntersectionObserver is unavailable.
|
|
610
|
+
*/
|
|
611
|
+
function createVisibilityGuard(element, onChange = null) {
|
|
612
|
+
let intersecting = true;
|
|
613
|
+
let pageVisible = typeof document === 'undefined' || !document.hidden;
|
|
614
|
+
let disposed = false;
|
|
615
|
+
let observer = null;
|
|
616
|
+
|
|
617
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
618
|
+
observer = new IntersectionObserver(
|
|
619
|
+
(entries) => {
|
|
620
|
+
intersecting = entries.some((entry) => entry.isIntersecting);
|
|
621
|
+
if (typeof onChange === 'function') onChange();
|
|
622
|
+
},
|
|
623
|
+
{ rootMargin: '180px' }
|
|
624
|
+
);
|
|
625
|
+
observer.observe(element);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const onVisibilityChange = () => {
|
|
629
|
+
pageVisible = typeof document !== 'undefined' && !document.hidden;
|
|
630
|
+
if (typeof onChange === 'function') onChange();
|
|
631
|
+
};
|
|
632
|
+
if (typeof document !== 'undefined') {
|
|
633
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
isVisible() {
|
|
638
|
+
return intersecting && pageVisible;
|
|
639
|
+
},
|
|
640
|
+
dispose() {
|
|
641
|
+
if (disposed) return;
|
|
642
|
+
disposed = true;
|
|
643
|
+
if (observer) observer.disconnect();
|
|
644
|
+
if (typeof document !== 'undefined') {
|
|
645
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
};
|
|
500
649
|
}
|
|
501
650
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const onVisibilityChange = () => {
|
|
524
|
-
pageVisible = typeof document !== 'undefined' && !document.hidden;
|
|
525
|
-
if (typeof onChange === 'function') onChange();
|
|
526
|
-
};
|
|
527
|
-
if (typeof document !== 'undefined') {
|
|
528
|
-
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return {
|
|
532
|
-
isVisible() {
|
|
533
|
-
return intersecting && pageVisible;
|
|
534
|
-
},
|
|
535
|
-
dispose() {
|
|
536
|
-
if (disposed) return;
|
|
537
|
-
disposed = true;
|
|
538
|
-
if (observer) observer.disconnect();
|
|
539
|
-
if (typeof document !== 'undefined') {
|
|
540
|
-
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
};
|
|
651
|
+
const PROGRESS_MOTION_WIDTH = 240;
|
|
652
|
+
const PROGRESS_MOTION_HEIGHT = 80;
|
|
653
|
+
const PROGRESS_MOTION_DURATION = 12.0;
|
|
654
|
+
const PROGRESS_MOTION_MAX_PX = 40.0;
|
|
655
|
+
|
|
656
|
+
const PROFILE_CONFIG = {
|
|
657
|
+
'model-training': { seed: 0.37, broad: 0.58, middle: 0.25, detail: 0.13, lobe: 0.24 },
|
|
658
|
+
'agent-migration': { seed: 1.71, broad: 0.72, middle: 0.10, detail: 0.03, lobe: 0.18 },
|
|
659
|
+
'visual-training': { seed: 2.83, broad: 0.66, middle: 0.16, detail: 0.06, lobe: 0.23 },
|
|
660
|
+
'botanic-lab': { seed: 3.46, broad: 0.54, middle: 0.18, detail: 0.05, lobe: 0.28 },
|
|
661
|
+
// ponytail: first-pass tide = asymmetric time warp on the same motion pipeline.
|
|
662
|
+
// Refine (foam line / wash streaks) in the shader after visual QA.
|
|
663
|
+
'tide': { seed: 4.12, broad: 0.82, middle: 0.07, detail: 0.02, lobe: 0.30, warp: 0.5 }
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const CACHE$1 = Object.create(null);
|
|
667
|
+
|
|
668
|
+
function gaussian(value, center, width) {
|
|
669
|
+
const delta = (value - center) / Math.max(width, 0.001);
|
|
670
|
+
return Math.exp(-delta * delta);
|
|
544
671
|
}
|
|
545
672
|
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
for (let x = 0; x < PROGRESS_MOTION_WIDTH; x += 1) {
|
|
575
|
-
let time = (x / PROGRESS_MOTION_WIDTH) * Math.PI * 2;
|
|
576
|
-
if (profile.warp) time += profile.warp * Math.sin(time * 2);
|
|
577
|
-
const centerA = 0.28 + Math.sin(time * 0.53 + profile.seed) * 0.13;
|
|
578
|
-
const centerB = 0.70 + Math.cos(time * 0.47 + profile.seed * 1.7) * 0.12;
|
|
579
|
-
|
|
580
|
-
for (let y = 0; y < PROGRESS_MOTION_HEIGHT; y += 1) {
|
|
581
|
-
const ratio = y / Math.max(PROGRESS_MOTION_HEIGHT - 1, 1);
|
|
582
|
-
const envelope = Math.pow(Math.max(Math.sin(Math.PI * ratio), 0), 0.48);
|
|
583
|
-
const broad = Math.sin(ratio * Math.PI * 2 * 1.35 + time * 0.58 + profile.seed) * profile.broad;
|
|
584
|
-
const middle = Math.sin(ratio * Math.PI * 2 * 3.2 - time * 0.91 + profile.seed * 2.1) * profile.middle;
|
|
585
|
-
const detail = Math.sin(ratio * Math.PI * 2 * 6.1 + time * 1.31 + profile.seed * 3.2) * profile.detail;
|
|
586
|
-
const lobes = (
|
|
587
|
-
gaussian(ratio, centerA, 0.09) * Math.sin(time * 1.11 + profile.seed * 4.0) -
|
|
588
|
-
gaussian(ratio, centerB, 0.10) * Math.cos(time * 0.97 + profile.seed * 3.3)
|
|
589
|
-
) * profile.lobe;
|
|
590
|
-
const normalized = Math.max(-1, Math.min(1, (broad + middle + detail + lobes) * envelope));
|
|
591
|
-
data[y * PROGRESS_MOTION_WIDTH + x] = Math.round((normalized * 0.5 + 0.5) * 255);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
return data;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function getProgressMotionData(id, edgeStyle = 'flow') {
|
|
599
|
-
const key = `${id}:${edgeStyle}`;
|
|
600
|
-
if (!CACHE$1[key]) CACHE$1[key] = createMotionData(id, edgeStyle);
|
|
601
|
-
return CACHE$1[key];
|
|
673
|
+
function createMotionData(id, edgeStyle = 'flow') {
|
|
674
|
+
const profile = edgeStyle === 'tide'
|
|
675
|
+
? PROFILE_CONFIG.tide
|
|
676
|
+
: (PROFILE_CONFIG[id] || PROFILE_CONFIG['visual-training']);
|
|
677
|
+
const data = new Uint8Array(PROGRESS_MOTION_WIDTH * PROGRESS_MOTION_HEIGHT);
|
|
678
|
+
|
|
679
|
+
for (let x = 0; x < PROGRESS_MOTION_WIDTH; x += 1) {
|
|
680
|
+
let time = (x / PROGRESS_MOTION_WIDTH) * Math.PI * 2;
|
|
681
|
+
if (profile.warp) time += profile.warp * Math.sin(time * 2);
|
|
682
|
+
const centerA = 0.28 + Math.sin(time * 0.53 + profile.seed) * 0.13;
|
|
683
|
+
const centerB = 0.70 + Math.cos(time * 0.47 + profile.seed * 1.7) * 0.12;
|
|
684
|
+
|
|
685
|
+
for (let y = 0; y < PROGRESS_MOTION_HEIGHT; y += 1) {
|
|
686
|
+
const ratio = y / Math.max(PROGRESS_MOTION_HEIGHT - 1, 1);
|
|
687
|
+
const envelope = Math.pow(Math.max(Math.sin(Math.PI * ratio), 0), 0.48);
|
|
688
|
+
const broad = Math.sin(ratio * Math.PI * 2 * 1.35 + time * 0.58 + profile.seed) * profile.broad;
|
|
689
|
+
const middle = Math.sin(ratio * Math.PI * 2 * 3.2 - time * 0.91 + profile.seed * 2.1) * profile.middle;
|
|
690
|
+
const detail = Math.sin(ratio * Math.PI * 2 * 6.1 + time * 1.31 + profile.seed * 3.2) * profile.detail;
|
|
691
|
+
const lobes = (
|
|
692
|
+
gaussian(ratio, centerA, 0.09) * Math.sin(time * 1.11 + profile.seed * 4.0) -
|
|
693
|
+
gaussian(ratio, centerB, 0.10) * Math.cos(time * 0.97 + profile.seed * 3.3)
|
|
694
|
+
) * profile.lobe;
|
|
695
|
+
const normalized = Math.max(-1, Math.min(1, (broad + middle + detail + lobes) * envelope));
|
|
696
|
+
data[y * PROGRESS_MOTION_WIDTH + x] = Math.round((normalized * 0.5 + 0.5) * 255);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return data;
|
|
602
701
|
}
|
|
603
702
|
|
|
604
|
-
function
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
((value >> 8) & 255) / 255,
|
|
609
|
-
(value & 255) / 255
|
|
610
|
-
];
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function stringSeed$1(value) {
|
|
614
|
-
let hash = 2166136261;
|
|
615
|
-
for (const character of value) {
|
|
616
|
-
hash ^= character.charCodeAt(0);
|
|
617
|
-
hash = Math.imul(hash, 16777619);
|
|
618
|
-
}
|
|
619
|
-
return (hash >>> 0) / 4294967295;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const PROFILE_INDEX = {
|
|
623
|
-
'model-training': 0,
|
|
624
|
-
'agent-migration': 1,
|
|
625
|
-
'visual-training': 2,
|
|
626
|
-
'tide': 3
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
const MOTION_SCALE_FACTORS = {
|
|
630
|
-
'model-training': 1.05,
|
|
631
|
-
'agent-migration': 1.04,
|
|
632
|
-
'visual-training': 1.04,
|
|
633
|
-
'tide': 1.18
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
const VERTEX_SHADER = `#version 300 es
|
|
637
|
-
in vec2 a_position;
|
|
638
|
-
out vec2 v_uv;
|
|
639
|
-
void main() {
|
|
640
|
-
v_uv = a_position * 0.5 + 0.5;
|
|
641
|
-
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
642
|
-
}`;
|
|
643
|
-
|
|
644
|
-
const FRAGMENT_SHADER = `#version 300 es
|
|
645
|
-
precision highp float;
|
|
646
|
-
|
|
647
|
-
in vec2 v_uv;
|
|
648
|
-
out vec4 outColor;
|
|
649
|
-
|
|
650
|
-
uniform vec2 u_resolution;
|
|
651
|
-
uniform float u_time;
|
|
652
|
-
uniform float u_progress;
|
|
653
|
-
uniform float u_seed;
|
|
654
|
-
uniform float u_profile;
|
|
655
|
-
uniform sampler2D u_motion;
|
|
656
|
-
uniform sampler2D u_effect;
|
|
657
|
-
uniform float u_hasEffect;
|
|
658
|
-
uniform float u_effectFrames;
|
|
659
|
-
uniform float u_motionDuration;
|
|
660
|
-
uniform float u_motionScale;
|
|
661
|
-
uniform vec3 u_dark;
|
|
662
|
-
uniform vec3 u_accentA;
|
|
663
|
-
uniform vec3 u_accentB;
|
|
664
|
-
uniform vec3 u_glow;
|
|
665
|
-
|
|
666
|
-
float hash21(vec2 p) {
|
|
667
|
-
p = fract(p * vec2(123.34, 456.21));
|
|
668
|
-
p += dot(p, p + 45.32 + u_seed * 11.7);
|
|
669
|
-
return fract(p.x * p.y);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
float noise(vec2 p) {
|
|
673
|
-
vec2 i = floor(p);
|
|
674
|
-
vec2 f = fract(p);
|
|
675
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
676
|
-
float a = hash21(i);
|
|
677
|
-
float b = hash21(i + vec2(1.0, 0.0));
|
|
678
|
-
float c = hash21(i + vec2(0.0, 1.0));
|
|
679
|
-
float d = hash21(i + vec2(1.0, 1.0));
|
|
680
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
float fbm(vec2 p) {
|
|
684
|
-
float value = 0.0;
|
|
685
|
-
float amplitude = 0.55;
|
|
686
|
-
mat2 rotation = mat2(0.82, 0.57, -0.57, 0.82);
|
|
687
|
-
for (int i = 0; i < 6; i++) {
|
|
688
|
-
value += noise(p) * amplitude;
|
|
689
|
-
p = rotation * p * 2.02 + 13.7;
|
|
690
|
-
amplitude *= 0.48;
|
|
691
|
-
}
|
|
692
|
-
return value;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
float gaussian(float value, float center, float width) {
|
|
696
|
-
float delta = (value - center) / max(width, 0.0001);
|
|
697
|
-
return exp(-delta * delta);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
float profileMix(float model, float agent, float visual) {
|
|
701
|
-
if (u_profile < 0.5) return model;
|
|
702
|
-
if (u_profile < 1.5) return agent;
|
|
703
|
-
return visual;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
float motionSample(float y, float t) {
|
|
707
|
-
float phase = fract(t / max(u_motionDuration, 0.001));
|
|
708
|
-
float captured = texture(u_motion, vec2(phase, 1.0 - clamp(y, 0.0, 1.0))).r;
|
|
709
|
-
return (captured * 2.0 - 1.0) * u_motionScale;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
float edgeDisplacement(float y, float t) {
|
|
713
|
-
return motionSample(y, t);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
float flowDisplacement(float y, float t) {
|
|
717
|
-
return (
|
|
718
|
-
motionSample(y - 0.024, t) * 0.08 +
|
|
719
|
-
motionSample(y - 0.012, t) * 0.18 +
|
|
720
|
-
motionSample(y, t) * 0.48 +
|
|
721
|
-
motionSample(y + 0.012, t) * 0.18 +
|
|
722
|
-
motionSample(y + 0.024, t) * 0.08
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
float ellipseRing(vec2 p, float radius, float width) {
|
|
727
|
-
return gaussian(length(p), radius, width);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
void main() {
|
|
731
|
-
vec2 uv = v_uv;
|
|
732
|
-
float t = u_time;
|
|
733
|
-
float edge = u_progress + edgeDisplacement(uv.y, t);
|
|
734
|
-
float flowEdge = u_progress + flowDisplacement(uv.y, t);
|
|
735
|
-
float d = uv.x - edge;
|
|
736
|
-
float fd = uv.x - flowEdge;
|
|
737
|
-
|
|
738
|
-
vec3 rightBase = vec3(0.125, 0.129, 0.145);
|
|
739
|
-
vec3 color = rightBase;
|
|
740
|
-
|
|
741
|
-
float leftMask = 1.0 - smoothstep(-0.001, 0.002, d);
|
|
742
|
-
color = mix(color, u_dark, leftMask * profileMix(0.96, 0.92, 0.96));
|
|
743
|
-
|
|
744
|
-
vec2 flowP = vec2((fd + 0.10) * 6.2, uv.y * 1.95);
|
|
745
|
-
float flowA = fbm(flowP + vec2(-t * 0.22, t * 0.27) + u_seed * 1.7);
|
|
746
|
-
float flowB = fbm(flowP * 1.52 + vec2(t * 0.28, -t * 0.36) + 8.2 + u_seed);
|
|
747
|
-
float flowC = fbm(flowP * 2.25 + vec2(-t * 0.41, t * 0.46) + 19.0);
|
|
748
|
-
|
|
749
|
-
float farCenter = profileMix(-0.060, -0.079, -0.045) + (flowA - 0.5) * profileMix(0.018, 0.022, 0.014);
|
|
750
|
-
float midCenter = profileMix(-0.039, -0.052, -0.030) + (flowB - 0.5) * profileMix(0.013, 0.016, 0.010);
|
|
751
|
-
float hotCenter = profileMix(-0.026, -0.029, -0.023) + (flowC - 0.5) * 0.010;
|
|
752
|
-
|
|
753
|
-
float farBand = gaussian(fd, farCenter, profileMix(0.035, 0.049, 0.030));
|
|
754
|
-
float midBand = gaussian(fd, midCenter, profileMix(0.026, 0.034, 0.026));
|
|
755
|
-
float hotBand = gaussian(fd, hotCenter, profileMix(0.023, 0.027, 0.026));
|
|
756
|
-
float darkTrough = gaussian(fd, profileMix(-0.050, -0.058, -0.044) + (flowB - 0.5) * 0.010, profileMix(0.020, 0.025, 0.021));
|
|
757
|
-
|
|
758
|
-
float ringY = 0.47 + sin(t * 0.58 + u_seed * 2.4) * 0.12;
|
|
759
|
-
vec2 ringP = vec2((fd + 0.086) / 0.078, (uv.y - ringY) / 0.25);
|
|
760
|
-
ringP += vec2((flowB - 0.5) * 0.08, (flowA - 0.5) * 0.06);
|
|
761
|
-
float ringTexture = fbm(ringP * 2.15 + vec2(t * 0.18, -t * 0.14) + u_seed * 1.9);
|
|
762
|
-
float ring = ellipseRing(ringP, 0.66, 0.32) * (0.30 + 0.64 * ringTexture);
|
|
763
|
-
float ringCore = gaussian(length(ringP), 0.25, 0.25);
|
|
764
|
-
float ringPulse = smoothstep(0.58, 0.90, 0.5 + 0.5 * sin(t * 0.82 + u_seed * 4.1));
|
|
765
|
-
float modelRing = ring * ringPulse * (1.0 - step(0.5, u_profile));
|
|
766
|
-
float visualRing = ring * 0.16 * step(1.5, u_profile) * ringPulse;
|
|
767
|
-
|
|
768
|
-
float cloudGate = leftMask * smoothstep(-0.30, -0.008, fd);
|
|
769
|
-
float textureA = smoothstep(0.24, 0.92, flowA * 0.72 + flowB * 0.42);
|
|
770
|
-
float textureB = smoothstep(0.28, 0.94, flowB * 0.68 + flowC * 0.38);
|
|
771
|
-
|
|
772
|
-
vec3 hotColor = u_accentB;
|
|
773
|
-
color += u_accentA * farBand * cloudGate * (0.07 + textureA * profileMix(0.42, 0.24, 0.40));
|
|
774
|
-
color += u_accentB * midBand * cloudGate * (0.15 + textureB * profileMix(0.70, 0.46, 0.68));
|
|
775
|
-
color += hotColor * hotBand * cloudGate * profileMix(0.88, 0.62, 0.84);
|
|
776
|
-
float modelMask = 1.0 - step(0.5, u_profile);
|
|
777
|
-
color += u_accentA * (modelRing + visualRing) * cloudGate * profileMix(0.54, 0.0, 0.34);
|
|
778
|
-
color *= 1.0 - darkTrough * profileMix(0.44, 0.24, 0.24) * cloudGate;
|
|
779
|
-
color *= 1.0 - ringCore * modelMask * ringPulse * 0.44 * cloudGate;
|
|
780
|
-
|
|
781
|
-
float broadHalo = exp(-abs(d) * 96.0);
|
|
782
|
-
float innerHalo = exp(-abs(d) * 176.0);
|
|
783
|
-
float colorCore = exp(-abs(d) * 360.0);
|
|
784
|
-
float sharpCore = exp(-abs(d) * 760.0);
|
|
785
|
-
float leftGate = 1.0 - smoothstep(-0.003, 0.005, d);
|
|
786
|
-
|
|
787
|
-
color += u_accentA * broadHalo * leftGate * profileMix(0.09, 0.04, 0.06);
|
|
788
|
-
color += hotColor * innerHalo * leftGate * profileMix(0.76, 0.72, 0.78);
|
|
789
|
-
color += u_glow * colorCore * profileMix(0.72, 0.34, 0.24);
|
|
790
|
-
|
|
791
|
-
float whiteStrength = profileMix(0.10, 0.0, 0.0);
|
|
792
|
-
color += vec3(1.0, 0.99, 0.91) * sharpCore * whiteStrength;
|
|
793
|
-
|
|
794
|
-
float rightCut = smoothstep(0.001, 0.006, d);
|
|
795
|
-
color = mix(color, rightBase, rightCut);
|
|
796
|
-
|
|
797
|
-
float effectX = (d * 1257.0 + 260.0) / 320.0;
|
|
798
|
-
float atlasPhase = fract(t / 12.0) * u_effectFrames;
|
|
799
|
-
float atlasFrameA = floor(atlasPhase);
|
|
800
|
-
float atlasFrameB = mod(atlasFrameA + 1.0, u_effectFrames);
|
|
801
|
-
float atlasMix = smoothstep(0.0, 1.0, fract(atlasPhase));
|
|
802
|
-
float atlasXA = (atlasFrameA + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
803
|
-
float atlasXB = (atlasFrameB + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
804
|
-
vec3 referenceA = texture(u_effect, vec2(atlasXA, uv.y)).rgb;
|
|
805
|
-
vec3 referenceB = texture(u_effect, vec2(atlasXB, uv.y)).rgb;
|
|
806
|
-
vec3 referenceColor = mix(referenceA, referenceB, atlasMix);
|
|
807
|
-
float stripMask = smoothstep(0.0, 0.018, effectX) * (1.0 - smoothstep(0.982, 1.0, effectX));
|
|
808
|
-
float referenceLeft = 1.0 - smoothstep(-0.026, -0.012, d);
|
|
809
|
-
// 参考图集只提供亮度结构,颜色始终由用户 colors(u_dark / u_accentA /
|
|
810
|
-
// u_accentB / u_glow)决定:这样 setColors / colors 属性在 WebGL 路径下
|
|
811
|
-
// 真实生效,改色有可见反馈,而不是被图集整体覆盖。
|
|
812
|
-
float referenceLuma = dot(referenceColor, vec3(0.299, 0.587, 0.114));
|
|
813
|
-
vec3 tintedColor = color * (0.42 + 0.86 * referenceLuma);
|
|
814
|
-
color = mix(color, tintedColor, stripMask * referenceLeft * u_hasEffect);
|
|
815
|
-
|
|
816
|
-
outColor = vec4(clamp(color, 0.0, 1.0), 1.0);
|
|
817
|
-
}`;
|
|
818
|
-
|
|
819
|
-
function compileShader(gl, type, source) {
|
|
820
|
-
const shader = gl.createShader(type);
|
|
821
|
-
gl.shaderSource(shader, source);
|
|
822
|
-
gl.compileShader(shader);
|
|
823
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
824
|
-
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
825
|
-
gl.deleteShader(shader);
|
|
826
|
-
throw new Error(message);
|
|
827
|
-
}
|
|
828
|
-
return shader;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function createProgram(gl) {
|
|
832
|
-
const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
833
|
-
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
834
|
-
const program = gl.createProgram();
|
|
835
|
-
gl.attachShader(program, vertex);
|
|
836
|
-
gl.attachShader(program, fragment);
|
|
837
|
-
gl.linkProgram(program);
|
|
838
|
-
gl.deleteShader(vertex);
|
|
839
|
-
gl.deleteShader(fragment);
|
|
840
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
841
|
-
const message = gl.getProgramInfoLog(program) || 'Unknown shader link error';
|
|
842
|
-
gl.deleteProgram(program);
|
|
843
|
-
throw new Error(message);
|
|
844
|
-
}
|
|
845
|
-
return program;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
class ProgressFlowRenderer {
|
|
849
|
-
constructor(canvas, preset, options = {}) {
|
|
850
|
-
const gl = canvas.getContext('webgl2', {
|
|
851
|
-
alpha: false,
|
|
852
|
-
antialias: true,
|
|
853
|
-
premultipliedAlpha: false,
|
|
854
|
-
powerPreference: options.powerPreference || 'high-performance'
|
|
855
|
-
});
|
|
856
|
-
if (!gl) throw new Error('WebGL2 unavailable');
|
|
857
|
-
|
|
858
|
-
this.canvas = canvas;
|
|
859
|
-
this.options = options;
|
|
860
|
-
this.gl = gl;
|
|
861
|
-
this.profile = preset.edgeStyle === 'tide' ? 3 : (PROFILE_INDEX[preset.id] ?? 2);
|
|
862
|
-
this.seed = stringSeed$1(`${preset.id}-shader`) * 13.7 + 1.0;
|
|
863
|
-
this.colors = preset.colors.map(hexToRgb01);
|
|
864
|
-
this.motionData = getProgressMotionData(preset.id, preset.edgeStyle);
|
|
865
|
-
const motionFactor = preset.edgeStyle === 'tide'
|
|
866
|
-
? MOTION_SCALE_FACTORS.tide
|
|
867
|
-
: (MOTION_SCALE_FACTORS[preset.id] || 1.04);
|
|
868
|
-
this.motionScale = (PROGRESS_MOTION_MAX_PX * motionFactor) / 1257;
|
|
869
|
-
this.disposed = false;
|
|
870
|
-
this.contextLost = false;
|
|
871
|
-
this.setupResources();
|
|
872
|
-
this.bindContextEvents();
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
setupResources() {
|
|
876
|
-
const gl = this.gl;
|
|
877
|
-
this.program = createProgram(gl);
|
|
878
|
-
|
|
879
|
-
this.position = gl.getAttribLocation(this.program, 'a_position');
|
|
880
|
-
this.uniforms = {
|
|
881
|
-
resolution: gl.getUniformLocation(this.program, 'u_resolution'),
|
|
882
|
-
time: gl.getUniformLocation(this.program, 'u_time'),
|
|
883
|
-
progress: gl.getUniformLocation(this.program, 'u_progress'),
|
|
884
|
-
seed: gl.getUniformLocation(this.program, 'u_seed'),
|
|
885
|
-
profile: gl.getUniformLocation(this.program, 'u_profile'),
|
|
886
|
-
motion: gl.getUniformLocation(this.program, 'u_motion'),
|
|
887
|
-
effect: gl.getUniformLocation(this.program, 'u_effect'),
|
|
888
|
-
hasEffect: gl.getUniformLocation(this.program, 'u_hasEffect'),
|
|
889
|
-
effectFrames: gl.getUniformLocation(this.program, 'u_effectFrames'),
|
|
890
|
-
motionDuration: gl.getUniformLocation(this.program, 'u_motionDuration'),
|
|
891
|
-
motionScale: gl.getUniformLocation(this.program, 'u_motionScale'),
|
|
892
|
-
dark: gl.getUniformLocation(this.program, 'u_dark'),
|
|
893
|
-
accentA: gl.getUniformLocation(this.program, 'u_accentA'),
|
|
894
|
-
accentB: gl.getUniformLocation(this.program, 'u_accentB'),
|
|
895
|
-
glow: gl.getUniformLocation(this.program, 'u_glow')
|
|
896
|
-
};
|
|
897
|
-
|
|
898
|
-
this.buffer = gl.createBuffer();
|
|
899
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
900
|
-
gl.bufferData(
|
|
901
|
-
gl.ARRAY_BUFFER,
|
|
902
|
-
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
|
|
903
|
-
gl.STATIC_DRAW
|
|
904
|
-
);
|
|
905
|
-
|
|
906
|
-
this.motionTexture = gl.createTexture();
|
|
907
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
908
|
-
gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
|
|
909
|
-
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
910
|
-
gl.texImage2D(
|
|
911
|
-
gl.TEXTURE_2D,
|
|
912
|
-
0,
|
|
913
|
-
gl.R8,
|
|
914
|
-
PROGRESS_MOTION_WIDTH,
|
|
915
|
-
PROGRESS_MOTION_HEIGHT,
|
|
916
|
-
0,
|
|
917
|
-
gl.RED,
|
|
918
|
-
gl.UNSIGNED_BYTE,
|
|
919
|
-
this.motionData
|
|
920
|
-
);
|
|
921
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
922
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
923
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
924
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
925
|
-
|
|
926
|
-
this.effectTexture = gl.createTexture();
|
|
927
|
-
gl.activeTexture(gl.TEXTURE1);
|
|
928
|
-
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
929
|
-
gl.texImage2D(
|
|
930
|
-
gl.TEXTURE_2D,
|
|
931
|
-
0,
|
|
932
|
-
gl.RGB,
|
|
933
|
-
1,
|
|
934
|
-
1,
|
|
935
|
-
0,
|
|
936
|
-
gl.RGB,
|
|
937
|
-
gl.UNSIGNED_BYTE,
|
|
938
|
-
new Uint8Array([32, 33, 38])
|
|
939
|
-
);
|
|
940
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
941
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
942
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
943
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
944
|
-
this.effectUploaded = false;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
bindContextEvents() {
|
|
948
|
-
this.onContextLost = (event) => {
|
|
949
|
-
event.preventDefault();
|
|
950
|
-
if (this.disposed) return;
|
|
951
|
-
this.contextLost = true;
|
|
952
|
-
if (typeof this.options.onContextLost === 'function') this.options.onContextLost(event);
|
|
953
|
-
};
|
|
954
|
-
this.onContextRestored = () => {
|
|
955
|
-
if (this.disposed) return;
|
|
956
|
-
this.setupResources();
|
|
957
|
-
this.contextLost = false;
|
|
958
|
-
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
959
|
-
if (typeof this.options.onContextRestored === 'function') this.options.onContextRestored();
|
|
960
|
-
};
|
|
961
|
-
this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
|
|
962
|
-
this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
resize(width, height, dpr) {
|
|
966
|
-
const pixelWidth = Math.max(1, Math.round(width * dpr));
|
|
967
|
-
const pixelHeight = Math.max(1, Math.round(height * dpr));
|
|
968
|
-
if (this.canvas.width !== pixelWidth || this.canvas.height !== pixelHeight) {
|
|
969
|
-
this.canvas.width = pixelWidth;
|
|
970
|
-
this.canvas.height = pixelHeight;
|
|
971
|
-
}
|
|
972
|
-
this.gl.viewport(0, 0, pixelWidth, pixelHeight);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
draw(time, progress, effectImage = null) {
|
|
976
|
-
if (this.disposed || this.contextLost) return;
|
|
977
|
-
const gl = this.gl;
|
|
978
|
-
gl.useProgram(this.program);
|
|
979
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
980
|
-
gl.enableVertexAttribArray(this.position);
|
|
981
|
-
gl.vertexAttribPointer(this.position, 2, gl.FLOAT, false, 0, 0);
|
|
982
|
-
|
|
983
|
-
gl.uniform2f(this.uniforms.resolution, this.canvas.width, this.canvas.height);
|
|
984
|
-
gl.uniform1f(this.uniforms.time, time);
|
|
985
|
-
gl.uniform1f(this.uniforms.progress, progress / 100);
|
|
986
|
-
gl.uniform1f(this.uniforms.seed, this.seed);
|
|
987
|
-
gl.uniform1f(this.uniforms.profile, this.profile);
|
|
988
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
989
|
-
gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
|
|
990
|
-
gl.uniform1i(this.uniforms.motion, 0);
|
|
991
|
-
gl.uniform1f(this.uniforms.motionDuration, PROGRESS_MOTION_DURATION);
|
|
992
|
-
gl.uniform1f(this.uniforms.motionScale, this.motionScale);
|
|
993
|
-
|
|
994
|
-
let hasEffect = this.effectUploaded ? 1 : 0;
|
|
995
|
-
const effectWidth = effectImage && (effectImage.naturalWidth || effectImage.width || 0);
|
|
996
|
-
if (!this.effectUploaded && effectImage && effectWidth > 0) {
|
|
997
|
-
gl.activeTexture(gl.TEXTURE1);
|
|
998
|
-
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
999
|
-
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
1000
|
-
try {
|
|
1001
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, effectImage);
|
|
1002
|
-
this.effectUploaded = true;
|
|
1003
|
-
hasEffect = 1;
|
|
1004
|
-
} catch (error) {
|
|
1005
|
-
console.warn('[画境观屿] 参考纹理图集上传失败,继续使用程序化降级。', error);
|
|
1006
|
-
}
|
|
1007
|
-
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
1008
|
-
}
|
|
1009
|
-
gl.activeTexture(gl.TEXTURE1);
|
|
1010
|
-
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
1011
|
-
gl.uniform1i(this.uniforms.effect, 1);
|
|
1012
|
-
gl.uniform1f(this.uniforms.hasEffect, hasEffect);
|
|
1013
|
-
gl.uniform1f(this.uniforms.effectFrames, 24);
|
|
1014
|
-
|
|
1015
|
-
gl.uniform3fv(this.uniforms.dark, this.colors[0]);
|
|
1016
|
-
gl.uniform3fv(this.uniforms.accentA, this.colors[1]);
|
|
1017
|
-
gl.uniform3fv(this.uniforms.accentB, this.colors[2]);
|
|
1018
|
-
gl.uniform3fv(this.uniforms.glow, this.colors[3]);
|
|
1019
|
-
|
|
1020
|
-
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
setColors(colors) {
|
|
1024
|
-
this.colors = colors.map(hexToRgb01);
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
dispose() {
|
|
1028
|
-
this.disposed = true;
|
|
1029
|
-
const gl = this.gl;
|
|
1030
|
-
this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
|
|
1031
|
-
this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
1032
|
-
gl.deleteBuffer(this.buffer);
|
|
1033
|
-
gl.deleteTexture(this.motionTexture);
|
|
1034
|
-
gl.deleteTexture(this.effectTexture);
|
|
1035
|
-
gl.deleteProgram(this.program);
|
|
1036
|
-
const lose = gl.getExtension('WEBGL_lose_context');
|
|
1037
|
-
if (lose) lose.loseContext();
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function createProgressFlowRenderer(canvas, preset, options = {}) {
|
|
1042
|
-
try {
|
|
1043
|
-
return new ProgressFlowRenderer(canvas, preset, options);
|
|
1044
|
-
} catch (error) {
|
|
1045
|
-
console.warn('[画境观屿] 进度流体 WebGL2 不可用,使用 Canvas 2D 降级。', error);
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
703
|
+
function getProgressMotionData(id, edgeStyle = 'flow') {
|
|
704
|
+
const key = `${id}:${edgeStyle}`;
|
|
705
|
+
if (!CACHE$1[key]) CACHE$1[key] = createMotionData(id, edgeStyle);
|
|
706
|
+
return CACHE$1[key];
|
|
1048
707
|
}
|
|
1049
708
|
|
|
1050
|
-
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const PALETTES = {
|
|
1058
|
-
'model-training': ['#20131f', '#ff3f94', '#ff8a3d', '#fff06a'],
|
|
1059
|
-
'agent-migration': ['#111a31', '#245bff', '#00cfff', '#5dffe6'],
|
|
1060
|
-
'visual-training': ['#1f172b', '#7042ff', '#42f58d', '#c4ff8a'],
|
|
1061
|
-
'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff'],
|
|
1062
|
-
'botanic-lab': ['#102a23', '#287a58', '#9bd65b', '#f2ffc4']
|
|
1063
|
-
};
|
|
1064
|
-
|
|
1065
|
-
function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
|
|
1066
|
-
context.save();
|
|
1067
|
-
context.translate(x, y);
|
|
1068
|
-
context.scale(1, radiusY / radiusX);
|
|
1069
|
-
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
1070
|
-
const alphaHex = (value) => {
|
|
1071
|
-
const text = Math.round(value).toString(16);
|
|
1072
|
-
return text.length === 1 ? `0${text}` : text;
|
|
1073
|
-
};
|
|
1074
|
-
gradient.addColorStop(0, `${color}${alphaHex(alpha * 255)}`);
|
|
1075
|
-
gradient.addColorStop(0.46, `${color}${alphaHex(alpha * 0.42 * 255)}`);
|
|
1076
|
-
gradient.addColorStop(1, `${color}00`);
|
|
1077
|
-
context.fillStyle = gradient;
|
|
1078
|
-
context.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
1079
|
-
context.restore();
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function createAtlas(id) {
|
|
1083
|
-
const palette = PALETTES[id] || PALETTES['visual-training'];
|
|
1084
|
-
const canvas = document.createElement('canvas');
|
|
1085
|
-
canvas.width = FRAME_WIDTH * PROGRESS_REFERENCE_FRAME_COUNT;
|
|
1086
|
-
canvas.height = FRAME_HEIGHT;
|
|
1087
|
-
const context = canvas.getContext('2d');
|
|
1088
|
-
|
|
1089
|
-
for (let frame = 0; frame < PROGRESS_REFERENCE_FRAME_COUNT; frame += 1) {
|
|
1090
|
-
const phase = (frame / PROGRESS_REFERENCE_FRAME_COUNT) * Math.PI * 2;
|
|
1091
|
-
const left = frame * FRAME_WIDTH;
|
|
1092
|
-
context.save();
|
|
1093
|
-
context.translate(left, 0);
|
|
1094
|
-
context.fillStyle = palette[0];
|
|
1095
|
-
context.fillRect(0, 0, FRAME_WIDTH, FRAME_HEIGHT);
|
|
1096
|
-
context.globalCompositeOperation = 'screen';
|
|
1097
|
-
|
|
1098
|
-
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);
|
|
1099
|
-
drawCloud(context, 45 + Math.cos(phase * 0.72) * 4, 23 + Math.sin(phase * 0.54) * 4, 22, 15, palette[2], 0.44);
|
|
1100
|
-
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);
|
|
1101
|
-
|
|
1102
|
-
context.globalCompositeOperation = 'source-over';
|
|
1103
|
-
const trough = context.createRadialGradient(41, 16, 1, 41, 16, 17);
|
|
1104
|
-
trough.addColorStop(0, 'rgba(5,6,11,0.64)');
|
|
1105
|
-
trough.addColorStop(0.58, 'rgba(6,7,12,0.26)');
|
|
1106
|
-
trough.addColorStop(1, 'rgba(6,7,12,0)');
|
|
1107
|
-
context.fillStyle = trough;
|
|
1108
|
-
context.fillRect(20, 0, 44, FRAME_HEIGHT);
|
|
1109
|
-
context.restore();
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
// A canvas is a valid TexImageSource. Keeping it directly avoids a PNG
|
|
1113
|
-
// encode -> base64 allocation -> Image decode round trip at startup.
|
|
1114
|
-
return { image: canvas, ready: true };
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function getProgressReferenceAtlas(id) {
|
|
1118
|
-
if (!CACHE[id]) CACHE[id] = createAtlas(id);
|
|
1119
|
-
return CACHE[id];
|
|
709
|
+
function hexToRgb01(hex) {
|
|
710
|
+
const value = Number.parseInt(hex.replace('#', ''), 16);
|
|
711
|
+
return [
|
|
712
|
+
((value >> 16) & 255) / 255,
|
|
713
|
+
((value >> 8) & 255) / 255,
|
|
714
|
+
(value & 255) / 255
|
|
715
|
+
];
|
|
1120
716
|
}
|
|
1121
717
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
718
|
+
function stringSeed$1(value) {
|
|
719
|
+
let hash = 2166136261;
|
|
720
|
+
for (const character of value) {
|
|
721
|
+
hash ^= character.charCodeAt(0);
|
|
722
|
+
hash = Math.imul(hash, 16777619);
|
|
723
|
+
}
|
|
724
|
+
return (hash >>> 0) / 4294967295;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const PROFILE_INDEX = {
|
|
728
|
+
'model-training': 0,
|
|
729
|
+
'agent-migration': 1,
|
|
730
|
+
'visual-training': 2,
|
|
731
|
+
'tide': 3
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const MOTION_SCALE_FACTORS = {
|
|
735
|
+
'model-training': 1.05,
|
|
736
|
+
'agent-migration': 1.04,
|
|
737
|
+
'visual-training': 1.04,
|
|
738
|
+
'tide': 1.18
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const VERTEX_SHADER = `#version 300 es
|
|
742
|
+
in vec2 a_position;
|
|
743
|
+
out vec2 v_uv;
|
|
744
|
+
void main() {
|
|
745
|
+
v_uv = a_position * 0.5 + 0.5;
|
|
746
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
747
|
+
}`;
|
|
748
|
+
|
|
749
|
+
const FRAGMENT_SHADER = `#version 300 es
|
|
750
|
+
precision highp float;
|
|
751
|
+
|
|
752
|
+
in vec2 v_uv;
|
|
753
|
+
out vec4 outColor;
|
|
754
|
+
|
|
755
|
+
uniform vec2 u_resolution;
|
|
756
|
+
uniform float u_time;
|
|
757
|
+
uniform float u_progress;
|
|
758
|
+
uniform float u_seed;
|
|
759
|
+
uniform float u_profile;
|
|
760
|
+
uniform sampler2D u_motion;
|
|
761
|
+
uniform sampler2D u_effect;
|
|
762
|
+
uniform float u_hasEffect;
|
|
763
|
+
uniform float u_effectFrames;
|
|
764
|
+
uniform float u_motionDuration;
|
|
765
|
+
uniform float u_motionScale;
|
|
766
|
+
uniform vec3 u_dark;
|
|
767
|
+
uniform vec3 u_accentA;
|
|
768
|
+
uniform vec3 u_accentB;
|
|
769
|
+
uniform vec3 u_glow;
|
|
770
|
+
|
|
771
|
+
float hash21(vec2 p) {
|
|
772
|
+
p = fract(p * vec2(123.34, 456.21));
|
|
773
|
+
p += dot(p, p + 45.32 + u_seed * 11.7);
|
|
774
|
+
return fract(p.x * p.y);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
float noise(vec2 p) {
|
|
778
|
+
vec2 i = floor(p);
|
|
779
|
+
vec2 f = fract(p);
|
|
780
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
781
|
+
float a = hash21(i);
|
|
782
|
+
float b = hash21(i + vec2(1.0, 0.0));
|
|
783
|
+
float c = hash21(i + vec2(0.0, 1.0));
|
|
784
|
+
float d = hash21(i + vec2(1.0, 1.0));
|
|
785
|
+
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
float fbm(vec2 p) {
|
|
789
|
+
float value = 0.0;
|
|
790
|
+
float amplitude = 0.55;
|
|
791
|
+
mat2 rotation = mat2(0.82, 0.57, -0.57, 0.82);
|
|
792
|
+
for (int i = 0; i < 6; i++) {
|
|
793
|
+
value += noise(p) * amplitude;
|
|
794
|
+
p = rotation * p * 2.02 + 13.7;
|
|
795
|
+
amplitude *= 0.48;
|
|
796
|
+
}
|
|
797
|
+
return value;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
float gaussian(float value, float center, float width) {
|
|
801
|
+
float delta = (value - center) / max(width, 0.0001);
|
|
802
|
+
return exp(-delta * delta);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
float profileMix(float model, float agent, float visual) {
|
|
806
|
+
if (u_profile < 0.5) return model;
|
|
807
|
+
if (u_profile < 1.5) return agent;
|
|
808
|
+
return visual;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
float motionSample(float y, float t) {
|
|
812
|
+
float phase = fract(t / max(u_motionDuration, 0.001));
|
|
813
|
+
float captured = texture(u_motion, vec2(phase, 1.0 - clamp(y, 0.0, 1.0))).r;
|
|
814
|
+
return (captured * 2.0 - 1.0) * u_motionScale;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
float edgeDisplacement(float y, float t) {
|
|
818
|
+
return motionSample(y, t);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
float flowDisplacement(float y, float t) {
|
|
822
|
+
return (
|
|
823
|
+
motionSample(y - 0.024, t) * 0.08 +
|
|
824
|
+
motionSample(y - 0.012, t) * 0.18 +
|
|
825
|
+
motionSample(y, t) * 0.48 +
|
|
826
|
+
motionSample(y + 0.012, t) * 0.18 +
|
|
827
|
+
motionSample(y + 0.024, t) * 0.08
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
float ellipseRing(vec2 p, float radius, float width) {
|
|
832
|
+
return gaussian(length(p), radius, width);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
void main() {
|
|
836
|
+
vec2 uv = v_uv;
|
|
837
|
+
float t = u_time;
|
|
838
|
+
float edge = u_progress + edgeDisplacement(uv.y, t);
|
|
839
|
+
float flowEdge = u_progress + flowDisplacement(uv.y, t);
|
|
840
|
+
float d = uv.x - edge;
|
|
841
|
+
float fd = uv.x - flowEdge;
|
|
842
|
+
|
|
843
|
+
vec3 rightBase = vec3(0.125, 0.129, 0.145);
|
|
844
|
+
vec3 color = rightBase;
|
|
845
|
+
|
|
846
|
+
float leftMask = 1.0 - smoothstep(-0.001, 0.002, d);
|
|
847
|
+
color = mix(color, u_dark, leftMask * profileMix(0.96, 0.92, 0.96));
|
|
848
|
+
|
|
849
|
+
vec2 flowP = vec2((fd + 0.10) * 6.2, uv.y * 1.95);
|
|
850
|
+
float flowA = fbm(flowP + vec2(-t * 0.22, t * 0.27) + u_seed * 1.7);
|
|
851
|
+
float flowB = fbm(flowP * 1.52 + vec2(t * 0.28, -t * 0.36) + 8.2 + u_seed);
|
|
852
|
+
float flowC = fbm(flowP * 2.25 + vec2(-t * 0.41, t * 0.46) + 19.0);
|
|
853
|
+
|
|
854
|
+
float farCenter = profileMix(-0.060, -0.079, -0.045) + (flowA - 0.5) * profileMix(0.018, 0.022, 0.014);
|
|
855
|
+
float midCenter = profileMix(-0.039, -0.052, -0.030) + (flowB - 0.5) * profileMix(0.013, 0.016, 0.010);
|
|
856
|
+
float hotCenter = profileMix(-0.026, -0.029, -0.023) + (flowC - 0.5) * 0.010;
|
|
857
|
+
|
|
858
|
+
float farBand = gaussian(fd, farCenter, profileMix(0.035, 0.049, 0.030));
|
|
859
|
+
float midBand = gaussian(fd, midCenter, profileMix(0.026, 0.034, 0.026));
|
|
860
|
+
float hotBand = gaussian(fd, hotCenter, profileMix(0.023, 0.027, 0.026));
|
|
861
|
+
float darkTrough = gaussian(fd, profileMix(-0.050, -0.058, -0.044) + (flowB - 0.5) * 0.010, profileMix(0.020, 0.025, 0.021));
|
|
862
|
+
|
|
863
|
+
float ringY = 0.47 + sin(t * 0.58 + u_seed * 2.4) * 0.12;
|
|
864
|
+
vec2 ringP = vec2((fd + 0.086) / 0.078, (uv.y - ringY) / 0.25);
|
|
865
|
+
ringP += vec2((flowB - 0.5) * 0.08, (flowA - 0.5) * 0.06);
|
|
866
|
+
float ringTexture = fbm(ringP * 2.15 + vec2(t * 0.18, -t * 0.14) + u_seed * 1.9);
|
|
867
|
+
float ring = ellipseRing(ringP, 0.66, 0.32) * (0.30 + 0.64 * ringTexture);
|
|
868
|
+
float ringCore = gaussian(length(ringP), 0.25, 0.25);
|
|
869
|
+
float ringPulse = smoothstep(0.58, 0.90, 0.5 + 0.5 * sin(t * 0.82 + u_seed * 4.1));
|
|
870
|
+
float modelRing = ring * ringPulse * (1.0 - step(0.5, u_profile));
|
|
871
|
+
float visualRing = ring * 0.16 * step(1.5, u_profile) * ringPulse;
|
|
872
|
+
|
|
873
|
+
float cloudGate = leftMask * smoothstep(-0.30, -0.008, fd);
|
|
874
|
+
float textureA = smoothstep(0.24, 0.92, flowA * 0.72 + flowB * 0.42);
|
|
875
|
+
float textureB = smoothstep(0.28, 0.94, flowB * 0.68 + flowC * 0.38);
|
|
876
|
+
|
|
877
|
+
vec3 hotColor = u_accentB;
|
|
878
|
+
color += u_accentA * farBand * cloudGate * (0.07 + textureA * profileMix(0.42, 0.24, 0.40));
|
|
879
|
+
color += u_accentB * midBand * cloudGate * (0.15 + textureB * profileMix(0.70, 0.46, 0.68));
|
|
880
|
+
color += hotColor * hotBand * cloudGate * profileMix(0.88, 0.62, 0.84);
|
|
881
|
+
float modelMask = 1.0 - step(0.5, u_profile);
|
|
882
|
+
color += u_accentA * (modelRing + visualRing) * cloudGate * profileMix(0.54, 0.0, 0.34);
|
|
883
|
+
color *= 1.0 - darkTrough * profileMix(0.44, 0.24, 0.24) * cloudGate;
|
|
884
|
+
color *= 1.0 - ringCore * modelMask * ringPulse * 0.44 * cloudGate;
|
|
885
|
+
|
|
886
|
+
float broadHalo = exp(-abs(d) * 96.0);
|
|
887
|
+
float innerHalo = exp(-abs(d) * 176.0);
|
|
888
|
+
float colorCore = exp(-abs(d) * 360.0);
|
|
889
|
+
float sharpCore = exp(-abs(d) * 760.0);
|
|
890
|
+
float leftGate = 1.0 - smoothstep(-0.003, 0.005, d);
|
|
891
|
+
|
|
892
|
+
color += u_accentA * broadHalo * leftGate * profileMix(0.09, 0.04, 0.06);
|
|
893
|
+
color += hotColor * innerHalo * leftGate * profileMix(0.76, 0.72, 0.78);
|
|
894
|
+
color += u_glow * colorCore * profileMix(0.72, 0.34, 0.24);
|
|
895
|
+
|
|
896
|
+
float whiteStrength = profileMix(0.10, 0.0, 0.0);
|
|
897
|
+
color += vec3(1.0, 0.99, 0.91) * sharpCore * whiteStrength;
|
|
898
|
+
|
|
899
|
+
float rightCut = smoothstep(0.001, 0.006, d);
|
|
900
|
+
color = mix(color, rightBase, rightCut);
|
|
901
|
+
|
|
902
|
+
float effectX = (d * 1257.0 + 260.0) / 320.0;
|
|
903
|
+
float atlasPhase = fract(t / 12.0) * u_effectFrames;
|
|
904
|
+
float atlasFrameA = floor(atlasPhase);
|
|
905
|
+
float atlasFrameB = mod(atlasFrameA + 1.0, u_effectFrames);
|
|
906
|
+
float atlasMix = smoothstep(0.0, 1.0, fract(atlasPhase));
|
|
907
|
+
float atlasXA = (atlasFrameA + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
908
|
+
float atlasXB = (atlasFrameB + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
|
|
909
|
+
vec3 referenceA = texture(u_effect, vec2(atlasXA, uv.y)).rgb;
|
|
910
|
+
vec3 referenceB = texture(u_effect, vec2(atlasXB, uv.y)).rgb;
|
|
911
|
+
vec3 referenceColor = mix(referenceA, referenceB, atlasMix);
|
|
912
|
+
float stripMask = smoothstep(0.0, 0.018, effectX) * (1.0 - smoothstep(0.982, 1.0, effectX));
|
|
913
|
+
float referenceLeft = 1.0 - smoothstep(-0.026, -0.012, d);
|
|
914
|
+
// 参考图集只提供亮度结构,颜色始终由用户 colors(u_dark / u_accentA /
|
|
915
|
+
// u_accentB / u_glow)决定:这样 setColors / colors 属性在 WebGL 路径下
|
|
916
|
+
// 真实生效,改色有可见反馈,而不是被图集整体覆盖。
|
|
917
|
+
float referenceLuma = dot(referenceColor, vec3(0.299, 0.587, 0.114));
|
|
918
|
+
vec3 tintedColor = color * (0.42 + 0.86 * referenceLuma);
|
|
919
|
+
color = mix(color, tintedColor, stripMask * referenceLeft * u_hasEffect);
|
|
920
|
+
|
|
921
|
+
outColor = vec4(clamp(color, 0.0, 1.0), 1.0);
|
|
922
|
+
}`;
|
|
923
|
+
|
|
924
|
+
function compileShader(gl, type, source) {
|
|
925
|
+
const shader = gl.createShader(type);
|
|
926
|
+
gl.shaderSource(shader, source);
|
|
927
|
+
gl.compileShader(shader);
|
|
928
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
929
|
+
const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
|
|
930
|
+
gl.deleteShader(shader);
|
|
931
|
+
throw new Error(message);
|
|
932
|
+
}
|
|
933
|
+
return shader;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function createProgram(gl) {
|
|
937
|
+
const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
938
|
+
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
939
|
+
const program = gl.createProgram();
|
|
940
|
+
gl.attachShader(program, vertex);
|
|
941
|
+
gl.attachShader(program, fragment);
|
|
942
|
+
gl.linkProgram(program);
|
|
943
|
+
gl.deleteShader(vertex);
|
|
944
|
+
gl.deleteShader(fragment);
|
|
945
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
946
|
+
const message = gl.getProgramInfoLog(program) || 'Unknown shader link error';
|
|
947
|
+
gl.deleteProgram(program);
|
|
948
|
+
throw new Error(message);
|
|
949
|
+
}
|
|
950
|
+
return program;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
class ProgressFlowRenderer {
|
|
954
|
+
constructor(canvas, preset, options = {}) {
|
|
955
|
+
const gl = canvas.getContext('webgl2', {
|
|
956
|
+
alpha: false,
|
|
957
|
+
antialias: true,
|
|
958
|
+
premultipliedAlpha: false,
|
|
959
|
+
powerPreference: options.powerPreference || 'high-performance'
|
|
960
|
+
});
|
|
961
|
+
if (!gl) throw new Error('WebGL2 unavailable');
|
|
962
|
+
|
|
963
|
+
this.canvas = canvas;
|
|
964
|
+
this.options = options;
|
|
965
|
+
this.gl = gl;
|
|
966
|
+
this.profile = preset.edgeStyle === 'tide' ? 3 : (PROFILE_INDEX[preset.id] ?? 2);
|
|
967
|
+
this.seed = stringSeed$1(`${preset.id}-shader`) * 13.7 + 1.0;
|
|
968
|
+
this.colors = preset.colors.map(hexToRgb01);
|
|
969
|
+
this.motionData = getProgressMotionData(preset.id, preset.edgeStyle);
|
|
970
|
+
const motionFactor = preset.edgeStyle === 'tide'
|
|
971
|
+
? MOTION_SCALE_FACTORS.tide
|
|
972
|
+
: (MOTION_SCALE_FACTORS[preset.id] || 1.04);
|
|
973
|
+
this.motionScale = (PROGRESS_MOTION_MAX_PX * motionFactor) / 1257;
|
|
974
|
+
this.disposed = false;
|
|
975
|
+
this.contextLost = false;
|
|
976
|
+
this.setupResources();
|
|
977
|
+
this.bindContextEvents();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
setupResources() {
|
|
981
|
+
const gl = this.gl;
|
|
982
|
+
this.program = createProgram(gl);
|
|
983
|
+
|
|
984
|
+
this.position = gl.getAttribLocation(this.program, 'a_position');
|
|
985
|
+
this.uniforms = {
|
|
986
|
+
resolution: gl.getUniformLocation(this.program, 'u_resolution'),
|
|
987
|
+
time: gl.getUniformLocation(this.program, 'u_time'),
|
|
988
|
+
progress: gl.getUniformLocation(this.program, 'u_progress'),
|
|
989
|
+
seed: gl.getUniformLocation(this.program, 'u_seed'),
|
|
990
|
+
profile: gl.getUniformLocation(this.program, 'u_profile'),
|
|
991
|
+
motion: gl.getUniformLocation(this.program, 'u_motion'),
|
|
992
|
+
effect: gl.getUniformLocation(this.program, 'u_effect'),
|
|
993
|
+
hasEffect: gl.getUniformLocation(this.program, 'u_hasEffect'),
|
|
994
|
+
effectFrames: gl.getUniformLocation(this.program, 'u_effectFrames'),
|
|
995
|
+
motionDuration: gl.getUniformLocation(this.program, 'u_motionDuration'),
|
|
996
|
+
motionScale: gl.getUniformLocation(this.program, 'u_motionScale'),
|
|
997
|
+
dark: gl.getUniformLocation(this.program, 'u_dark'),
|
|
998
|
+
accentA: gl.getUniformLocation(this.program, 'u_accentA'),
|
|
999
|
+
accentB: gl.getUniformLocation(this.program, 'u_accentB'),
|
|
1000
|
+
glow: gl.getUniformLocation(this.program, 'u_glow')
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
this.buffer = gl.createBuffer();
|
|
1004
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
1005
|
+
gl.bufferData(
|
|
1006
|
+
gl.ARRAY_BUFFER,
|
|
1007
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
|
|
1008
|
+
gl.STATIC_DRAW
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
this.motionTexture = gl.createTexture();
|
|
1012
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1013
|
+
gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
|
|
1014
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
1015
|
+
gl.texImage2D(
|
|
1016
|
+
gl.TEXTURE_2D,
|
|
1017
|
+
0,
|
|
1018
|
+
gl.R8,
|
|
1019
|
+
PROGRESS_MOTION_WIDTH,
|
|
1020
|
+
PROGRESS_MOTION_HEIGHT,
|
|
1021
|
+
0,
|
|
1022
|
+
gl.RED,
|
|
1023
|
+
gl.UNSIGNED_BYTE,
|
|
1024
|
+
this.motionData
|
|
1025
|
+
);
|
|
1026
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1027
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1028
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
1029
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1030
|
+
|
|
1031
|
+
this.effectTexture = gl.createTexture();
|
|
1032
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1033
|
+
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
1034
|
+
gl.texImage2D(
|
|
1035
|
+
gl.TEXTURE_2D,
|
|
1036
|
+
0,
|
|
1037
|
+
gl.RGB,
|
|
1038
|
+
1,
|
|
1039
|
+
1,
|
|
1040
|
+
0,
|
|
1041
|
+
gl.RGB,
|
|
1042
|
+
gl.UNSIGNED_BYTE,
|
|
1043
|
+
new Uint8Array([32, 33, 38])
|
|
1044
|
+
);
|
|
1045
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1046
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1047
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1048
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1049
|
+
this.effectUploaded = false;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
bindContextEvents() {
|
|
1053
|
+
this.onContextLost = (event) => {
|
|
1054
|
+
event.preventDefault();
|
|
1055
|
+
if (this.disposed) return;
|
|
1056
|
+
this.contextLost = true;
|
|
1057
|
+
if (typeof this.options.onContextLost === 'function') this.options.onContextLost(event);
|
|
1058
|
+
};
|
|
1059
|
+
this.onContextRestored = () => {
|
|
1060
|
+
if (this.disposed) return;
|
|
1061
|
+
this.setupResources();
|
|
1062
|
+
this.contextLost = false;
|
|
1063
|
+
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
1064
|
+
if (typeof this.options.onContextRestored === 'function') this.options.onContextRestored();
|
|
1065
|
+
};
|
|
1066
|
+
this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
|
|
1067
|
+
this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
resize(width, height, dpr) {
|
|
1071
|
+
const pixelWidth = Math.max(1, Math.round(width * dpr));
|
|
1072
|
+
const pixelHeight = Math.max(1, Math.round(height * dpr));
|
|
1073
|
+
if (this.canvas.width !== pixelWidth || this.canvas.height !== pixelHeight) {
|
|
1074
|
+
this.canvas.width = pixelWidth;
|
|
1075
|
+
this.canvas.height = pixelHeight;
|
|
1076
|
+
}
|
|
1077
|
+
this.gl.viewport(0, 0, pixelWidth, pixelHeight);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
draw(time, progress, effectImage = null) {
|
|
1081
|
+
if (this.disposed || this.contextLost) return;
|
|
1082
|
+
const gl = this.gl;
|
|
1083
|
+
gl.useProgram(this.program);
|
|
1084
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
1085
|
+
gl.enableVertexAttribArray(this.position);
|
|
1086
|
+
gl.vertexAttribPointer(this.position, 2, gl.FLOAT, false, 0, 0);
|
|
1087
|
+
|
|
1088
|
+
gl.uniform2f(this.uniforms.resolution, this.canvas.width, this.canvas.height);
|
|
1089
|
+
gl.uniform1f(this.uniforms.time, time);
|
|
1090
|
+
gl.uniform1f(this.uniforms.progress, progress / 100);
|
|
1091
|
+
gl.uniform1f(this.uniforms.seed, this.seed);
|
|
1092
|
+
gl.uniform1f(this.uniforms.profile, this.profile);
|
|
1093
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1094
|
+
gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
|
|
1095
|
+
gl.uniform1i(this.uniforms.motion, 0);
|
|
1096
|
+
gl.uniform1f(this.uniforms.motionDuration, PROGRESS_MOTION_DURATION);
|
|
1097
|
+
gl.uniform1f(this.uniforms.motionScale, this.motionScale);
|
|
1098
|
+
|
|
1099
|
+
let hasEffect = this.effectUploaded ? 1 : 0;
|
|
1100
|
+
const effectWidth = effectImage && (effectImage.naturalWidth || effectImage.width || 0);
|
|
1101
|
+
if (!this.effectUploaded && effectImage && effectWidth > 0) {
|
|
1102
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1103
|
+
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
1104
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
1105
|
+
try {
|
|
1106
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, effectImage);
|
|
1107
|
+
this.effectUploaded = true;
|
|
1108
|
+
hasEffect = 1;
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
console.warn('[画境观屿] 参考纹理图集上传失败,继续使用程序化降级。', error);
|
|
1111
|
+
}
|
|
1112
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
1113
|
+
}
|
|
1114
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1115
|
+
gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
|
|
1116
|
+
gl.uniform1i(this.uniforms.effect, 1);
|
|
1117
|
+
gl.uniform1f(this.uniforms.hasEffect, hasEffect);
|
|
1118
|
+
gl.uniform1f(this.uniforms.effectFrames, 24);
|
|
1119
|
+
|
|
1120
|
+
gl.uniform3fv(this.uniforms.dark, this.colors[0]);
|
|
1121
|
+
gl.uniform3fv(this.uniforms.accentA, this.colors[1]);
|
|
1122
|
+
gl.uniform3fv(this.uniforms.accentB, this.colors[2]);
|
|
1123
|
+
gl.uniform3fv(this.uniforms.glow, this.colors[3]);
|
|
1124
|
+
|
|
1125
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
setColors(colors) {
|
|
1129
|
+
this.colors = colors.map(hexToRgb01);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
dispose() {
|
|
1133
|
+
this.disposed = true;
|
|
1134
|
+
const gl = this.gl;
|
|
1135
|
+
this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
|
|
1136
|
+
this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
|
|
1137
|
+
gl.deleteBuffer(this.buffer);
|
|
1138
|
+
gl.deleteTexture(this.motionTexture);
|
|
1139
|
+
gl.deleteTexture(this.effectTexture);
|
|
1140
|
+
gl.deleteProgram(this.program);
|
|
1141
|
+
const lose = gl.getExtension('WEBGL_lose_context');
|
|
1142
|
+
if (lose) lose.loseContext();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function createProgressFlowRenderer(canvas, preset, options = {}) {
|
|
1147
|
+
try {
|
|
1148
|
+
return new ProgressFlowRenderer(canvas, preset, options);
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
console.warn('[画境观屿] 进度流体 WebGL2 不可用,使用 Canvas 2D 降级。', error);
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const PROGRESS_REFERENCE_DURATION = 12;
|
|
1156
|
+
const PROGRESS_REFERENCE_FRAME_COUNT = 24;
|
|
1157
|
+
|
|
1158
|
+
const FRAME_WIDTH = 64;
|
|
1159
|
+
const FRAME_HEIGHT = 32;
|
|
1160
|
+
const CACHE = Object.create(null);
|
|
1161
|
+
|
|
1162
|
+
const PALETTES = {
|
|
1163
|
+
'model-training': ['#20131f', '#ff3f94', '#ff8a3d', '#fff06a'],
|
|
1164
|
+
'agent-migration': ['#111a31', '#245bff', '#00cfff', '#5dffe6'],
|
|
1165
|
+
'visual-training': ['#1f172b', '#7042ff', '#42f58d', '#c4ff8a'],
|
|
1166
|
+
'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff'],
|
|
1167
|
+
'botanic-lab': ['#102a23', '#287a58', '#9bd65b', '#f2ffc4']
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
|
|
1171
|
+
context.save();
|
|
1172
|
+
context.translate(x, y);
|
|
1173
|
+
context.scale(1, radiusY / radiusX);
|
|
1174
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
1175
|
+
const alphaHex = (value) => {
|
|
1176
|
+
const text = Math.round(value).toString(16);
|
|
1177
|
+
return text.length === 1 ? `0${text}` : text;
|
|
1178
|
+
};
|
|
1179
|
+
gradient.addColorStop(0, `${color}${alphaHex(alpha * 255)}`);
|
|
1180
|
+
gradient.addColorStop(0.46, `${color}${alphaHex(alpha * 0.42 * 255)}`);
|
|
1181
|
+
gradient.addColorStop(1, `${color}00`);
|
|
1182
|
+
context.fillStyle = gradient;
|
|
1183
|
+
context.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
1184
|
+
context.restore();
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function createAtlas(id) {
|
|
1188
|
+
const palette = PALETTES[id] || PALETTES['visual-training'];
|
|
1189
|
+
const canvas = document.createElement('canvas');
|
|
1190
|
+
canvas.width = FRAME_WIDTH * PROGRESS_REFERENCE_FRAME_COUNT;
|
|
1191
|
+
canvas.height = FRAME_HEIGHT;
|
|
1192
|
+
const context = canvas.getContext('2d');
|
|
1193
|
+
|
|
1194
|
+
for (let frame = 0; frame < PROGRESS_REFERENCE_FRAME_COUNT; frame += 1) {
|
|
1195
|
+
const phase = (frame / PROGRESS_REFERENCE_FRAME_COUNT) * Math.PI * 2;
|
|
1196
|
+
const left = frame * FRAME_WIDTH;
|
|
1197
|
+
context.save();
|
|
1198
|
+
context.translate(left, 0);
|
|
1199
|
+
context.fillStyle = palette[0];
|
|
1200
|
+
context.fillRect(0, 0, FRAME_WIDTH, FRAME_HEIGHT);
|
|
1201
|
+
context.globalCompositeOperation = 'screen';
|
|
1202
|
+
|
|
1203
|
+
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);
|
|
1204
|
+
drawCloud(context, 45 + Math.cos(phase * 0.72) * 4, 23 + Math.sin(phase * 0.54) * 4, 22, 15, palette[2], 0.44);
|
|
1205
|
+
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);
|
|
1206
|
+
|
|
1207
|
+
context.globalCompositeOperation = 'source-over';
|
|
1208
|
+
const trough = context.createRadialGradient(41, 16, 1, 41, 16, 17);
|
|
1209
|
+
trough.addColorStop(0, 'rgba(5,6,11,0.64)');
|
|
1210
|
+
trough.addColorStop(0.58, 'rgba(6,7,12,0.26)');
|
|
1211
|
+
trough.addColorStop(1, 'rgba(6,7,12,0)');
|
|
1212
|
+
context.fillStyle = trough;
|
|
1213
|
+
context.fillRect(20, 0, 44, FRAME_HEIGHT);
|
|
1214
|
+
context.restore();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// A canvas is a valid TexImageSource. Keeping it directly avoids a PNG
|
|
1218
|
+
// encode -> base64 allocation -> Image decode round trip at startup.
|
|
1219
|
+
return { image: canvas, ready: true };
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function getProgressReferenceAtlas(id) {
|
|
1223
|
+
if (!CACHE[id]) CACHE[id] = createAtlas(id);
|
|
1224
|
+
return CACHE[id];
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Attach a WebGL2 fluid overlay to a progress capsule root.
|
|
1229
|
+
*
|
|
1230
|
+
* @param {object} params
|
|
1231
|
+
* @param {HTMLElement} params.root progress capsule root element
|
|
1232
|
+
* @param {HTMLCanvasElement} params.canvas 2D fallback canvas (kept beneath)
|
|
1233
|
+
* @param {object} params.preset progress preset
|
|
1234
|
+
* @param {() => number} params.getProgress reads the current progress value
|
|
1235
|
+
* @returns {{ update(flowTime: number): void, setDprCap(cap: number): void, dispose(): void } | null}
|
|
1236
|
+
*/
|
|
1237
|
+
function attachProgressFlowOverlay({
|
|
1238
|
+
root,
|
|
1239
|
+
canvas,
|
|
1240
|
+
preset,
|
|
1241
|
+
getProgress,
|
|
1242
|
+
dprCap = 2,
|
|
1243
|
+
powerPreference = 'high-performance',
|
|
1244
|
+
onContextLost,
|
|
1245
|
+
onContextRestored
|
|
1246
|
+
}) {
|
|
1247
|
+
const overlay = document.createElement('canvas');
|
|
1248
|
+
overlay.className = 'hj-progress-canvas hj-progress-overlay';
|
|
1249
|
+
overlay.setAttribute('aria-hidden', 'true');
|
|
1250
|
+
canvas.insertAdjacentElement('afterend', overlay);
|
|
1251
|
+
|
|
1252
|
+
const renderer = createProgressFlowRenderer(overlay, preset, {
|
|
1253
|
+
powerPreference,
|
|
1254
|
+
onContextLost,
|
|
1255
|
+
onContextRestored
|
|
1256
|
+
});
|
|
1257
|
+
if (!renderer) {
|
|
1258
|
+
overlay.remove();
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const atlas = getProgressReferenceAtlas(preset.id);
|
|
1263
|
+
root.classList.add('has-webgl-progress');
|
|
1264
|
+
|
|
1265
|
+
const resize = () => {
|
|
1266
|
+
const bounds = root.getBoundingClientRect();
|
|
1267
|
+
const width = Math.max(1, bounds.width);
|
|
1268
|
+
const height = Math.max(1, bounds.height);
|
|
1269
|
+
const dpr = Math.min(window.devicePixelRatio || 1, dprCap);
|
|
1270
|
+
overlay.style.width = `${width}px`;
|
|
1271
|
+
overlay.style.height = `${height}px`;
|
|
1272
|
+
renderer.resize(width, height, dpr);
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
let resizeObserver = null;
|
|
1276
|
+
let onWindowResize = null;
|
|
1277
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1278
|
+
resizeObserver = new ResizeObserver(resize);
|
|
1279
|
+
resizeObserver.observe(root);
|
|
1280
|
+
} else {
|
|
1281
|
+
onWindowResize = resize;
|
|
1282
|
+
window.addEventListener('resize', onWindowResize);
|
|
1283
|
+
}
|
|
1284
|
+
resize();
|
|
1285
|
+
|
|
1286
|
+
return {
|
|
1287
|
+
update(flowTime) {
|
|
1288
|
+
const effectTime = flowTime % PROGRESS_REFERENCE_DURATION;
|
|
1289
|
+
const progress = getProgress();
|
|
1290
|
+
renderer.draw(
|
|
1291
|
+
effectTime,
|
|
1292
|
+
Number.isFinite(progress) ? progress : preset.initialProgress,
|
|
1293
|
+
atlas.ready ? atlas.image : null
|
|
1294
|
+
);
|
|
1295
|
+
},
|
|
1296
|
+
setColors(colors) {
|
|
1297
|
+
renderer.setColors(colors);
|
|
1298
|
+
},
|
|
1299
|
+
setDprCap(cap) {
|
|
1300
|
+
dprCap = cap;
|
|
1301
|
+
resize();
|
|
1302
|
+
},
|
|
1303
|
+
dispose() {
|
|
1304
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
1305
|
+
if (onWindowResize) window.removeEventListener('resize', onWindowResize);
|
|
1306
|
+
renderer.dispose();
|
|
1307
|
+
overlay.remove();
|
|
1308
|
+
root.classList.remove('has-webgl-progress');
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1206
1311
|
}
|
|
1207
1312
|
|
|
1208
1313
|
/**
|
|
@@ -1221,1228 +1326,1228 @@ function nextTick(fn) {
|
|
|
1221
1326
|
}
|
|
1222
1327
|
}
|
|
1223
1328
|
|
|
1224
|
-
function toFiniteNumber(value) {
|
|
1225
|
-
if (value == null || typeof value === 'boolean') return null;
|
|
1226
|
-
if (typeof value === 'string' && value.trim() === '') return null;
|
|
1227
|
-
const number = Number(value);
|
|
1228
|
-
return Number.isFinite(number) ? number : null;
|
|
1329
|
+
function toFiniteNumber(value) {
|
|
1330
|
+
if (value == null || typeof value === 'boolean') return null;
|
|
1331
|
+
if (typeof value === 'string' && value.trim() === '') return null;
|
|
1332
|
+
const number = Number(value);
|
|
1333
|
+
return Number.isFinite(number) ? number : null;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function normalizeRange(min, max, fallbackMin = 0, fallbackMax = 100) {
|
|
1337
|
+
let nextMin = toFiniteNumber(min);
|
|
1338
|
+
let nextMax = toFiniteNumber(max);
|
|
1339
|
+
if (nextMin === null) nextMin = fallbackMin;
|
|
1340
|
+
if (nextMax === null) nextMax = fallbackMax;
|
|
1341
|
+
if (nextMin > nextMax) {
|
|
1342
|
+
const swap = nextMin;
|
|
1343
|
+
nextMin = nextMax;
|
|
1344
|
+
nextMax = swap;
|
|
1345
|
+
}
|
|
1346
|
+
return [nextMin, nextMax];
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function clampProgressValue(value, min, max) {
|
|
1350
|
+
const number = toFiniteNumber(value);
|
|
1351
|
+
if (number === null) return null;
|
|
1352
|
+
return Math.min(Math.max(number, min), max);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function normalizeProgressStep(step, fallback = 'any') {
|
|
1356
|
+
if (step === 'any' || step == null) return 'any';
|
|
1357
|
+
const increment = toFiniteNumber(step);
|
|
1358
|
+
return increment !== null && increment > 0 ? increment : fallback;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function snapProgressValue(value, min, max, step = 'any') {
|
|
1362
|
+
const clamped = clampProgressValue(value, min, max);
|
|
1363
|
+
if (clamped === null) return null;
|
|
1364
|
+
if (step === 'any' || step == null) return clamped;
|
|
1365
|
+
const increment = normalizeProgressStep(step);
|
|
1366
|
+
if (increment === 'any') return clamped;
|
|
1367
|
+
const snapped = min + Math.round((clamped - min) / increment) * increment;
|
|
1368
|
+
return Math.min(Math.max(Number(snapped.toFixed(12)), min), max);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function progressRatio(value, min, max) {
|
|
1372
|
+
const range = max - min;
|
|
1373
|
+
if (!Number.isFinite(range) || range <= 0) return 0;
|
|
1374
|
+
return Math.min(Math.max((value - min) / range, 0), 1);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function normalizeFps(value, fallback = 60) {
|
|
1378
|
+
const fps = Number(value);
|
|
1379
|
+
if (!Number.isFinite(fps)) return fallback;
|
|
1380
|
+
return Math.min(60, Math.max(1, fps));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function createFrameGate(initialFps = 60) {
|
|
1384
|
+
let fps = normalizeFps(initialFps);
|
|
1385
|
+
let elapsed = 0;
|
|
1386
|
+
return {
|
|
1387
|
+
shouldDraw(delta) {
|
|
1388
|
+
elapsed += delta;
|
|
1389
|
+
const interval = 1 / fps;
|
|
1390
|
+
if (elapsed + 0.0001 < interval) return false;
|
|
1391
|
+
elapsed %= interval;
|
|
1392
|
+
return true;
|
|
1393
|
+
},
|
|
1394
|
+
setFps(value) {
|
|
1395
|
+
fps = normalizeFps(value, fps);
|
|
1396
|
+
elapsed = 0;
|
|
1397
|
+
return fps;
|
|
1398
|
+
},
|
|
1399
|
+
getFps() {
|
|
1400
|
+
return fps;
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function createReducedMotionPreference(enabled, onChange) {
|
|
1406
|
+
const query = enabled && typeof matchMedia !== 'undefined'
|
|
1407
|
+
? matchMedia('(prefers-reduced-motion: reduce)')
|
|
1408
|
+
: null;
|
|
1409
|
+
const notify = () => {
|
|
1410
|
+
if (typeof onChange === 'function') onChange(Boolean(query && query.matches));
|
|
1411
|
+
};
|
|
1412
|
+
if (query) {
|
|
1413
|
+
if (typeof query.addEventListener === 'function') query.addEventListener('change', notify);
|
|
1414
|
+
else if (typeof query.addListener === 'function') query.addListener(notify);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
matches() {
|
|
1418
|
+
return Boolean(query && query.matches);
|
|
1419
|
+
},
|
|
1420
|
+
dispose() {
|
|
1421
|
+
if (!query) return;
|
|
1422
|
+
if (typeof query.removeEventListener === 'function') query.removeEventListener('change', notify);
|
|
1423
|
+
else if (typeof query.removeListener === 'function') query.removeListener(notify);
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
1429
|
+
const SUPPORTS_CTX_FILTER = typeof CanvasRenderingContext2D !== 'undefined' && 'filter' in CanvasRenderingContext2D.prototype;
|
|
1430
|
+
|
|
1431
|
+
function stringSeed(value) {
|
|
1432
|
+
let hash = 2166136261;
|
|
1433
|
+
for (const character of value) {
|
|
1434
|
+
hash ^= character.charCodeAt(0);
|
|
1435
|
+
hash = Math.imul(hash, 16777619);
|
|
1436
|
+
}
|
|
1437
|
+
return (hash >>> 0) / 4294967295;
|
|
1229
1438
|
}
|
|
1230
1439
|
|
|
1231
|
-
function
|
|
1232
|
-
|
|
1233
|
-
let nextMax = toFiniteNumber(max);
|
|
1234
|
-
if (nextMin === null) nextMin = fallbackMin;
|
|
1235
|
-
if (nextMax === null) nextMax = fallbackMax;
|
|
1236
|
-
if (nextMin > nextMax) {
|
|
1237
|
-
const swap = nextMin;
|
|
1238
|
-
nextMin = nextMax;
|
|
1239
|
-
nextMax = swap;
|
|
1240
|
-
}
|
|
1241
|
-
return [nextMin, nextMax];
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
function clampProgressValue(value, min, max) {
|
|
1245
|
-
const number = toFiniteNumber(value);
|
|
1246
|
-
if (number === null) return null;
|
|
1247
|
-
return Math.min(Math.max(number, min), max);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
function normalizeProgressStep(step, fallback = 'any') {
|
|
1251
|
-
if (step === 'any' || step == null) return 'any';
|
|
1252
|
-
const increment = toFiniteNumber(step);
|
|
1253
|
-
return increment !== null && increment > 0 ? increment : fallback;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
function snapProgressValue(value, min, max, step = 'any') {
|
|
1257
|
-
const clamped = clampProgressValue(value, min, max);
|
|
1258
|
-
if (clamped === null) return null;
|
|
1259
|
-
if (step === 'any' || step == null) return clamped;
|
|
1260
|
-
const increment = normalizeProgressStep(step);
|
|
1261
|
-
if (increment === 'any') return clamped;
|
|
1262
|
-
const snapped = min + Math.round((clamped - min) / increment) * increment;
|
|
1263
|
-
return Math.min(Math.max(Number(snapped.toFixed(12)), min), max);
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
function progressRatio(value, min, max) {
|
|
1267
|
-
const range = max - min;
|
|
1268
|
-
if (!Number.isFinite(range) || range <= 0) return 0;
|
|
1269
|
-
return Math.min(Math.max((value - min) / range, 0), 1);
|
|
1440
|
+
function interpolate(template, values) {
|
|
1441
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => (values[key] !== undefined ? values[key] : ''));
|
|
1270
1442
|
}
|
|
1271
1443
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
:
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1444
|
+
const FLOW_PROFILES = {
|
|
1445
|
+
'model-training': {
|
|
1446
|
+
cycles: [0.98, 3.05, 5.45],
|
|
1447
|
+
amplitudes: [11.2, 11.0, 5.2],
|
|
1448
|
+
speeds: [0.31, -0.53, 0.78],
|
|
1449
|
+
bulgeAmplitude: 8.2,
|
|
1450
|
+
timeScale: 1.0,
|
|
1451
|
+
glowWidth: 5.8,
|
|
1452
|
+
haloWidth: 20,
|
|
1453
|
+
whiteAlpha: 0.72,
|
|
1454
|
+
whiteWidth: 1.15,
|
|
1455
|
+
cloudWidth: 0.17,
|
|
1456
|
+
autoRange: [25, 66]
|
|
1457
|
+
},
|
|
1458
|
+
'agent-migration': {
|
|
1459
|
+
cycles: [0.62, 1.55, 2.95],
|
|
1460
|
+
amplitudes: [16.0, 7.8, 2.3],
|
|
1461
|
+
speeds: [0.25, -0.4, 0.60],
|
|
1462
|
+
bulgeAmplitude: 8.4,
|
|
1463
|
+
timeScale: 0.82,
|
|
1464
|
+
glowWidth: 6.3,
|
|
1465
|
+
haloWidth: 21,
|
|
1466
|
+
whiteAlpha: 0.18,
|
|
1467
|
+
whiteWidth: 0.45,
|
|
1468
|
+
cloudWidth: 0.18,
|
|
1469
|
+
autoRange: [24, 62]
|
|
1470
|
+
},
|
|
1471
|
+
'visual-training': {
|
|
1472
|
+
cycles: [0.88, 2.45, 4.35],
|
|
1473
|
+
amplitudes: [12.8, 10.2, 4.3],
|
|
1474
|
+
speeds: [0.28, -0.47, 0.69],
|
|
1475
|
+
bulgeAmplitude: 7.3,
|
|
1476
|
+
timeScale: 0.91,
|
|
1477
|
+
glowWidth: 6.0,
|
|
1478
|
+
haloWidth: 21,
|
|
1479
|
+
whiteAlpha: 0.30,
|
|
1480
|
+
whiteWidth: 0.55,
|
|
1481
|
+
cloudWidth: 0.175,
|
|
1482
|
+
autoRange: [20, 75]
|
|
1483
|
+
},
|
|
1484
|
+
// ponytail: first-pass tide = asymmetric surge on the 2D fallback path.
|
|
1485
|
+
// Tune surge/amplitudes after visual QA against the WebGL overlay.
|
|
1486
|
+
'tide': {
|
|
1487
|
+
cycles: [0.72, 1.9, 4.2],
|
|
1488
|
+
amplitudes: [19.0, 6.5, 1.5],
|
|
1489
|
+
speeds: [0.42, -0.5, 0.66],
|
|
1490
|
+
bulgeAmplitude: 9.5,
|
|
1491
|
+
timeScale: 0.85,
|
|
1492
|
+
glowWidth: 6.2,
|
|
1493
|
+
haloWidth: 21,
|
|
1494
|
+
whiteAlpha: 0.5,
|
|
1495
|
+
whiteWidth: 1.0,
|
|
1496
|
+
cloudWidth: 0.18,
|
|
1497
|
+
autoRange: [18, 70],
|
|
1498
|
+
surge: 0.55
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
class ProgressCapsuleController {
|
|
1503
|
+
constructor({ root, canvas, valueElement, preset, emitter, options, copy, dirty }) {
|
|
1504
|
+
this.root = root;
|
|
1505
|
+
this.canvas = canvas;
|
|
1506
|
+
this.valueElement = valueElement;
|
|
1507
|
+
this.preset = preset;
|
|
1508
|
+
this.emitter = emitter;
|
|
1509
|
+
this.options = options;
|
|
1510
|
+
this.copy = copy;
|
|
1511
|
+
this.dirty = dirty;
|
|
1512
|
+
this.valueSuffix = options.valueSuffix ?? copy.valueSuffix;
|
|
1513
|
+
this.profile = preset.edgeStyle === 'tide'
|
|
1514
|
+
? FLOW_PROFILES.tide
|
|
1515
|
+
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
1516
|
+
[this.min, this.max] = normalizeRange(options.min, options.max);
|
|
1517
|
+
this.step = normalizeProgressStep(options.step);
|
|
1518
|
+
this.precision = Number.isInteger(Number(options.precision))
|
|
1519
|
+
? clamp(Number(options.precision), 0, 20)
|
|
1520
|
+
: 0;
|
|
1521
|
+
this.formatValue = typeof options.formatValue === 'function' ? options.formatValue : null;
|
|
1522
|
+
this.direction = options.direction === 'rtl' ? 'rtl' : 'ltr';
|
|
1523
|
+
this.dprCap = effectiveDprCap(options.quality, options.renderScale);
|
|
1524
|
+
this.ctx = canvas.getContext('2d');
|
|
1525
|
+
this.value = snapProgressValue(options.value ?? preset.initialProgress, this.min, this.max, this.step);
|
|
1526
|
+
if (this.value === null) this.value = clamp(preset.initialProgress, this.min, this.max);
|
|
1527
|
+
this.dragging = false;
|
|
1528
|
+
this.webglActive = false;
|
|
1529
|
+
this.flowTime = stringSeed(preset.id) * 31;
|
|
1530
|
+
this.seed = stringSeed(`${preset.id}-reference`) * Math.PI * 2;
|
|
1531
|
+
this.randomState = Math.floor(stringSeed(`${preset.id}-auto`) * 0x7fffffff) || 1;
|
|
1532
|
+
this.dpr = 1;
|
|
1533
|
+
this.width = 0;
|
|
1534
|
+
this.height = 0;
|
|
1535
|
+
this.handlers = {};
|
|
1536
|
+
|
|
1537
|
+
this.onResize = () => {
|
|
1538
|
+
this.resizeCanvas();
|
|
1539
|
+
if (typeof this.options.onResize === 'function') this.options.onResize();
|
|
1540
|
+
};
|
|
1541
|
+
this.resizeObserver = typeof ResizeObserver !== 'undefined'
|
|
1542
|
+
? new ResizeObserver(this.onResize)
|
|
1543
|
+
: null;
|
|
1544
|
+
if (this.resizeObserver) this.resizeObserver.observe(this.root);
|
|
1545
|
+
else window.addEventListener('resize', this.onResize);
|
|
1546
|
+
|
|
1547
|
+
this.suppressEvents = true;
|
|
1548
|
+
this.setProgress(this.value, 'init');
|
|
1549
|
+
this.suppressEvents = false;
|
|
1550
|
+
this.bindEvents();
|
|
1551
|
+
this.resizeCanvas();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
random() {
|
|
1555
|
+
this.randomState = (Math.imul(this.randomState, 1664525) + 1013904223) >>> 0;
|
|
1556
|
+
return this.randomState / 4294967296;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
displayText() {
|
|
1560
|
+
if (this.formatValue) {
|
|
1561
|
+
return String(this.formatValue(this.value, {
|
|
1562
|
+
min: this.min,
|
|
1563
|
+
max: this.max,
|
|
1564
|
+
suffix: this.valueSuffix
|
|
1565
|
+
}));
|
|
1566
|
+
}
|
|
1567
|
+
const number = this.precision > 0 ? this.value.toFixed(this.precision) : String(Math.round(this.value));
|
|
1568
|
+
return `${number}${this.valueSuffix}`;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
syncValueDom() {
|
|
1572
|
+
const text = this.displayText();
|
|
1573
|
+
this.root.style.setProperty('--progress', this.value.toFixed(2));
|
|
1574
|
+
this.root.style.setProperty('--progress-ratio', progressRatio(this.value, this.min, this.max).toFixed(4));
|
|
1575
|
+
this.root.setAttribute('aria-valuenow', String(this.value));
|
|
1576
|
+
this.root.setAttribute('aria-valuetext', text);
|
|
1577
|
+
if (this.valueElement) this.valueElement.textContent = text;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
setProgress(nextValue, source = 'auto') {
|
|
1581
|
+
const next = snapProgressValue(nextValue, this.min, this.max, this.step);
|
|
1582
|
+
if (next === null) return false;
|
|
1583
|
+
this.value = next;
|
|
1584
|
+
this.syncValueDom();
|
|
1585
|
+
if (!this.suppressEvents) {
|
|
1586
|
+
if (this.dirty) this.dirty.value = true;
|
|
1587
|
+
this.emitter.emit('change', { value: this.value, source });
|
|
1588
|
+
}
|
|
1589
|
+
return true;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
setValueSuffix(suffix) {
|
|
1593
|
+
if (this.dirty) this.dirty.valueSuffix = true;
|
|
1594
|
+
this.valueSuffix = String(suffix == null ? '' : suffix);
|
|
1595
|
+
this.syncValueDom();
|
|
1596
|
+
return this;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
setStep(step) {
|
|
1600
|
+
const nextStep = normalizeProgressStep(step, this.step);
|
|
1601
|
+
if (nextStep === this.step) return this;
|
|
1602
|
+
if (this.dirty) this.dirty.step = true;
|
|
1603
|
+
this.step = nextStep;
|
|
1604
|
+
const next = snapProgressValue(this.value, this.min, this.max, this.step);
|
|
1605
|
+
if (next !== null && next !== this.value) this.setProgress(next, 'prop');
|
|
1606
|
+
return this;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
setPrecision(precision) {
|
|
1610
|
+
const value = Number(precision);
|
|
1611
|
+
if (!Number.isInteger(value) || value < 0 || value > 20) return this;
|
|
1612
|
+
if (this.dirty) this.dirty.precision = true;
|
|
1613
|
+
this.precision = value;
|
|
1614
|
+
this.syncValueDom();
|
|
1615
|
+
return this;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
setFormatValue(formatter) {
|
|
1619
|
+
if (formatter != null && typeof formatter !== 'function') return this;
|
|
1620
|
+
if (this.dirty) this.dirty.formatValue = true;
|
|
1621
|
+
this.formatValue = formatter || null;
|
|
1622
|
+
this.syncValueDom();
|
|
1623
|
+
return this;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
setDirection(direction) {
|
|
1627
|
+
if (this.dirty) this.dirty.direction = true;
|
|
1628
|
+
this.direction = direction === 'rtl' ? 'rtl' : 'ltr';
|
|
1629
|
+
this.root.dataset.direction = this.direction;
|
|
1630
|
+
return this;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
setShowValue(show) {
|
|
1634
|
+
if (this.dirty) this.dirty.showValue = true;
|
|
1635
|
+
if (this.valueElement) this.valueElement.hidden = show === false;
|
|
1636
|
+
return this;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
resizeCanvas() {
|
|
1640
|
+
const bounds = this.root.getBoundingClientRect();
|
|
1641
|
+
this.dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
|
|
1642
|
+
this.width = Math.max(1, bounds.width);
|
|
1643
|
+
this.height = Math.max(1, bounds.height);
|
|
1644
|
+
this.canvas.width = Math.round(this.width * this.dpr);
|
|
1645
|
+
this.canvas.height = Math.round(this.height * this.dpr);
|
|
1646
|
+
this.canvas.style.width = `${this.width}px`;
|
|
1647
|
+
this.canvas.style.height = `${this.height}px`;
|
|
1648
|
+
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
edgeEnvelope(yRatio) {
|
|
1652
|
+
const edge = Math.sin(Math.PI * clamp(yRatio, 0, 1));
|
|
1653
|
+
return Math.pow(Math.max(edge, 0), 0.48);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
localBulge(yRatio, time, index) {
|
|
1657
|
+
const direction = index === 0 ? 1 : -1;
|
|
1658
|
+
const center = 0.28 + index * 0.40 + Math.sin(time * (0.19 + index * 0.035) + this.seed * (1.1 + index)) * 0.13;
|
|
1659
|
+
const width = 0.075 + index * 0.016 + Math.sin(time * 0.13 + this.seed * 2.1) * 0.012;
|
|
1660
|
+
const distance = (yRatio - center) / Math.max(width, 0.035);
|
|
1661
|
+
const gaussian = Math.exp(-0.5 * distance * distance);
|
|
1662
|
+
return gaussian * Math.sin(time * (0.71 + index * 0.09) + this.seed * (2.7 + index)) * this.profile.bulgeAmplitude * direction;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
edgeOffset(y, time, phase = 0, amplitudeScale = 1) {
|
|
1666
|
+
const yRatio = this.height > 0 ? y / this.height : 0;
|
|
1667
|
+
const envelope = this.edgeEnvelope(yRatio);
|
|
1668
|
+
const scaledTime = time * this.profile.timeScale;
|
|
1669
|
+
const phaseTime = this.profile.surge
|
|
1670
|
+
? scaledTime + this.profile.surge * Math.sin(scaledTime * 2)
|
|
1671
|
+
: scaledTime;
|
|
1672
|
+
let offset = 0;
|
|
1673
|
+
|
|
1674
|
+
for (let index = 0; index < this.profile.cycles.length; index += 1) {
|
|
1675
|
+
const cycle = this.profile.cycles[index];
|
|
1676
|
+
const amplitude = this.profile.amplitudes[index];
|
|
1677
|
+
const speed = this.profile.speeds[index];
|
|
1678
|
+
const amplitudeMotion = 0.74 + 0.26 * Math.sin(
|
|
1679
|
+
phaseTime * (0.17 + index * 0.045) + this.seed * (index + 2.4)
|
|
1680
|
+
);
|
|
1681
|
+
offset += Math.sin(
|
|
1682
|
+
yRatio * Math.PI * 2 * cycle + phaseTime * speed * Math.PI * 2 + this.seed * (index + 1) + phase
|
|
1683
|
+
) * amplitude * amplitudeMotion;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
offset += this.localBulge(yRatio, phaseTime + phase, 0);
|
|
1687
|
+
offset += this.localBulge(yRatio, phaseTime - phase * 0.7, 1);
|
|
1688
|
+
return offset * envelope * amplitudeScale;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
createEdgePath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
|
|
1692
|
+
const step = Math.max(1.8, this.height / 92);
|
|
1693
|
+
ctx.beginPath();
|
|
1694
|
+
for (let y = 0; y <= this.height + step; y += step) {
|
|
1695
|
+
const x = baseX + this.edgeOffset(y, time, phase, amplitudeScale);
|
|
1696
|
+
if (y === 0) ctx.moveTo(x, y);
|
|
1697
|
+
else ctx.lineTo(x, y);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
createFillPath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
|
|
1702
|
+
const step = Math.max(1.8, this.height / 92);
|
|
1703
|
+
ctx.beginPath();
|
|
1704
|
+
ctx.moveTo(0, 0);
|
|
1705
|
+
ctx.lineTo(baseX + this.edgeOffset(0, time, phase, amplitudeScale), 0);
|
|
1706
|
+
for (let y = step; y <= this.height + step; y += step) {
|
|
1707
|
+
ctx.lineTo(baseX + this.edgeOffset(y, time, phase, amplitudeScale), y);
|
|
1708
|
+
}
|
|
1709
|
+
ctx.lineTo(0, this.height);
|
|
1710
|
+
ctx.closePath();
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
drawEllipticalGlow(x, y, radiusX, radiusY, color, alpha) {
|
|
1714
|
+
const ctx = this.ctx;
|
|
1715
|
+
ctx.save();
|
|
1716
|
+
ctx.translate(x, y);
|
|
1717
|
+
ctx.scale(1, radiusY / radiusX);
|
|
1718
|
+
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
1719
|
+
gradient.addColorStop(0, hexToRgba(color, alpha));
|
|
1720
|
+
gradient.addColorStop(0.42, hexToRgba(color, alpha * 0.48));
|
|
1721
|
+
gradient.addColorStop(1, hexToRgba(color, 0));
|
|
1722
|
+
ctx.globalCompositeOperation = 'screen';
|
|
1723
|
+
ctx.fillStyle = gradient;
|
|
1724
|
+
ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
1725
|
+
ctx.restore();
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
drawDarkEllipticalShadow(x, y, radiusX, radiusY, alpha) {
|
|
1729
|
+
const ctx = this.ctx;
|
|
1730
|
+
ctx.save();
|
|
1731
|
+
ctx.translate(x, y);
|
|
1732
|
+
ctx.scale(1, radiusY / radiusX);
|
|
1733
|
+
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
|
|
1734
|
+
gradient.addColorStop(0, `rgba(6, 6, 11, ${alpha})`);
|
|
1735
|
+
gradient.addColorStop(0.54, `rgba(8, 8, 14, ${alpha * 0.62})`);
|
|
1736
|
+
gradient.addColorStop(1, 'rgba(8, 8, 14, 0)');
|
|
1737
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1738
|
+
ctx.fillStyle = gradient;
|
|
1739
|
+
ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
|
|
1740
|
+
ctx.restore();
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
drawColorClouds(shoreline, time, accentA, accentB, glow) {
|
|
1744
|
+
this.ctx;
|
|
1745
|
+
const width = this.width;
|
|
1746
|
+
const height = this.height;
|
|
1747
|
+
const scale = this.profile.cloudWidth;
|
|
1748
|
+
const t = time * this.profile.timeScale;
|
|
1749
|
+
|
|
1750
|
+
const upperY = height * (0.28 + Math.sin(t * 0.24 + this.seed) * 0.13);
|
|
1751
|
+
const lowerY = height * (0.70 + Math.cos(t * 0.21 + this.seed * 1.7) * 0.12);
|
|
1752
|
+
const middleY = height * (0.49 + Math.sin(t * 0.31 + this.seed * 2.3) * 0.15);
|
|
1753
|
+
|
|
1754
|
+
const farX = shoreline - width * 0.095;
|
|
1755
|
+
const farRx = Math.max(52, width * scale);
|
|
1756
|
+
const farRy = height * 0.42;
|
|
1757
|
+
this.drawEllipticalGlow(farX, upperY, farRx, farRy, accentA, 0.38);
|
|
1758
|
+
this.drawDarkEllipticalShadow(
|
|
1759
|
+
farX + farRx * 0.16,
|
|
1760
|
+
upperY,
|
|
1761
|
+
farRx * 0.58,
|
|
1762
|
+
farRy * 0.66,
|
|
1763
|
+
0.74
|
|
1764
|
+
);
|
|
1765
|
+
|
|
1766
|
+
const lowerX = shoreline - width * 0.072;
|
|
1767
|
+
const lowerRx = Math.max(44, width * scale * 0.82);
|
|
1768
|
+
const lowerRy = height * 0.36;
|
|
1769
|
+
this.drawEllipticalGlow(lowerX, lowerY, lowerRx, lowerRy, accentB, 0.31);
|
|
1770
|
+
this.drawDarkEllipticalShadow(
|
|
1771
|
+
lowerX + lowerRx * 0.14,
|
|
1772
|
+
lowerY,
|
|
1773
|
+
lowerRx * 0.54,
|
|
1774
|
+
lowerRy * 0.62,
|
|
1775
|
+
0.64
|
|
1776
|
+
);
|
|
1777
|
+
|
|
1778
|
+
this.drawEllipticalGlow(
|
|
1779
|
+
shoreline - width * 0.034,
|
|
1780
|
+
middleY,
|
|
1781
|
+
Math.max(30, width * scale * 0.48),
|
|
1782
|
+
height * 0.27,
|
|
1783
|
+
glow,
|
|
1784
|
+
0.20
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
drawPathBand({ baseX, time, phase, amplitudeScale, color, alpha, blur, width, composite = 'screen' }) {
|
|
1789
|
+
const ctx = this.ctx;
|
|
1790
|
+
this.createEdgePath(ctx, baseX, time, phase, amplitudeScale);
|
|
1791
|
+
ctx.save();
|
|
1792
|
+
ctx.globalCompositeOperation = composite;
|
|
1793
|
+
ctx.globalAlpha = alpha;
|
|
1794
|
+
// Safari < 18 and some old WebViews ignore ctx.filter; setting it is a
|
|
1795
|
+
// no-op there, so only assign when supported to keep intent explicit.
|
|
1796
|
+
if (SUPPORTS_CTX_FILTER) ctx.filter = `blur(${blur}px)`;
|
|
1797
|
+
ctx.strokeStyle = color;
|
|
1798
|
+
ctx.lineWidth = width;
|
|
1799
|
+
ctx.stroke();
|
|
1800
|
+
ctx.restore();
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
setRange(min, max) {
|
|
1804
|
+
const nextRange = normalizeRange(min, max, this.min, this.max);
|
|
1805
|
+
this.min = nextRange[0];
|
|
1806
|
+
this.max = nextRange[1];
|
|
1807
|
+
this.root.setAttribute('aria-valuemin', String(this.min));
|
|
1808
|
+
this.root.setAttribute('aria-valuemax', String(this.max));
|
|
1809
|
+
const next = snapProgressValue(this.value, this.min, this.max, this.step);
|
|
1810
|
+
if (next !== this.value) this.setProgress(next, 'prop');
|
|
1811
|
+
else this.syncValueDom();
|
|
1812
|
+
return this;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
drawReferenceFlow() {
|
|
1816
|
+
const ctx = this.ctx;
|
|
1817
|
+
const width = this.width;
|
|
1818
|
+
const height = this.height;
|
|
1819
|
+
if (!ctx || width <= 0 || height <= 0) return;
|
|
1820
|
+
|
|
1821
|
+
const range = Math.max(this.max - this.min, 1);
|
|
1822
|
+
const shoreline = width * Math.min(Math.max((this.value - this.min) / range, 0), 1);
|
|
1823
|
+
const time = this.flowTime;
|
|
1824
|
+
const [dark, accentA, accentB, glow] = this.preset.colors;
|
|
1825
|
+
|
|
1826
|
+
ctx.clearRect(0, 0, width, height);
|
|
1827
|
+
ctx.fillStyle = '#202126';
|
|
1828
|
+
ctx.fillRect(0, 0, width, height);
|
|
1829
|
+
|
|
1830
|
+
this.createFillPath(ctx, shoreline, time, 0, 1);
|
|
1831
|
+
const bodyGradient = ctx.createLinearGradient(0, 0, Math.max(shoreline, 1), 0);
|
|
1832
|
+
bodyGradient.addColorStop(0, dark);
|
|
1833
|
+
bodyGradient.addColorStop(0.74, dark);
|
|
1834
|
+
bodyGradient.addColorStop(0.89, hexToRgba(dark, 0.99));
|
|
1835
|
+
bodyGradient.addColorStop(0.955, hexToRgba(accentA, 0.09));
|
|
1836
|
+
bodyGradient.addColorStop(0.992, hexToRgba(accentB, 0.54));
|
|
1837
|
+
bodyGradient.addColorStop(1, hexToRgba(glow, 0.78));
|
|
1838
|
+
ctx.fillStyle = bodyGradient;
|
|
1839
|
+
ctx.fill();
|
|
1840
|
+
|
|
1841
|
+
this.drawColorClouds(shoreline, time, accentA, accentB, glow);
|
|
1842
|
+
|
|
1843
|
+
this.drawPathBand({
|
|
1844
|
+
baseX: shoreline - width * 0.105,
|
|
1845
|
+
time,
|
|
1846
|
+
phase: 1.42,
|
|
1847
|
+
amplitudeScale: 1.18,
|
|
1848
|
+
color: accentA,
|
|
1849
|
+
alpha: 0.22,
|
|
1850
|
+
blur: 21,
|
|
1851
|
+
width: 54
|
|
1852
|
+
});
|
|
1853
|
+
this.drawPathBand({
|
|
1854
|
+
baseX: shoreline - width * 0.073,
|
|
1855
|
+
time,
|
|
1856
|
+
phase: -0.92,
|
|
1857
|
+
amplitudeScale: 1.06,
|
|
1858
|
+
color: accentB,
|
|
1859
|
+
alpha: 0.30,
|
|
1860
|
+
blur: 15,
|
|
1861
|
+
width: 42
|
|
1862
|
+
});
|
|
1863
|
+
this.drawPathBand({
|
|
1864
|
+
baseX: shoreline - width * 0.047,
|
|
1865
|
+
time,
|
|
1866
|
+
phase: 0.42,
|
|
1867
|
+
amplitudeScale: 0.94,
|
|
1868
|
+
color: 'rgba(5, 5, 10, 0.92)',
|
|
1869
|
+
alpha: 0.72,
|
|
1870
|
+
blur: 12,
|
|
1871
|
+
width: 34,
|
|
1872
|
+
composite: 'source-over'
|
|
1873
|
+
});
|
|
1874
|
+
this.drawPathBand({
|
|
1875
|
+
baseX: shoreline - width * 0.025,
|
|
1876
|
+
time,
|
|
1877
|
+
phase: -0.28,
|
|
1878
|
+
amplitudeScale: 0.96,
|
|
1879
|
+
color: accentB,
|
|
1880
|
+
alpha: 0.66,
|
|
1881
|
+
blur: 8,
|
|
1882
|
+
width: 28
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
ctx.save();
|
|
1886
|
+
this.createFillPath(ctx, shoreline, time, 0, 1);
|
|
1887
|
+
ctx.clip();
|
|
1888
|
+
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
1889
|
+
|
|
1890
|
+
ctx.save();
|
|
1891
|
+
ctx.globalCompositeOperation = 'screen';
|
|
1892
|
+
ctx.strokeStyle = hexToRgba(accentA, 0.20);
|
|
1893
|
+
ctx.lineWidth = this.profile.haloWidth;
|
|
1894
|
+
ctx.shadowColor = accentA;
|
|
1895
|
+
ctx.shadowBlur = this.profile.haloWidth * 0.72;
|
|
1896
|
+
ctx.stroke();
|
|
1897
|
+
ctx.restore();
|
|
1898
|
+
|
|
1899
|
+
ctx.save();
|
|
1900
|
+
ctx.globalCompositeOperation = 'screen';
|
|
1901
|
+
ctx.strokeStyle = hexToRgba(accentB, 0.78);
|
|
1902
|
+
ctx.lineWidth = this.profile.glowWidth + 4.2;
|
|
1903
|
+
ctx.shadowColor = accentB;
|
|
1904
|
+
ctx.shadowBlur = 8;
|
|
1905
|
+
ctx.stroke();
|
|
1906
|
+
ctx.restore();
|
|
1907
|
+
|
|
1908
|
+
ctx.save();
|
|
1909
|
+
ctx.globalCompositeOperation = 'screen';
|
|
1910
|
+
ctx.strokeStyle = hexToRgba(glow, 0.88);
|
|
1911
|
+
ctx.lineWidth = this.profile.glowWidth;
|
|
1912
|
+
ctx.shadowColor = glow;
|
|
1913
|
+
ctx.shadowBlur = 5;
|
|
1914
|
+
ctx.stroke();
|
|
1915
|
+
ctx.restore();
|
|
1916
|
+
|
|
1917
|
+
ctx.restore();
|
|
1918
|
+
|
|
1919
|
+
ctx.save();
|
|
1920
|
+
ctx.globalCompositeOperation = 'screen';
|
|
1921
|
+
ctx.strokeStyle = hexToRgba(glow, 0.84);
|
|
1922
|
+
ctx.lineWidth = 2.15;
|
|
1923
|
+
ctx.shadowColor = glow;
|
|
1924
|
+
ctx.shadowBlur = 2.5;
|
|
1925
|
+
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
1926
|
+
ctx.stroke();
|
|
1927
|
+
ctx.restore();
|
|
1928
|
+
|
|
1929
|
+
if (this.profile.whiteAlpha > 0.05) {
|
|
1930
|
+
ctx.save();
|
|
1931
|
+
ctx.globalCompositeOperation = 'screen';
|
|
1932
|
+
ctx.strokeStyle = `rgba(255,255,245,${this.profile.whiteAlpha})`;
|
|
1933
|
+
ctx.lineWidth = this.profile.whiteWidth;
|
|
1934
|
+
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
1935
|
+
ctx.stroke();
|
|
1936
|
+
ctx.restore();
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
updateFromPointer(event) {
|
|
1941
|
+
const bounds = this.root.getBoundingClientRect();
|
|
1942
|
+
let ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
|
|
1943
|
+
if (this.direction === 'rtl') ratio = 1 - ratio;
|
|
1944
|
+
this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
onKeyDown(event) {
|
|
1948
|
+
const range = this.max - this.min;
|
|
1949
|
+
const configuredStep = Number(this.step);
|
|
1950
|
+
const step = Number.isFinite(configuredStep) && configuredStep > 0
|
|
1951
|
+
? configuredStep
|
|
1952
|
+
: Math.max(range / 100, 1e-7);
|
|
1953
|
+
const direction = this.direction === 'rtl' ? -1 : 1;
|
|
1954
|
+
const aliases = { Left: 'ArrowLeft', Right: 'ArrowRight', Up: 'ArrowUp', Down: 'ArrowDown' };
|
|
1955
|
+
const keyCodes = {
|
|
1956
|
+
35: 'End', 36: 'Home', 33: 'PageUp', 34: 'PageDown',
|
|
1957
|
+
37: 'ArrowLeft', 38: 'ArrowUp', 39: 'ArrowRight', 40: 'ArrowDown'
|
|
1958
|
+
};
|
|
1959
|
+
const key = aliases[event.key] || event.key || keyCodes[event.keyCode];
|
|
1960
|
+
let next = null;
|
|
1961
|
+
if (key === 'ArrowRight') next = this.value + step * direction;
|
|
1962
|
+
else if (key === 'ArrowLeft') next = this.value - step * direction;
|
|
1963
|
+
else if (key === 'ArrowUp') next = this.value + step;
|
|
1964
|
+
else if (key === 'ArrowDown') next = this.value - step;
|
|
1965
|
+
else if (key === 'PageUp') next = this.value + step * 10;
|
|
1966
|
+
else if (key === 'PageDown') next = this.value - step * 10;
|
|
1967
|
+
else if (key === 'Home') next = this.min;
|
|
1968
|
+
else if (key === 'End') next = this.max;
|
|
1969
|
+
if (next === null) return;
|
|
1970
|
+
event.preventDefault();
|
|
1971
|
+
this.setProgress(next, 'keyboard');
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
beginDrag(event) {
|
|
1975
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
1976
|
+
event.preventDefault();
|
|
1977
|
+
// preventDefault 会吞掉点击聚焦,主动聚焦让根节点获得无障碍焦点。
|
|
1978
|
+
try { this.root.focus({ preventScroll: true }); } catch { this.root.focus(); }
|
|
1979
|
+
this.dragging = true;
|
|
1980
|
+
this.pendingPointer = null;
|
|
1981
|
+
this._dragRaf = 0;
|
|
1982
|
+
this.root.classList.add('is-dragging');
|
|
1983
|
+
try { this.root.setPointerCapture?.(event.pointerId); } catch {}
|
|
1984
|
+
this.emitter.emit('dragstart', { value: this.value });
|
|
1985
|
+
this.updateFromPointer(event);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
moveDrag(event) {
|
|
1989
|
+
if (!this.dragging) return;
|
|
1990
|
+
event.preventDefault();
|
|
1991
|
+
// rAF 合并:一帧最多一次 setProgress/change,视觉仍每帧更新。
|
|
1992
|
+
this.pendingPointer = event;
|
|
1993
|
+
if (this._dragRaf) return;
|
|
1994
|
+
this._dragRaf = requestAnimationFrame(() => {
|
|
1995
|
+
this._dragRaf = 0;
|
|
1996
|
+
const pending = this.pendingPointer;
|
|
1997
|
+
this.pendingPointer = null;
|
|
1998
|
+
if (pending) this.updateFromPointer(pending);
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
endDrag(event) {
|
|
2003
|
+
if (!this.dragging) return;
|
|
2004
|
+
this.dragging = false;
|
|
2005
|
+
if (this._dragRaf) {
|
|
2006
|
+
cancelAnimationFrame(this._dragRaf);
|
|
2007
|
+
this._dragRaf = 0;
|
|
2008
|
+
}
|
|
2009
|
+
const pending = this.pendingPointer;
|
|
2010
|
+
this.pendingPointer = null;
|
|
2011
|
+
if (pending) this.updateFromPointer(pending);
|
|
2012
|
+
this.root.classList.remove('is-dragging');
|
|
2013
|
+
try {
|
|
2014
|
+
if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
|
|
2015
|
+
this.root.releasePointerCapture(event.pointerId);
|
|
2016
|
+
}
|
|
2017
|
+
} catch {}
|
|
2018
|
+
this.emitter.emit('dragend', { value: this.value });
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
bindEvents() {
|
|
2022
|
+
if (this.options.draggable !== false) {
|
|
2023
|
+
this.handlers.pointerdown = (event) => this.beginDrag(event);
|
|
2024
|
+
this.handlers.pointermove = (event) => this.moveDrag(event);
|
|
2025
|
+
this.handlers.pointerup = (event) => this.endDrag(event);
|
|
2026
|
+
this.handlers.pointercancel = (event) => this.endDrag(event);
|
|
2027
|
+
this.root.addEventListener('pointerdown', this.handlers.pointerdown);
|
|
2028
|
+
this.root.addEventListener('pointermove', this.handlers.pointermove);
|
|
2029
|
+
this.root.addEventListener('pointerup', this.handlers.pointerup);
|
|
2030
|
+
this.root.addEventListener('pointercancel', this.handlers.pointercancel);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
if (this.options.keyboard !== false) {
|
|
2034
|
+
this.handlers.keydown = (event) => this.onKeyDown(event);
|
|
2035
|
+
this.root.addEventListener('keydown', this.handlers.keydown);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
update(delta, paused) {
|
|
2041
|
+
if (!paused) this.flowTime += delta;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
draw() {
|
|
2045
|
+
// Performance fix: when the WebGL overlay is active the 2D layer is
|
|
2046
|
+
// hidden behind it, so drawing it every frame would be wasted CPU.
|
|
2047
|
+
if (this.webglActive) return;
|
|
2048
|
+
this.drawReferenceFlow();
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
randomize() {
|
|
2052
|
+
this.flowTime = this.random() * 40;
|
|
2053
|
+
return this.flowTime;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
setColors(colors) {
|
|
2057
|
+
if (!Array.isArray(colors) || colors.length !== 4) return;
|
|
2058
|
+
this.preset.colors = [...colors];
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
dispose() {
|
|
2062
|
+
if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
|
|
2063
|
+
if (this.resizeObserver) this.resizeObserver.disconnect();
|
|
2064
|
+
else window.removeEventListener('resize', this.onResize);
|
|
2065
|
+
for (const name of Object.keys(this.handlers)) {
|
|
2066
|
+
const handler = this.handlers[name];
|
|
2067
|
+
this.root.removeEventListener(name, handler);
|
|
2068
|
+
}
|
|
2069
|
+
this.handlers = {};
|
|
2070
|
+
}
|
|
1321
2071
|
}
|
|
1322
2072
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
if (
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
);
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
ctx.restore();
|
|
1802
|
-
|
|
1803
|
-
ctx.save();
|
|
1804
|
-
ctx.globalCompositeOperation = 'screen';
|
|
1805
|
-
ctx.strokeStyle = hexToRgba(glow, 0.88);
|
|
1806
|
-
ctx.lineWidth = this.profile.glowWidth;
|
|
1807
|
-
ctx.shadowColor = glow;
|
|
1808
|
-
ctx.shadowBlur = 5;
|
|
1809
|
-
ctx.stroke();
|
|
1810
|
-
ctx.restore();
|
|
1811
|
-
|
|
1812
|
-
ctx.restore();
|
|
1813
|
-
|
|
1814
|
-
ctx.save();
|
|
1815
|
-
ctx.globalCompositeOperation = 'screen';
|
|
1816
|
-
ctx.strokeStyle = hexToRgba(glow, 0.84);
|
|
1817
|
-
ctx.lineWidth = 2.15;
|
|
1818
|
-
ctx.shadowColor = glow;
|
|
1819
|
-
ctx.shadowBlur = 2.5;
|
|
1820
|
-
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
1821
|
-
ctx.stroke();
|
|
1822
|
-
ctx.restore();
|
|
1823
|
-
|
|
1824
|
-
if (this.profile.whiteAlpha > 0.05) {
|
|
1825
|
-
ctx.save();
|
|
1826
|
-
ctx.globalCompositeOperation = 'screen';
|
|
1827
|
-
ctx.strokeStyle = `rgba(255,255,245,${this.profile.whiteAlpha})`;
|
|
1828
|
-
ctx.lineWidth = this.profile.whiteWidth;
|
|
1829
|
-
this.createEdgePath(ctx, shoreline, time, 0, 1);
|
|
1830
|
-
ctx.stroke();
|
|
1831
|
-
ctx.restore();
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
updateFromPointer(event) {
|
|
1836
|
-
const bounds = this.root.getBoundingClientRect();
|
|
1837
|
-
let ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
|
|
1838
|
-
if (this.direction === 'rtl') ratio = 1 - ratio;
|
|
1839
|
-
this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
onKeyDown(event) {
|
|
1843
|
-
const range = this.max - this.min;
|
|
1844
|
-
const configuredStep = Number(this.step);
|
|
1845
|
-
const step = Number.isFinite(configuredStep) && configuredStep > 0
|
|
1846
|
-
? configuredStep
|
|
1847
|
-
: Math.max(range / 100, 1e-7);
|
|
1848
|
-
const direction = this.direction === 'rtl' ? -1 : 1;
|
|
1849
|
-
const aliases = { Left: 'ArrowLeft', Right: 'ArrowRight', Up: 'ArrowUp', Down: 'ArrowDown' };
|
|
1850
|
-
const keyCodes = {
|
|
1851
|
-
35: 'End', 36: 'Home', 33: 'PageUp', 34: 'PageDown',
|
|
1852
|
-
37: 'ArrowLeft', 38: 'ArrowUp', 39: 'ArrowRight', 40: 'ArrowDown'
|
|
1853
|
-
};
|
|
1854
|
-
const key = aliases[event.key] || event.key || keyCodes[event.keyCode];
|
|
1855
|
-
let next = null;
|
|
1856
|
-
if (key === 'ArrowRight') next = this.value + step * direction;
|
|
1857
|
-
else if (key === 'ArrowLeft') next = this.value - step * direction;
|
|
1858
|
-
else if (key === 'ArrowUp') next = this.value + step;
|
|
1859
|
-
else if (key === 'ArrowDown') next = this.value - step;
|
|
1860
|
-
else if (key === 'PageUp') next = this.value + step * 10;
|
|
1861
|
-
else if (key === 'PageDown') next = this.value - step * 10;
|
|
1862
|
-
else if (key === 'Home') next = this.min;
|
|
1863
|
-
else if (key === 'End') next = this.max;
|
|
1864
|
-
if (next === null) return;
|
|
1865
|
-
event.preventDefault();
|
|
1866
|
-
this.setProgress(next, 'keyboard');
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
beginDrag(event) {
|
|
1870
|
-
if (event.button !== undefined && event.button !== 0) return;
|
|
1871
|
-
event.preventDefault();
|
|
1872
|
-
// preventDefault 会吞掉点击聚焦,主动聚焦让根节点获得无障碍焦点。
|
|
1873
|
-
try { this.root.focus({ preventScroll: true }); } catch { this.root.focus(); }
|
|
1874
|
-
this.dragging = true;
|
|
1875
|
-
this.pendingPointer = null;
|
|
1876
|
-
this._dragRaf = 0;
|
|
1877
|
-
this.root.classList.add('is-dragging');
|
|
1878
|
-
try { this.root.setPointerCapture?.(event.pointerId); } catch {}
|
|
1879
|
-
this.emitter.emit('dragstart', { value: this.value });
|
|
1880
|
-
this.updateFromPointer(event);
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
moveDrag(event) {
|
|
1884
|
-
if (!this.dragging) return;
|
|
1885
|
-
event.preventDefault();
|
|
1886
|
-
// rAF 合并:一帧最多一次 setProgress/change,视觉仍每帧更新。
|
|
1887
|
-
this.pendingPointer = event;
|
|
1888
|
-
if (this._dragRaf) return;
|
|
1889
|
-
this._dragRaf = requestAnimationFrame(() => {
|
|
1890
|
-
this._dragRaf = 0;
|
|
1891
|
-
const pending = this.pendingPointer;
|
|
1892
|
-
this.pendingPointer = null;
|
|
1893
|
-
if (pending) this.updateFromPointer(pending);
|
|
1894
|
-
});
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
endDrag(event) {
|
|
1898
|
-
if (!this.dragging) return;
|
|
1899
|
-
this.dragging = false;
|
|
1900
|
-
if (this._dragRaf) {
|
|
1901
|
-
cancelAnimationFrame(this._dragRaf);
|
|
1902
|
-
this._dragRaf = 0;
|
|
1903
|
-
}
|
|
1904
|
-
const pending = this.pendingPointer;
|
|
1905
|
-
this.pendingPointer = null;
|
|
1906
|
-
if (pending) this.updateFromPointer(pending);
|
|
1907
|
-
this.root.classList.remove('is-dragging');
|
|
1908
|
-
try {
|
|
1909
|
-
if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
|
|
1910
|
-
this.root.releasePointerCapture(event.pointerId);
|
|
1911
|
-
}
|
|
1912
|
-
} catch {}
|
|
1913
|
-
this.emitter.emit('dragend', { value: this.value });
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
bindEvents() {
|
|
1917
|
-
if (this.options.draggable !== false) {
|
|
1918
|
-
this.handlers.pointerdown = (event) => this.beginDrag(event);
|
|
1919
|
-
this.handlers.pointermove = (event) => this.moveDrag(event);
|
|
1920
|
-
this.handlers.pointerup = (event) => this.endDrag(event);
|
|
1921
|
-
this.handlers.pointercancel = (event) => this.endDrag(event);
|
|
1922
|
-
this.root.addEventListener('pointerdown', this.handlers.pointerdown);
|
|
1923
|
-
this.root.addEventListener('pointermove', this.handlers.pointermove);
|
|
1924
|
-
this.root.addEventListener('pointerup', this.handlers.pointerup);
|
|
1925
|
-
this.root.addEventListener('pointercancel', this.handlers.pointercancel);
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
if (this.options.keyboard !== false) {
|
|
1929
|
-
this.handlers.keydown = (event) => this.onKeyDown(event);
|
|
1930
|
-
this.root.addEventListener('keydown', this.handlers.keydown);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
update(delta, paused) {
|
|
1936
|
-
if (!paused) this.flowTime += delta;
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
draw() {
|
|
1940
|
-
// Performance fix: when the WebGL overlay is active the 2D layer is
|
|
1941
|
-
// hidden behind it, so drawing it every frame would be wasted CPU.
|
|
1942
|
-
if (this.webglActive) return;
|
|
1943
|
-
this.drawReferenceFlow();
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
randomize() {
|
|
1947
|
-
this.flowTime = this.random() * 40;
|
|
1948
|
-
return this.flowTime;
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
setColors(colors) {
|
|
1952
|
-
if (!Array.isArray(colors) || colors.length !== 4) return;
|
|
1953
|
-
this.preset.colors = [...colors];
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
dispose() {
|
|
1957
|
-
if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
|
|
1958
|
-
if (this.resizeObserver) this.resizeObserver.disconnect();
|
|
1959
|
-
else window.removeEventListener('resize', this.onResize);
|
|
1960
|
-
for (const name of Object.keys(this.handlers)) {
|
|
1961
|
-
const handler = this.handlers[name];
|
|
1962
|
-
this.root.removeEventListener(name, handler);
|
|
1963
|
-
}
|
|
1964
|
-
this.handlers = {};
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
/**
|
|
1969
|
-
* Mount a fluid progress capsule into `container`.
|
|
1970
|
-
*
|
|
1971
|
-
* Options: preset (literary name), width, height, value/modelValue, min, max,
|
|
1972
|
-
* step, draggable, keyboard, direction, precision/formatValue, colors,
|
|
1973
|
-
* edgeStyle, textRatio (0-100, text region
|
|
1974
|
-
* width in percent), text (HTML string or DOM nodes for the text slot),
|
|
1975
|
-
* colorContent (HTML string or DOM nodes for the color slot),
|
|
1976
|
-
* showValue (show/hide the right-side percentage), quality, renderScale,
|
|
1977
|
-
* powerPreference, fps, paused/static, respectReducedMotion, cssVars.
|
|
1978
|
-
*/
|
|
1979
|
-
function createProgressCapsule(container, options = {}) {
|
|
1980
|
-
if (!container || typeof container.appendChild !== 'function') {
|
|
1981
|
-
throw new Error('createProgressCapsule: container element is required');
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
const preset = { ...getPreset('progress', options.preset ?? '星火') };
|
|
1985
|
-
const merged = normalizeOptions(DEFAULTS.progress, preset, options);
|
|
1986
|
-
[merged.min, merged.max] = normalizeRange(merged.min, merged.max);
|
|
1987
|
-
if (merged.modelValue !== undefined) merged.value = merged.modelValue;
|
|
1988
|
-
const normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
|
|
1989
|
-
if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
1990
|
-
const initialEdgeStyle = merged.edgeStyle === 'tide' || merged.edgeStyle === 'flow'
|
|
1991
|
-
? merged.edgeStyle
|
|
1992
|
-
: preset.edgeStyle;
|
|
1993
|
-
preset.edgeStyle = initialEdgeStyle;
|
|
1994
|
-
merged.edgeStyle = initialEdgeStyle;
|
|
1995
|
-
merged.colors = [...preset.colors];
|
|
1996
|
-
const optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
|
|
1997
|
-
let colorOverride = optionColors.length === 4 && optionColors.every(Boolean)
|
|
1998
|
-
? [...optionColors]
|
|
1999
|
-
: null;
|
|
2000
|
-
let edgeStyleOverride = options.edgeStyle === 'flow' || options.edgeStyle === 'tide'
|
|
2001
|
-
? merged.edgeStyle
|
|
2002
|
-
: null;
|
|
2003
|
-
const copy = COPY;
|
|
2004
|
-
let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
|
|
2005
|
-
// Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
|
|
2006
|
-
// 这里统一按 true 处理。
|
|
2007
|
-
const disabled = merged.disabled === true || merged.disabled === '' || merged.disabled === 'true';
|
|
2008
|
-
const readonly = merged.readonly === true || merged.readonly === '' || merged.readonly === 'true';
|
|
2009
|
-
const locked = disabled || readonly;
|
|
2010
|
-
const effectiveDraggable = locked ? false : merged.draggable !== false;
|
|
2011
|
-
const effectiveKeyboard = locked ? false : merged.keyboard !== false;
|
|
2012
|
-
const dirty = {
|
|
2013
|
-
preset: false,
|
|
2014
|
-
colors: false,
|
|
2015
|
-
textRatio: false,
|
|
2016
|
-
cssVars: false,
|
|
2017
|
-
edgeStyle: false,
|
|
2018
|
-
value: false,
|
|
2019
|
-
step: false,
|
|
2020
|
-
precision: false,
|
|
2021
|
-
formatValue: false,
|
|
2022
|
-
direction: false,
|
|
2023
|
-
showValue: false,
|
|
2024
|
-
valueSuffix: false,
|
|
2025
|
-
quality: false,
|
|
2026
|
-
renderScale: false,
|
|
2027
|
-
paused: false,
|
|
2028
|
-
static: false,
|
|
2029
|
-
fps: false
|
|
2030
|
-
};
|
|
2031
|
-
|
|
2032
|
-
const root = document.createElement('div');
|
|
2033
|
-
root.className = 'hj-capsule-root hj-progress-root';
|
|
2034
|
-
root.setAttribute('role', 'slider');
|
|
2035
|
-
root.setAttribute('data-draggable', String(effectiveDraggable));
|
|
2036
|
-
root.dataset.direction = merged.direction === 'rtl' ? 'rtl' : 'ltr';
|
|
2037
|
-
if (disabled) {
|
|
2038
|
-
root.setAttribute('aria-disabled', 'true');
|
|
2039
|
-
root.classList.add('is-disabled');
|
|
2040
|
-
}
|
|
2041
|
-
if (readonly) {
|
|
2042
|
-
root.setAttribute('aria-readonly', 'true');
|
|
2043
|
-
root.classList.add('is-readonly');
|
|
2044
|
-
}
|
|
2045
|
-
root.setAttribute('tabindex', locked ? '-1' : (effectiveKeyboard ? '0' : '-1'));
|
|
2046
|
-
root.setAttribute('aria-orientation', 'horizontal');
|
|
2047
|
-
root.setAttribute('aria-valuemin', String(merged.min ?? 0));
|
|
2048
|
-
root.setAttribute('aria-valuemax', String(merged.max ?? 100));
|
|
2049
|
-
root.setAttribute('aria-valuenow', String(preset.initialProgress));
|
|
2050
|
-
const updateAria = () => {
|
|
2051
|
-
root.setAttribute(
|
|
2052
|
-
'aria-label',
|
|
2053
|
-
customLabel || interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
|
|
2054
|
-
);
|
|
2055
|
-
};
|
|
2056
|
-
updateAria();
|
|
2057
|
-
const canvas = document.createElement('canvas');
|
|
2058
|
-
canvas.className = 'hj-progress-canvas';
|
|
2059
|
-
canvas.setAttribute('aria-hidden', 'true');
|
|
2060
|
-
|
|
2061
|
-
// Slot containers are always present but transparent by default; they only
|
|
2062
|
-
// provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
|
|
2063
|
-
const fillContent = (layer, content) => {
|
|
2064
|
-
layer.innerHTML = '';
|
|
2065
|
-
if (content == null) return;
|
|
2066
|
-
if (typeof content === 'string') {
|
|
2067
|
-
layer.innerHTML = content;
|
|
2068
|
-
return;
|
|
2069
|
-
}
|
|
2070
|
-
const nodes = Array.isArray(content) ? content : [content];
|
|
2071
|
-
for (const node of nodes) {
|
|
2072
|
-
if (node && typeof node.nodeType === 'number') layer.appendChild(node);
|
|
2073
|
-
}
|
|
2074
|
-
};
|
|
2075
|
-
|
|
2076
|
-
const textLayer = document.createElement('div');
|
|
2077
|
-
textLayer.className = 'hj-progress-text hj-progress-copy';
|
|
2078
|
-
|
|
2079
|
-
const visualLayer = document.createElement('div');
|
|
2080
|
-
visualLayer.className = 'hj-progress-visual';
|
|
2081
|
-
|
|
2082
|
-
fillContent(textLayer, options.text);
|
|
2083
|
-
fillContent(visualLayer, options.colorContent);
|
|
2084
|
-
|
|
2085
|
-
const valueElement = document.createElement('div');
|
|
2086
|
-
valueElement.className = 'hj-progress-value';
|
|
2087
|
-
valueElement.setAttribute('aria-hidden', 'true');
|
|
2088
|
-
valueElement.hidden = merged.showValue === false;
|
|
2089
|
-
|
|
2090
|
-
root.appendChild(canvas);
|
|
2091
|
-
root.appendChild(textLayer);
|
|
2092
|
-
root.appendChild(visualLayer);
|
|
2093
|
-
root.appendChild(valueElement);
|
|
2094
|
-
container.appendChild(root);
|
|
2095
|
-
|
|
2096
|
-
const emitter = createEmitter();
|
|
2097
|
-
let manuallyPaused = merged.paused === true;
|
|
2098
|
-
let reducedPaused = false;
|
|
2099
|
-
let staticMode = merged.static === true;
|
|
2100
|
-
let contextLost = false;
|
|
2101
|
-
let disposed = false;
|
|
2102
|
-
let renderOnce = () => {};
|
|
2103
|
-
const onContextLost = () => {
|
|
2104
|
-
contextLost = true;
|
|
2105
|
-
emitter.emit('contextlost', {});
|
|
2106
|
-
};
|
|
2107
|
-
const onContextRestored = () => {
|
|
2108
|
-
contextLost = false;
|
|
2109
|
-
emitter.emit('contextrestored', {});
|
|
2110
|
-
renderOnce();
|
|
2111
|
-
wakeScheduler();
|
|
2112
|
-
};
|
|
2113
|
-
|
|
2114
|
-
const applySize = () => {
|
|
2115
|
-
root.style.width = parseSize(merged.width);
|
|
2116
|
-
root.style.height = parseSize(merged.height);
|
|
2117
|
-
const vars = merged.cssVars || {};
|
|
2118
|
-
for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
|
|
2119
|
-
if (merged.textRatio !== undefined) {
|
|
2120
|
-
root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
|
|
2121
|
-
}
|
|
2122
|
-
};
|
|
2123
|
-
applySize();
|
|
2124
|
-
|
|
2125
|
-
const controller = new ProgressCapsuleController({
|
|
2126
|
-
root,
|
|
2127
|
-
canvas,
|
|
2128
|
-
valueElement,
|
|
2129
|
-
preset,
|
|
2130
|
-
emitter,
|
|
2131
|
-
options: {
|
|
2132
|
-
...merged,
|
|
2133
|
-
draggable: effectiveDraggable,
|
|
2134
|
-
keyboard: effectiveKeyboard,
|
|
2135
|
-
onResize: () => {
|
|
2136
|
-
if (manuallyPaused || reducedPaused || staticMode) renderOnce();
|
|
2137
|
-
}
|
|
2138
|
-
},
|
|
2139
|
-
copy,
|
|
2140
|
-
dirty
|
|
2141
|
-
});
|
|
2142
|
-
|
|
2143
|
-
let overlay = null;
|
|
2144
|
-
if (merged.renderer !== 'canvas2d') {
|
|
2145
|
-
overlay = attachProgressFlowOverlay({
|
|
2146
|
-
root,
|
|
2147
|
-
canvas,
|
|
2148
|
-
preset,
|
|
2149
|
-
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
2150
|
-
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
2151
|
-
powerPreference: merged.powerPreference,
|
|
2152
|
-
onContextLost,
|
|
2153
|
-
onContextRestored
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
if (overlay) controller.webglActive = true;
|
|
2157
|
-
else if (merged.renderer !== 'canvas2d') {
|
|
2158
|
-
nextTick(() => {
|
|
2159
|
-
if (disposed) return;
|
|
2160
|
-
emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
const visibility = createVisibilityGuard(root, wakeScheduler);
|
|
2165
|
-
const motionPreference = createReducedMotionPreference(
|
|
2166
|
-
merged.respectReducedMotion,
|
|
2167
|
-
(matches) => {
|
|
2168
|
-
reducedPaused = matches;
|
|
2169
|
-
if (matches) renderOnce();
|
|
2170
|
-
else wakeScheduler();
|
|
2171
|
-
}
|
|
2172
|
-
);
|
|
2173
|
-
reducedPaused = motionPreference.matches();
|
|
2174
|
-
const isMotionPaused = () => manuallyPaused || reducedPaused || staticMode || contextLost;
|
|
2175
|
-
let flowTime = 0;
|
|
2176
|
-
const frameGate = createFrameGate(merged.fps);
|
|
2177
|
-
renderOnce = () => {
|
|
2178
|
-
controller.draw();
|
|
2179
|
-
if (overlay) overlay.update(flowTime);
|
|
2180
|
-
};
|
|
2181
|
-
renderOnce();
|
|
2182
|
-
const offPausedChange = emitter.on('change', () => {
|
|
2183
|
-
if (isMotionPaused()) renderOnce();
|
|
2184
|
-
});
|
|
2185
|
-
const unsubscribe = subscribeScheduler(
|
|
2186
|
-
(delta, now) => {
|
|
2187
|
-
flowTime += delta;
|
|
2188
|
-
controller.update(delta, false);
|
|
2189
|
-
if (frameGate.shouldDraw(delta)) {
|
|
2190
|
-
controller.draw();
|
|
2191
|
-
if (overlay) overlay.update(flowTime);
|
|
2192
|
-
}
|
|
2193
|
-
},
|
|
2194
|
-
() => isMotionPaused() || !visibility.isVisible()
|
|
2195
|
-
);
|
|
2196
|
-
|
|
2197
|
-
nextTick(() => {
|
|
2198
|
-
if (disposed) return;
|
|
2199
|
-
emitter.emit('ready', { preset: { ...preset } });
|
|
2200
|
-
});
|
|
2201
|
-
|
|
2202
|
-
const syncDom = () => {
|
|
2203
|
-
updateAria();
|
|
2204
|
-
};
|
|
2205
|
-
|
|
2206
|
-
return {
|
|
2207
|
-
element: root,
|
|
2208
|
-
canvas,
|
|
2209
|
-
preset,
|
|
2210
|
-
on: emitter.on,
|
|
2211
|
-
off: emitter.off,
|
|
2212
|
-
setValue(value, source = 'prop') {
|
|
2213
|
-
controller.setProgress(value, source);
|
|
2214
|
-
return this;
|
|
2215
|
-
},
|
|
2216
|
-
getValue() {
|
|
2217
|
-
return controller.value;
|
|
2218
|
-
},
|
|
2219
|
-
setRange(min, max) {
|
|
2220
|
-
controller.setRange(min, max);
|
|
2221
|
-
if (isMotionPaused()) renderOnce();
|
|
2222
|
-
return this;
|
|
2223
|
-
},
|
|
2224
|
-
setPreset(ref) {
|
|
2225
|
-
const next = getPreset('progress', ref);
|
|
2226
|
-
dirty.preset = true;
|
|
2227
|
-
Object.assign(preset, next);
|
|
2228
|
-
if (colorOverride) preset.colors = [...colorOverride];
|
|
2229
|
-
if (edgeStyleOverride) preset.edgeStyle = edgeStyleOverride;
|
|
2230
|
-
const nextColors = preset.colors.map(normalizeColor);
|
|
2231
|
-
if (nextColors.every(Boolean)) preset.colors = nextColors;
|
|
2232
|
-
controller.preset = preset;
|
|
2233
|
-
controller.profile = preset.edgeStyle === 'tide'
|
|
2234
|
-
? FLOW_PROFILES.tide
|
|
2235
|
-
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
2236
|
-
controller.flowTime = stringSeed(next.id) * 31;
|
|
2237
|
-
controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
|
|
2238
|
-
syncDom();
|
|
2239
|
-
if (overlay) {
|
|
2240
|
-
overlay.dispose();
|
|
2241
|
-
contextLost = false;
|
|
2242
|
-
overlay = attachProgressFlowOverlay({
|
|
2243
|
-
root,
|
|
2244
|
-
canvas,
|
|
2245
|
-
preset,
|
|
2246
|
-
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
2247
|
-
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
2248
|
-
powerPreference: merged.powerPreference,
|
|
2249
|
-
onContextLost,
|
|
2250
|
-
onContextRestored
|
|
2251
|
-
});
|
|
2252
|
-
}
|
|
2253
|
-
controller.webglActive = Boolean(overlay);
|
|
2254
|
-
controller.resizeCanvas();
|
|
2255
|
-
if (isMotionPaused()) renderOnce();
|
|
2256
|
-
emitter.emit('presetchange', { preset: { ...preset } });
|
|
2257
|
-
return this;
|
|
2258
|
-
},
|
|
2259
|
-
setEdgeStyle(edgeStyle) {
|
|
2260
|
-
const value = String(edgeStyle || '').toLowerCase();
|
|
2261
|
-
if (value !== 'flow' && value !== 'tide') return this;
|
|
2262
|
-
dirty.edgeStyle = true;
|
|
2263
|
-
edgeStyleOverride = value;
|
|
2264
|
-
preset.edgeStyle = value;
|
|
2265
|
-
controller.profile = value === 'tide'
|
|
2266
|
-
? FLOW_PROFILES.tide
|
|
2267
|
-
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
2268
|
-
if (overlay) {
|
|
2269
|
-
overlay.dispose();
|
|
2270
|
-
contextLost = false;
|
|
2271
|
-
overlay = attachProgressFlowOverlay({
|
|
2272
|
-
root,
|
|
2273
|
-
canvas,
|
|
2274
|
-
preset,
|
|
2275
|
-
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
2276
|
-
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
2277
|
-
powerPreference: merged.powerPreference,
|
|
2278
|
-
onContextLost,
|
|
2279
|
-
onContextRestored
|
|
2280
|
-
});
|
|
2281
|
-
}
|
|
2282
|
-
controller.webglActive = Boolean(overlay);
|
|
2283
|
-
controller.resizeCanvas();
|
|
2284
|
-
if (isMotionPaused()) renderOnce();
|
|
2285
|
-
return this;
|
|
2286
|
-
},
|
|
2287
|
-
setText(content) {
|
|
2288
|
-
fillContent(textLayer, content);
|
|
2289
|
-
return this;
|
|
2290
|
-
},
|
|
2291
|
-
setColorContent(content) {
|
|
2292
|
-
fillContent(visualLayer, content);
|
|
2293
|
-
return this;
|
|
2294
|
-
},
|
|
2295
|
-
setTextRatio(ratio) {
|
|
2296
|
-
const value = Number(ratio);
|
|
2297
|
-
if (!Number.isFinite(value) || value < 0 || value > 100) return this;
|
|
2298
|
-
dirty.textRatio = true;
|
|
2299
|
-
merged.textRatio = value;
|
|
2300
|
-
root.style.setProperty('--hj-text-width', `${value}%`);
|
|
2301
|
-
return this;
|
|
2302
|
-
},
|
|
2303
|
-
setCssVars(vars) {
|
|
2304
|
-
if (!vars || typeof vars !== 'object') return this;
|
|
2305
|
-
dirty.cssVars = true;
|
|
2306
|
-
merged.cssVars = { ...(merged.cssVars || {}), ...vars };
|
|
2307
|
-
for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
|
|
2308
|
-
return this;
|
|
2309
|
-
},
|
|
2310
|
-
setLabel(label) {
|
|
2311
|
-
customLabel = typeof label === 'string' && label ? label : null;
|
|
2312
|
-
updateAria();
|
|
2313
|
-
return this;
|
|
2314
|
-
},
|
|
2315
|
-
setValueSuffix(suffix) {
|
|
2316
|
-
controller.setValueSuffix(suffix);
|
|
2317
|
-
return this;
|
|
2318
|
-
},
|
|
2319
|
-
setStep(step) {
|
|
2320
|
-
controller.setStep(step);
|
|
2321
|
-
return this;
|
|
2322
|
-
},
|
|
2323
|
-
setPrecision(precision) {
|
|
2324
|
-
controller.setPrecision(precision);
|
|
2325
|
-
return this;
|
|
2326
|
-
},
|
|
2327
|
-
setFormatValue(formatter) {
|
|
2328
|
-
controller.setFormatValue(formatter);
|
|
2329
|
-
return this;
|
|
2330
|
-
},
|
|
2331
|
-
setDirection(direction) {
|
|
2332
|
-
controller.setDirection(direction);
|
|
2333
|
-
return this;
|
|
2334
|
-
},
|
|
2335
|
-
setShowValue(show) {
|
|
2336
|
-
controller.setShowValue(show);
|
|
2337
|
-
return this;
|
|
2338
|
-
},
|
|
2339
|
-
setColors(colors) {
|
|
2340
|
-
const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
|
|
2341
|
-
if (next.length !== 4 || next.some((color) => !color)) return this;
|
|
2342
|
-
dirty.colors = true;
|
|
2343
|
-
colorOverride = [...next];
|
|
2344
|
-
preset.colors = [...next];
|
|
2345
|
-
controller.setColors(next);
|
|
2346
|
-
if (overlay) overlay.setColors(next);
|
|
2347
|
-
controller.resizeCanvas();
|
|
2348
|
-
if (isMotionPaused()) renderOnce();
|
|
2349
|
-
return this;
|
|
2350
|
-
},
|
|
2351
|
-
setSize(width, height) {
|
|
2352
|
-
if (width !== undefined) {
|
|
2353
|
-
parseSize(width);
|
|
2354
|
-
merged.width = width;
|
|
2355
|
-
}
|
|
2356
|
-
if (height !== undefined) {
|
|
2357
|
-
parseSize(height);
|
|
2358
|
-
merged.height = height;
|
|
2359
|
-
}
|
|
2360
|
-
applySize();
|
|
2361
|
-
controller.resizeCanvas();
|
|
2362
|
-
if (isMotionPaused()) renderOnce();
|
|
2363
|
-
return this;
|
|
2364
|
-
},
|
|
2365
|
-
randomize() {
|
|
2366
|
-
flowTime = controller.randomize();
|
|
2367
|
-
if (isMotionPaused()) renderOnce();
|
|
2368
|
-
return this;
|
|
2369
|
-
},
|
|
2370
|
-
pause() {
|
|
2371
|
-
dirty.paused = true;
|
|
2372
|
-
manuallyPaused = true;
|
|
2373
|
-
renderOnce();
|
|
2374
|
-
return this;
|
|
2375
|
-
},
|
|
2376
|
-
resume() {
|
|
2377
|
-
dirty.paused = true;
|
|
2378
|
-
manuallyPaused = false;
|
|
2379
|
-
wakeScheduler();
|
|
2380
|
-
return this;
|
|
2381
|
-
},
|
|
2382
|
-
setPaused(value) {
|
|
2383
|
-
return value ? this.pause() : this.resume();
|
|
2384
|
-
},
|
|
2385
|
-
setStatic(value) {
|
|
2386
|
-
dirty.static = true;
|
|
2387
|
-
staticMode = value === true;
|
|
2388
|
-
merged.static = staticMode;
|
|
2389
|
-
if (staticMode) renderOnce();
|
|
2390
|
-
else wakeScheduler();
|
|
2391
|
-
return this;
|
|
2392
|
-
},
|
|
2393
|
-
setFps(fps) {
|
|
2394
|
-
dirty.fps = true;
|
|
2395
|
-
merged.fps = frameGate.setFps(fps);
|
|
2396
|
-
wakeScheduler();
|
|
2397
|
-
return this;
|
|
2398
|
-
},
|
|
2399
|
-
setQuality(quality) {
|
|
2400
|
-
dirty.quality = true;
|
|
2401
|
-
merged.quality = quality;
|
|
2402
|
-
controller.dprCap = effectiveDprCap(quality, merged.renderScale);
|
|
2403
|
-
controller.resizeCanvas();
|
|
2404
|
-
if (overlay) overlay.setDprCap(controller.dprCap);
|
|
2405
|
-
if (isMotionPaused()) renderOnce();
|
|
2406
|
-
return this;
|
|
2407
|
-
},
|
|
2408
|
-
setRenderScale(renderScale) {
|
|
2409
|
-
const value = Number(renderScale);
|
|
2410
|
-
if (!Number.isFinite(value)) return this;
|
|
2411
|
-
dirty.renderScale = true;
|
|
2412
|
-
merged.renderScale = Math.min(1, Math.max(0.25, value));
|
|
2413
|
-
controller.dprCap = effectiveDprCap(merged.quality, merged.renderScale);
|
|
2414
|
-
controller.resizeCanvas();
|
|
2415
|
-
if (overlay) overlay.setDprCap(controller.dprCap);
|
|
2416
|
-
if (isMotionPaused()) renderOnce();
|
|
2417
|
-
return this;
|
|
2418
|
-
},
|
|
2419
|
-
dispose() {
|
|
2420
|
-
disposed = true;
|
|
2421
|
-
unsubscribe();
|
|
2422
|
-
offPausedChange();
|
|
2423
|
-
visibility.dispose();
|
|
2424
|
-
motionPreference.dispose();
|
|
2425
|
-
if (overlay) overlay.dispose();
|
|
2426
|
-
controller.dispose();
|
|
2427
|
-
root.remove();
|
|
2428
|
-
},
|
|
2429
|
-
get textRatio() { return merged.textRatio; },
|
|
2430
|
-
get cssVars() { return merged.cssVars; },
|
|
2431
|
-
get min() { return controller.min; },
|
|
2432
|
-
get max() { return controller.max; },
|
|
2433
|
-
get step() { return controller.step; },
|
|
2434
|
-
get precision() { return controller.precision; },
|
|
2435
|
-
get formatValue() { return controller.formatValue; },
|
|
2436
|
-
get direction() { return controller.direction; },
|
|
2437
|
-
get showValue() { return !valueElement.hidden; },
|
|
2438
|
-
get valueSuffix() { return controller.valueSuffix; },
|
|
2439
|
-
get quality() { return merged.quality; },
|
|
2440
|
-
get renderScale() { return merged.renderScale; },
|
|
2441
|
-
get paused() { return manuallyPaused; },
|
|
2442
|
-
get static() { return staticMode; },
|
|
2443
|
-
get fps() { return frameGate.getFps(); },
|
|
2444
|
-
dirty
|
|
2445
|
-
};
|
|
2073
|
+
/**
|
|
2074
|
+
* Mount a fluid progress capsule into `container`.
|
|
2075
|
+
*
|
|
2076
|
+
* Options: preset (literary name), width, height, value/modelValue, min, max,
|
|
2077
|
+
* step, draggable, keyboard, direction, precision/formatValue, colors,
|
|
2078
|
+
* edgeStyle, textRatio (0-100, text region
|
|
2079
|
+
* width in percent), text (HTML string or DOM nodes for the text slot),
|
|
2080
|
+
* colorContent (HTML string or DOM nodes for the color slot),
|
|
2081
|
+
* showValue (show/hide the right-side percentage), quality, renderScale,
|
|
2082
|
+
* powerPreference, fps, paused/static, respectReducedMotion, cssVars.
|
|
2083
|
+
*/
|
|
2084
|
+
function createProgressCapsule(container, options = {}) {
|
|
2085
|
+
if (!container || typeof container.appendChild !== 'function') {
|
|
2086
|
+
throw new Error('createProgressCapsule: container element is required');
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const preset = { ...getPreset('progress', options.preset ?? '星火') };
|
|
2090
|
+
const merged = normalizeOptions(DEFAULTS.progress, preset, options);
|
|
2091
|
+
[merged.min, merged.max] = normalizeRange(merged.min, merged.max);
|
|
2092
|
+
if (merged.modelValue !== undefined) merged.value = merged.modelValue;
|
|
2093
|
+
const normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
|
|
2094
|
+
if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
|
|
2095
|
+
const initialEdgeStyle = merged.edgeStyle === 'tide' || merged.edgeStyle === 'flow'
|
|
2096
|
+
? merged.edgeStyle
|
|
2097
|
+
: preset.edgeStyle;
|
|
2098
|
+
preset.edgeStyle = initialEdgeStyle;
|
|
2099
|
+
merged.edgeStyle = initialEdgeStyle;
|
|
2100
|
+
merged.colors = [...preset.colors];
|
|
2101
|
+
const optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
|
|
2102
|
+
let colorOverride = optionColors.length === 4 && optionColors.every(Boolean)
|
|
2103
|
+
? [...optionColors]
|
|
2104
|
+
: null;
|
|
2105
|
+
let edgeStyleOverride = options.edgeStyle === 'flow' || options.edgeStyle === 'tide'
|
|
2106
|
+
? merged.edgeStyle
|
|
2107
|
+
: null;
|
|
2108
|
+
const copy = COPY;
|
|
2109
|
+
let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
|
|
2110
|
+
// Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
|
|
2111
|
+
// 这里统一按 true 处理。
|
|
2112
|
+
const disabled = merged.disabled === true || merged.disabled === '' || merged.disabled === 'true';
|
|
2113
|
+
const readonly = merged.readonly === true || merged.readonly === '' || merged.readonly === 'true';
|
|
2114
|
+
const locked = disabled || readonly;
|
|
2115
|
+
const effectiveDraggable = locked ? false : merged.draggable !== false;
|
|
2116
|
+
const effectiveKeyboard = locked ? false : merged.keyboard !== false;
|
|
2117
|
+
const dirty = {
|
|
2118
|
+
preset: false,
|
|
2119
|
+
colors: false,
|
|
2120
|
+
textRatio: false,
|
|
2121
|
+
cssVars: false,
|
|
2122
|
+
edgeStyle: false,
|
|
2123
|
+
value: false,
|
|
2124
|
+
step: false,
|
|
2125
|
+
precision: false,
|
|
2126
|
+
formatValue: false,
|
|
2127
|
+
direction: false,
|
|
2128
|
+
showValue: false,
|
|
2129
|
+
valueSuffix: false,
|
|
2130
|
+
quality: false,
|
|
2131
|
+
renderScale: false,
|
|
2132
|
+
paused: false,
|
|
2133
|
+
static: false,
|
|
2134
|
+
fps: false
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
const root = document.createElement('div');
|
|
2138
|
+
root.className = 'hj-capsule-root hj-progress-root';
|
|
2139
|
+
root.setAttribute('role', 'slider');
|
|
2140
|
+
root.setAttribute('data-draggable', String(effectiveDraggable));
|
|
2141
|
+
root.dataset.direction = merged.direction === 'rtl' ? 'rtl' : 'ltr';
|
|
2142
|
+
if (disabled) {
|
|
2143
|
+
root.setAttribute('aria-disabled', 'true');
|
|
2144
|
+
root.classList.add('is-disabled');
|
|
2145
|
+
}
|
|
2146
|
+
if (readonly) {
|
|
2147
|
+
root.setAttribute('aria-readonly', 'true');
|
|
2148
|
+
root.classList.add('is-readonly');
|
|
2149
|
+
}
|
|
2150
|
+
root.setAttribute('tabindex', locked ? '-1' : (effectiveKeyboard ? '0' : '-1'));
|
|
2151
|
+
root.setAttribute('aria-orientation', 'horizontal');
|
|
2152
|
+
root.setAttribute('aria-valuemin', String(merged.min ?? 0));
|
|
2153
|
+
root.setAttribute('aria-valuemax', String(merged.max ?? 100));
|
|
2154
|
+
root.setAttribute('aria-valuenow', String(preset.initialProgress));
|
|
2155
|
+
const updateAria = () => {
|
|
2156
|
+
root.setAttribute(
|
|
2157
|
+
'aria-label',
|
|
2158
|
+
customLabel || interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
|
|
2159
|
+
);
|
|
2160
|
+
};
|
|
2161
|
+
updateAria();
|
|
2162
|
+
const canvas = document.createElement('canvas');
|
|
2163
|
+
canvas.className = 'hj-progress-canvas';
|
|
2164
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
2165
|
+
|
|
2166
|
+
// Slot containers are always present but transparent by default; they only
|
|
2167
|
+
// provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
|
|
2168
|
+
const fillContent = (layer, content) => {
|
|
2169
|
+
layer.innerHTML = '';
|
|
2170
|
+
if (content == null) return;
|
|
2171
|
+
if (typeof content === 'string') {
|
|
2172
|
+
layer.innerHTML = content;
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const nodes = Array.isArray(content) ? content : [content];
|
|
2176
|
+
for (const node of nodes) {
|
|
2177
|
+
if (node && typeof node.nodeType === 'number') layer.appendChild(node);
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
|
|
2181
|
+
const textLayer = document.createElement('div');
|
|
2182
|
+
textLayer.className = 'hj-progress-text hj-progress-copy';
|
|
2183
|
+
|
|
2184
|
+
const visualLayer = document.createElement('div');
|
|
2185
|
+
visualLayer.className = 'hj-progress-visual';
|
|
2186
|
+
|
|
2187
|
+
fillContent(textLayer, options.text);
|
|
2188
|
+
fillContent(visualLayer, options.colorContent);
|
|
2189
|
+
|
|
2190
|
+
const valueElement = document.createElement('div');
|
|
2191
|
+
valueElement.className = 'hj-progress-value';
|
|
2192
|
+
valueElement.setAttribute('aria-hidden', 'true');
|
|
2193
|
+
valueElement.hidden = merged.showValue === false;
|
|
2194
|
+
|
|
2195
|
+
root.appendChild(canvas);
|
|
2196
|
+
root.appendChild(textLayer);
|
|
2197
|
+
root.appendChild(visualLayer);
|
|
2198
|
+
root.appendChild(valueElement);
|
|
2199
|
+
container.appendChild(root);
|
|
2200
|
+
|
|
2201
|
+
const emitter = createEmitter();
|
|
2202
|
+
let manuallyPaused = merged.paused === true;
|
|
2203
|
+
let reducedPaused = false;
|
|
2204
|
+
let staticMode = merged.static === true;
|
|
2205
|
+
let contextLost = false;
|
|
2206
|
+
let disposed = false;
|
|
2207
|
+
let renderOnce = () => {};
|
|
2208
|
+
const onContextLost = () => {
|
|
2209
|
+
contextLost = true;
|
|
2210
|
+
emitter.emit('contextlost', {});
|
|
2211
|
+
};
|
|
2212
|
+
const onContextRestored = () => {
|
|
2213
|
+
contextLost = false;
|
|
2214
|
+
emitter.emit('contextrestored', {});
|
|
2215
|
+
renderOnce();
|
|
2216
|
+
wakeScheduler();
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
const applySize = () => {
|
|
2220
|
+
root.style.width = parseSize(merged.width);
|
|
2221
|
+
root.style.height = parseSize(merged.height);
|
|
2222
|
+
const vars = merged.cssVars || {};
|
|
2223
|
+
for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
|
|
2224
|
+
if (merged.textRatio !== undefined) {
|
|
2225
|
+
root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
applySize();
|
|
2229
|
+
|
|
2230
|
+
const controller = new ProgressCapsuleController({
|
|
2231
|
+
root,
|
|
2232
|
+
canvas,
|
|
2233
|
+
valueElement,
|
|
2234
|
+
preset,
|
|
2235
|
+
emitter,
|
|
2236
|
+
options: {
|
|
2237
|
+
...merged,
|
|
2238
|
+
draggable: effectiveDraggable,
|
|
2239
|
+
keyboard: effectiveKeyboard,
|
|
2240
|
+
onResize: () => {
|
|
2241
|
+
if (manuallyPaused || reducedPaused || staticMode) renderOnce();
|
|
2242
|
+
}
|
|
2243
|
+
},
|
|
2244
|
+
copy,
|
|
2245
|
+
dirty
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
let overlay = null;
|
|
2249
|
+
if (merged.renderer !== 'canvas2d') {
|
|
2250
|
+
overlay = attachProgressFlowOverlay({
|
|
2251
|
+
root,
|
|
2252
|
+
canvas,
|
|
2253
|
+
preset,
|
|
2254
|
+
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
2255
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
2256
|
+
powerPreference: merged.powerPreference,
|
|
2257
|
+
onContextLost,
|
|
2258
|
+
onContextRestored
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
if (overlay) controller.webglActive = true;
|
|
2262
|
+
else if (merged.renderer !== 'canvas2d') {
|
|
2263
|
+
nextTick(() => {
|
|
2264
|
+
if (disposed) return;
|
|
2265
|
+
emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
const visibility = createVisibilityGuard(root, wakeScheduler);
|
|
2270
|
+
const motionPreference = createReducedMotionPreference(
|
|
2271
|
+
merged.respectReducedMotion,
|
|
2272
|
+
(matches) => {
|
|
2273
|
+
reducedPaused = matches;
|
|
2274
|
+
if (matches) renderOnce();
|
|
2275
|
+
else wakeScheduler();
|
|
2276
|
+
}
|
|
2277
|
+
);
|
|
2278
|
+
reducedPaused = motionPreference.matches();
|
|
2279
|
+
const isMotionPaused = () => manuallyPaused || reducedPaused || staticMode || contextLost;
|
|
2280
|
+
let flowTime = 0;
|
|
2281
|
+
const frameGate = createFrameGate(merged.fps);
|
|
2282
|
+
renderOnce = () => {
|
|
2283
|
+
controller.draw();
|
|
2284
|
+
if (overlay) overlay.update(flowTime);
|
|
2285
|
+
};
|
|
2286
|
+
renderOnce();
|
|
2287
|
+
const offPausedChange = emitter.on('change', () => {
|
|
2288
|
+
if (isMotionPaused()) renderOnce();
|
|
2289
|
+
});
|
|
2290
|
+
const unsubscribe = subscribeScheduler(
|
|
2291
|
+
(delta, now) => {
|
|
2292
|
+
flowTime += delta;
|
|
2293
|
+
controller.update(delta, false);
|
|
2294
|
+
if (frameGate.shouldDraw(delta)) {
|
|
2295
|
+
controller.draw();
|
|
2296
|
+
if (overlay) overlay.update(flowTime);
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
() => isMotionPaused() || !visibility.isVisible()
|
|
2300
|
+
);
|
|
2301
|
+
|
|
2302
|
+
nextTick(() => {
|
|
2303
|
+
if (disposed) return;
|
|
2304
|
+
emitter.emit('ready', { preset: { ...preset } });
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
const syncDom = () => {
|
|
2308
|
+
updateAria();
|
|
2309
|
+
};
|
|
2310
|
+
|
|
2311
|
+
return {
|
|
2312
|
+
element: root,
|
|
2313
|
+
canvas,
|
|
2314
|
+
preset,
|
|
2315
|
+
on: emitter.on,
|
|
2316
|
+
off: emitter.off,
|
|
2317
|
+
setValue(value, source = 'prop') {
|
|
2318
|
+
controller.setProgress(value, source);
|
|
2319
|
+
return this;
|
|
2320
|
+
},
|
|
2321
|
+
getValue() {
|
|
2322
|
+
return controller.value;
|
|
2323
|
+
},
|
|
2324
|
+
setRange(min, max) {
|
|
2325
|
+
controller.setRange(min, max);
|
|
2326
|
+
if (isMotionPaused()) renderOnce();
|
|
2327
|
+
return this;
|
|
2328
|
+
},
|
|
2329
|
+
setPreset(ref) {
|
|
2330
|
+
const next = getPreset('progress', ref);
|
|
2331
|
+
dirty.preset = true;
|
|
2332
|
+
Object.assign(preset, next);
|
|
2333
|
+
if (colorOverride) preset.colors = [...colorOverride];
|
|
2334
|
+
if (edgeStyleOverride) preset.edgeStyle = edgeStyleOverride;
|
|
2335
|
+
const nextColors = preset.colors.map(normalizeColor);
|
|
2336
|
+
if (nextColors.every(Boolean)) preset.colors = nextColors;
|
|
2337
|
+
controller.preset = preset;
|
|
2338
|
+
controller.profile = preset.edgeStyle === 'tide'
|
|
2339
|
+
? FLOW_PROFILES.tide
|
|
2340
|
+
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
2341
|
+
controller.flowTime = stringSeed(next.id) * 31;
|
|
2342
|
+
controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
|
|
2343
|
+
syncDom();
|
|
2344
|
+
if (overlay) {
|
|
2345
|
+
overlay.dispose();
|
|
2346
|
+
contextLost = false;
|
|
2347
|
+
overlay = attachProgressFlowOverlay({
|
|
2348
|
+
root,
|
|
2349
|
+
canvas,
|
|
2350
|
+
preset,
|
|
2351
|
+
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
2352
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
2353
|
+
powerPreference: merged.powerPreference,
|
|
2354
|
+
onContextLost,
|
|
2355
|
+
onContextRestored
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
controller.webglActive = Boolean(overlay);
|
|
2359
|
+
controller.resizeCanvas();
|
|
2360
|
+
if (isMotionPaused()) renderOnce();
|
|
2361
|
+
emitter.emit('presetchange', { preset: { ...preset } });
|
|
2362
|
+
return this;
|
|
2363
|
+
},
|
|
2364
|
+
setEdgeStyle(edgeStyle) {
|
|
2365
|
+
const value = String(edgeStyle || '').toLowerCase();
|
|
2366
|
+
if (value !== 'flow' && value !== 'tide') return this;
|
|
2367
|
+
dirty.edgeStyle = true;
|
|
2368
|
+
edgeStyleOverride = value;
|
|
2369
|
+
preset.edgeStyle = value;
|
|
2370
|
+
controller.profile = value === 'tide'
|
|
2371
|
+
? FLOW_PROFILES.tide
|
|
2372
|
+
: (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
|
|
2373
|
+
if (overlay) {
|
|
2374
|
+
overlay.dispose();
|
|
2375
|
+
contextLost = false;
|
|
2376
|
+
overlay = attachProgressFlowOverlay({
|
|
2377
|
+
root,
|
|
2378
|
+
canvas,
|
|
2379
|
+
preset,
|
|
2380
|
+
getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
|
|
2381
|
+
dprCap: effectiveDprCap(merged.quality, merged.renderScale),
|
|
2382
|
+
powerPreference: merged.powerPreference,
|
|
2383
|
+
onContextLost,
|
|
2384
|
+
onContextRestored
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
controller.webglActive = Boolean(overlay);
|
|
2388
|
+
controller.resizeCanvas();
|
|
2389
|
+
if (isMotionPaused()) renderOnce();
|
|
2390
|
+
return this;
|
|
2391
|
+
},
|
|
2392
|
+
setText(content) {
|
|
2393
|
+
fillContent(textLayer, content);
|
|
2394
|
+
return this;
|
|
2395
|
+
},
|
|
2396
|
+
setColorContent(content) {
|
|
2397
|
+
fillContent(visualLayer, content);
|
|
2398
|
+
return this;
|
|
2399
|
+
},
|
|
2400
|
+
setTextRatio(ratio) {
|
|
2401
|
+
const value = Number(ratio);
|
|
2402
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) return this;
|
|
2403
|
+
dirty.textRatio = true;
|
|
2404
|
+
merged.textRatio = value;
|
|
2405
|
+
root.style.setProperty('--hj-text-width', `${value}%`);
|
|
2406
|
+
return this;
|
|
2407
|
+
},
|
|
2408
|
+
setCssVars(vars) {
|
|
2409
|
+
if (!vars || typeof vars !== 'object') return this;
|
|
2410
|
+
dirty.cssVars = true;
|
|
2411
|
+
merged.cssVars = { ...(merged.cssVars || {}), ...vars };
|
|
2412
|
+
for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
|
|
2413
|
+
return this;
|
|
2414
|
+
},
|
|
2415
|
+
setLabel(label) {
|
|
2416
|
+
customLabel = typeof label === 'string' && label ? label : null;
|
|
2417
|
+
updateAria();
|
|
2418
|
+
return this;
|
|
2419
|
+
},
|
|
2420
|
+
setValueSuffix(suffix) {
|
|
2421
|
+
controller.setValueSuffix(suffix);
|
|
2422
|
+
return this;
|
|
2423
|
+
},
|
|
2424
|
+
setStep(step) {
|
|
2425
|
+
controller.setStep(step);
|
|
2426
|
+
return this;
|
|
2427
|
+
},
|
|
2428
|
+
setPrecision(precision) {
|
|
2429
|
+
controller.setPrecision(precision);
|
|
2430
|
+
return this;
|
|
2431
|
+
},
|
|
2432
|
+
setFormatValue(formatter) {
|
|
2433
|
+
controller.setFormatValue(formatter);
|
|
2434
|
+
return this;
|
|
2435
|
+
},
|
|
2436
|
+
setDirection(direction) {
|
|
2437
|
+
controller.setDirection(direction);
|
|
2438
|
+
return this;
|
|
2439
|
+
},
|
|
2440
|
+
setShowValue(show) {
|
|
2441
|
+
controller.setShowValue(show);
|
|
2442
|
+
return this;
|
|
2443
|
+
},
|
|
2444
|
+
setColors(colors) {
|
|
2445
|
+
const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
|
|
2446
|
+
if (next.length !== 4 || next.some((color) => !color)) return this;
|
|
2447
|
+
dirty.colors = true;
|
|
2448
|
+
colorOverride = [...next];
|
|
2449
|
+
preset.colors = [...next];
|
|
2450
|
+
controller.setColors(next);
|
|
2451
|
+
if (overlay) overlay.setColors(next);
|
|
2452
|
+
controller.resizeCanvas();
|
|
2453
|
+
if (isMotionPaused()) renderOnce();
|
|
2454
|
+
return this;
|
|
2455
|
+
},
|
|
2456
|
+
setSize(width, height) {
|
|
2457
|
+
if (width !== undefined) {
|
|
2458
|
+
parseSize(width);
|
|
2459
|
+
merged.width = width;
|
|
2460
|
+
}
|
|
2461
|
+
if (height !== undefined) {
|
|
2462
|
+
parseSize(height);
|
|
2463
|
+
merged.height = height;
|
|
2464
|
+
}
|
|
2465
|
+
applySize();
|
|
2466
|
+
controller.resizeCanvas();
|
|
2467
|
+
if (isMotionPaused()) renderOnce();
|
|
2468
|
+
return this;
|
|
2469
|
+
},
|
|
2470
|
+
randomize() {
|
|
2471
|
+
flowTime = controller.randomize();
|
|
2472
|
+
if (isMotionPaused()) renderOnce();
|
|
2473
|
+
return this;
|
|
2474
|
+
},
|
|
2475
|
+
pause() {
|
|
2476
|
+
dirty.paused = true;
|
|
2477
|
+
manuallyPaused = true;
|
|
2478
|
+
renderOnce();
|
|
2479
|
+
return this;
|
|
2480
|
+
},
|
|
2481
|
+
resume() {
|
|
2482
|
+
dirty.paused = true;
|
|
2483
|
+
manuallyPaused = false;
|
|
2484
|
+
wakeScheduler();
|
|
2485
|
+
return this;
|
|
2486
|
+
},
|
|
2487
|
+
setPaused(value) {
|
|
2488
|
+
return value ? this.pause() : this.resume();
|
|
2489
|
+
},
|
|
2490
|
+
setStatic(value) {
|
|
2491
|
+
dirty.static = true;
|
|
2492
|
+
staticMode = value === true;
|
|
2493
|
+
merged.static = staticMode;
|
|
2494
|
+
if (staticMode) renderOnce();
|
|
2495
|
+
else wakeScheduler();
|
|
2496
|
+
return this;
|
|
2497
|
+
},
|
|
2498
|
+
setFps(fps) {
|
|
2499
|
+
dirty.fps = true;
|
|
2500
|
+
merged.fps = frameGate.setFps(fps);
|
|
2501
|
+
wakeScheduler();
|
|
2502
|
+
return this;
|
|
2503
|
+
},
|
|
2504
|
+
setQuality(quality) {
|
|
2505
|
+
dirty.quality = true;
|
|
2506
|
+
merged.quality = quality;
|
|
2507
|
+
controller.dprCap = effectiveDprCap(quality, merged.renderScale);
|
|
2508
|
+
controller.resizeCanvas();
|
|
2509
|
+
if (overlay) overlay.setDprCap(controller.dprCap);
|
|
2510
|
+
if (isMotionPaused()) renderOnce();
|
|
2511
|
+
return this;
|
|
2512
|
+
},
|
|
2513
|
+
setRenderScale(renderScale) {
|
|
2514
|
+
const value = Number(renderScale);
|
|
2515
|
+
if (!Number.isFinite(value)) return this;
|
|
2516
|
+
dirty.renderScale = true;
|
|
2517
|
+
merged.renderScale = Math.min(1, Math.max(0.25, value));
|
|
2518
|
+
controller.dprCap = effectiveDprCap(merged.quality, merged.renderScale);
|
|
2519
|
+
controller.resizeCanvas();
|
|
2520
|
+
if (overlay) overlay.setDprCap(controller.dprCap);
|
|
2521
|
+
if (isMotionPaused()) renderOnce();
|
|
2522
|
+
return this;
|
|
2523
|
+
},
|
|
2524
|
+
dispose() {
|
|
2525
|
+
disposed = true;
|
|
2526
|
+
unsubscribe();
|
|
2527
|
+
offPausedChange();
|
|
2528
|
+
visibility.dispose();
|
|
2529
|
+
motionPreference.dispose();
|
|
2530
|
+
if (overlay) overlay.dispose();
|
|
2531
|
+
controller.dispose();
|
|
2532
|
+
root.remove();
|
|
2533
|
+
},
|
|
2534
|
+
get textRatio() { return merged.textRatio; },
|
|
2535
|
+
get cssVars() { return merged.cssVars; },
|
|
2536
|
+
get min() { return controller.min; },
|
|
2537
|
+
get max() { return controller.max; },
|
|
2538
|
+
get step() { return controller.step; },
|
|
2539
|
+
get precision() { return controller.precision; },
|
|
2540
|
+
get formatValue() { return controller.formatValue; },
|
|
2541
|
+
get direction() { return controller.direction; },
|
|
2542
|
+
get showValue() { return !valueElement.hidden; },
|
|
2543
|
+
get valueSuffix() { return controller.valueSuffix; },
|
|
2544
|
+
get quality() { return merged.quality; },
|
|
2545
|
+
get renderScale() { return merged.renderScale; },
|
|
2546
|
+
get paused() { return manuallyPaused; },
|
|
2547
|
+
get static() { return staticMode; },
|
|
2548
|
+
get fps() { return frameGate.getFps(); },
|
|
2549
|
+
dirty
|
|
2550
|
+
};
|
|
2446
2551
|
}
|
|
2447
2552
|
|
|
2448
2553
|
export { ProgressCapsuleController, createProgressCapsule };
|