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