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