@5even7/dlc-ui 0.2.18 → 0.2.20

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +158 -144
  2. package/EXTENDING.md +114 -0
  3. package/LICENSE +21 -21
  4. package/README.md +389 -338
  5. package/THIRD_PARTY_NOTICES.md +221 -0
  6. package/dist/avatar-creator/styles/humation.mjs +26 -0
  7. package/dist/avatar-creator/styles.mjs +76 -0
  8. package/dist/avatar-creator.cjs +7831 -0
  9. package/dist/avatar-creator.mjs +74 -0
  10. package/dist/capsule.cjs +185 -66
  11. package/dist/capsule.mjs +364 -1340
  12. package/dist/chunks/avatar-creator-DiAmhpRa.mjs +687 -0
  13. package/dist/chunks/color-DnmNceaf.mjs +350 -0
  14. package/dist/chunks/controls-B5_ctvvA.mjs +160 -0
  15. package/dist/chunks/copy-7J0XIj_0.mjs +13 -0
  16. package/dist/chunks/cosmic-fallback-v8G5_ibh.mjs +408 -0
  17. package/dist/chunks/emitter-CxLYMSbw.mjs +38 -0
  18. package/dist/chunks/emo-Dvahy5Qr.mjs +1140 -0
  19. package/dist/chunks/hash-BiO02UNb.mjs +38 -0
  20. package/dist/chunks/humation-B9OPUGL7.mjs +2987 -0
  21. package/dist/chunks/identicon-BUdMd-yf.mjs +3063 -0
  22. package/dist/chunks/index-BchW1tWE.mjs +478 -0
  23. package/dist/chunks/index-BtH8Lbra.mjs +296 -0
  24. package/dist/chunks/index-C02dvGLD.mjs +263 -0
  25. package/dist/chunks/index-CFybYBA1.mjs +278 -0
  26. package/dist/chunks/index-DgI872-7.mjs +241 -0
  27. package/dist/chunks/index-_sf_14Zu.mjs +455 -0
  28. package/dist/chunks/index-w5Pzdwhv.mjs +305 -0
  29. package/dist/chunks/motion-a8_f8_Ui.mjs +177 -0
  30. package/dist/chunks/number-BqWLX0iQ.mjs +231 -0
  31. package/dist/chunks/progress-KweS4Rx0.mjs +1836 -0
  32. package/dist/chunks/size-Bztk2ryJ.mjs +22 -0
  33. package/dist/chunks/wrapper-DXhy1E7Z.mjs +514 -0
  34. package/dist/color.cjs +186 -67
  35. package/dist/color.mjs +341 -1280
  36. package/dist/emo/miao/angry.mjs +4 -0
  37. package/dist/emo/miao/asleep.mjs +4 -0
  38. package/dist/emo/miao/badminton.mjs +4 -0
  39. package/dist/emo/miao/confident.mjs +4 -0
  40. package/dist/emo/miao/cry.mjs +4 -0
  41. package/dist/emo/miao/investigate.mjs +4 -0
  42. package/dist/emo/miao/laugh.mjs +4 -0
  43. package/dist/emo/miao/leisure.mjs +4 -0
  44. package/dist/emo/miao/mock.mjs +4 -0
  45. package/dist/emo/miao/music.mjs +4 -0
  46. package/dist/emo/miao/mute.mjs +4 -0
  47. package/dist/emo/miao/panic.mjs +4 -0
  48. package/dist/emo/miao/ponder.mjs +4 -0
  49. package/dist/emo/miao/question.mjs +4 -0
  50. package/dist/emo/miao/sad.mjs +4 -0
  51. package/dist/emo/miao/shocked.mjs +4 -0
  52. package/dist/emo/miao/shy.mjs +4 -0
  53. package/dist/emo/miao/sigh.mjs +4 -0
  54. package/dist/emo/miao/smile.mjs +4 -0
  55. package/dist/emo/miao/snigger.mjs +4 -0
  56. package/dist/emo/miao/static.mjs +4 -0
  57. package/dist/emo/miao/yawn.mjs +4 -0
  58. package/dist/emo/miao/yummy.mjs +4 -0
  59. package/dist/emo.cjs +27161 -25964
  60. package/dist/emo.mjs +8 -1421
  61. package/dist/index.cjs +32729 -24466
  62. package/dist/index.mjs +16 -4757
  63. package/dist/index.umd.js +33115 -24852
  64. package/dist/index.umd.min.js +3 -1
  65. package/dist/progress.cjs +195 -76
  66. package/dist/progress.mjs +7 -2448
  67. package/dist/vue2.cjs +33282 -25002
  68. package/dist/vue2.mjs +63 -5131
  69. package/dist/vue3.cjs +33282 -25002
  70. package/dist/vue3.mjs +53 -5122
  71. package/package.json +161 -136
  72. package/styles/avatar-creator.css +38 -0
  73. package/styles/base.css +32 -32
  74. package/styles/color.css +18 -18
  75. package/styles/emo.css +110 -89
  76. package/styles/progress.css +96 -96
  77. package/styles/theme.css +1 -0
  78. package/types/avatar-creator-styles.d.ts +10 -0
  79. package/types/avatar-creator.d.ts +143 -0
  80. package/types/capsule.d.ts +15 -15
  81. package/types/color.d.ts +15 -15
  82. package/types/emo-miao.d.ts +9 -0
  83. package/types/emo.d.ts +38 -37
  84. package/types/index.d.ts +593 -554
  85. package/types/progress.d.ts +16 -16
  86. package/types/vue2.d.ts +11 -10
  87. package/types/vue3.d.ts +169 -146
@@ -0,0 +1,1836 @@
1
+ import { c as createEmitter } from './emitter-CxLYMSbw.mjs';
2
+ import { p as parseSize } from './size-Bztk2ryJ.mjs';
3
+ import { t as toFiniteNumber, e as effectiveDprCap, g as getPreset, n as normalizeOptions, D as DEFAULTS, b as nextTick } from './number-BqWLX0iQ.mjs';
4
+ import { C as COPY } from './copy-7J0XIj_0.mjs';
5
+ import { a as hexToRgba, n as normalizeColor } from './color-DnmNceaf.mjs';
6
+ import { a as createVisibilityGuard, b as createReducedMotionPreference, c as createFrameGate, s as subscribeScheduler, w as wakeScheduler } from './motion-a8_f8_Ui.mjs';
7
+
8
+ const PROGRESS_MOTION_WIDTH = 240;
9
+ const PROGRESS_MOTION_HEIGHT = 80;
10
+ const PROGRESS_MOTION_DURATION = 12.0;
11
+ const PROGRESS_MOTION_MAX_PX = 40.0;
12
+
13
+ const PROFILE_CONFIG = {
14
+ 'model-training': { seed: 0.37, broad: 0.58, middle: 0.25, detail: 0.13, lobe: 0.24 },
15
+ 'agent-migration': { seed: 1.71, broad: 0.72, middle: 0.10, detail: 0.03, lobe: 0.18 },
16
+ 'visual-training': { seed: 2.83, broad: 0.66, middle: 0.16, detail: 0.06, lobe: 0.23 },
17
+ 'botanic-lab': { seed: 3.46, broad: 0.54, middle: 0.18, detail: 0.05, lobe: 0.28 },
18
+ // ponytail: first-pass tide = asymmetric time warp on the same motion pipeline.
19
+ // Refine (foam line / wash streaks) in the shader after visual QA.
20
+ 'tide': { seed: 4.12, broad: 0.82, middle: 0.07, detail: 0.02, lobe: 0.30, warp: 0.5 }
21
+ };
22
+
23
+ const CACHE$1 = Object.create(null);
24
+
25
+ function gaussian(value, center, width) {
26
+ const delta = (value - center) / Math.max(width, 0.001);
27
+ return Math.exp(-delta * delta);
28
+ }
29
+
30
+ function createMotionData(id, edgeStyle = 'flow') {
31
+ const profile = edgeStyle === 'tide'
32
+ ? PROFILE_CONFIG.tide
33
+ : (PROFILE_CONFIG[id] || PROFILE_CONFIG['visual-training']);
34
+ const data = new Uint8Array(PROGRESS_MOTION_WIDTH * PROGRESS_MOTION_HEIGHT);
35
+
36
+ for (let x = 0; x < PROGRESS_MOTION_WIDTH; x += 1) {
37
+ let time = (x / PROGRESS_MOTION_WIDTH) * Math.PI * 2;
38
+ if (profile.warp) time += profile.warp * Math.sin(time * 2);
39
+ const centerA = 0.28 + Math.sin(time * 0.53 + profile.seed) * 0.13;
40
+ const centerB = 0.70 + Math.cos(time * 0.47 + profile.seed * 1.7) * 0.12;
41
+
42
+ for (let y = 0; y < PROGRESS_MOTION_HEIGHT; y += 1) {
43
+ const ratio = y / Math.max(PROGRESS_MOTION_HEIGHT - 1, 1);
44
+ const envelope = Math.pow(Math.max(Math.sin(Math.PI * ratio), 0), 0.48);
45
+ const broad = Math.sin(ratio * Math.PI * 2 * 1.35 + time * 0.58 + profile.seed) * profile.broad;
46
+ const middle = Math.sin(ratio * Math.PI * 2 * 3.2 - time * 0.91 + profile.seed * 2.1) * profile.middle;
47
+ const detail = Math.sin(ratio * Math.PI * 2 * 6.1 + time * 1.31 + profile.seed * 3.2) * profile.detail;
48
+ const lobes = (
49
+ gaussian(ratio, centerA, 0.09) * Math.sin(time * 1.11 + profile.seed * 4.0) -
50
+ gaussian(ratio, centerB, 0.10) * Math.cos(time * 0.97 + profile.seed * 3.3)
51
+ ) * profile.lobe;
52
+ const normalized = Math.max(-1, Math.min(1, (broad + middle + detail + lobes) * envelope));
53
+ data[y * PROGRESS_MOTION_WIDTH + x] = Math.round((normalized * 0.5 + 0.5) * 255);
54
+ }
55
+ }
56
+
57
+ return data;
58
+ }
59
+
60
+ function getProgressMotionData(id, edgeStyle = 'flow') {
61
+ const key = `${id}:${edgeStyle}`;
62
+ if (!CACHE$1[key]) CACHE$1[key] = createMotionData(id, edgeStyle);
63
+ return CACHE$1[key];
64
+ }
65
+
66
+ function hexToRgb01(hex) {
67
+ const value = Number.parseInt(hex.replace('#', ''), 16);
68
+ return [
69
+ ((value >> 16) & 255) / 255,
70
+ ((value >> 8) & 255) / 255,
71
+ (value & 255) / 255
72
+ ];
73
+ }
74
+
75
+ function stringSeed$1(value) {
76
+ let hash = 2166136261;
77
+ for (const character of value) {
78
+ hash ^= character.charCodeAt(0);
79
+ hash = Math.imul(hash, 16777619);
80
+ }
81
+ return (hash >>> 0) / 4294967295;
82
+ }
83
+
84
+ const PROFILE_INDEX = {
85
+ 'model-training': 0,
86
+ 'agent-migration': 1,
87
+ 'visual-training': 2,
88
+ 'tide': 3
89
+ };
90
+
91
+ const MOTION_SCALE_FACTORS = {
92
+ 'model-training': 1.05,
93
+ 'agent-migration': 1.04,
94
+ 'visual-training': 1.04,
95
+ 'tide': 1.18
96
+ };
97
+
98
+ const VERTEX_SHADER = `#version 300 es
99
+ in vec2 a_position;
100
+ out vec2 v_uv;
101
+ void main() {
102
+ v_uv = a_position * 0.5 + 0.5;
103
+ gl_Position = vec4(a_position, 0.0, 1.0);
104
+ }`;
105
+
106
+ const FRAGMENT_SHADER = `#version 300 es
107
+ precision highp float;
108
+
109
+ in vec2 v_uv;
110
+ out vec4 outColor;
111
+
112
+ uniform vec2 u_resolution;
113
+ uniform float u_time;
114
+ uniform float u_progress;
115
+ uniform float u_seed;
116
+ uniform float u_profile;
117
+ uniform sampler2D u_motion;
118
+ uniform sampler2D u_effect;
119
+ uniform float u_hasEffect;
120
+ uniform float u_effectFrames;
121
+ uniform float u_motionDuration;
122
+ uniform float u_motionScale;
123
+ uniform vec3 u_dark;
124
+ uniform vec3 u_accentA;
125
+ uniform vec3 u_accentB;
126
+ uniform vec3 u_glow;
127
+
128
+ float hash21(vec2 p) {
129
+ p = fract(p * vec2(123.34, 456.21));
130
+ p += dot(p, p + 45.32 + u_seed * 11.7);
131
+ return fract(p.x * p.y);
132
+ }
133
+
134
+ float noise(vec2 p) {
135
+ vec2 i = floor(p);
136
+ vec2 f = fract(p);
137
+ f = f * f * (3.0 - 2.0 * f);
138
+ float a = hash21(i);
139
+ float b = hash21(i + vec2(1.0, 0.0));
140
+ float c = hash21(i + vec2(0.0, 1.0));
141
+ float d = hash21(i + vec2(1.0, 1.0));
142
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
143
+ }
144
+
145
+ float fbm(vec2 p) {
146
+ float value = 0.0;
147
+ float amplitude = 0.55;
148
+ mat2 rotation = mat2(0.82, 0.57, -0.57, 0.82);
149
+ for (int i = 0; i < 6; i++) {
150
+ value += noise(p) * amplitude;
151
+ p = rotation * p * 2.02 + 13.7;
152
+ amplitude *= 0.48;
153
+ }
154
+ return value;
155
+ }
156
+
157
+ float gaussian(float value, float center, float width) {
158
+ float delta = (value - center) / max(width, 0.0001);
159
+ return exp(-delta * delta);
160
+ }
161
+
162
+ float profileMix(float model, float agent, float visual) {
163
+ if (u_profile < 0.5) return model;
164
+ if (u_profile < 1.5) return agent;
165
+ return visual;
166
+ }
167
+
168
+ float motionSample(float y, float t) {
169
+ float phase = fract(t / max(u_motionDuration, 0.001));
170
+ float captured = texture(u_motion, vec2(phase, 1.0 - clamp(y, 0.0, 1.0))).r;
171
+ return (captured * 2.0 - 1.0) * u_motionScale;
172
+ }
173
+
174
+ float edgeDisplacement(float y, float t) {
175
+ return motionSample(y, t);
176
+ }
177
+
178
+ float flowDisplacement(float y, float t) {
179
+ return (
180
+ motionSample(y - 0.024, t) * 0.08 +
181
+ motionSample(y - 0.012, t) * 0.18 +
182
+ motionSample(y, t) * 0.48 +
183
+ motionSample(y + 0.012, t) * 0.18 +
184
+ motionSample(y + 0.024, t) * 0.08
185
+ );
186
+ }
187
+
188
+ float ellipseRing(vec2 p, float radius, float width) {
189
+ return gaussian(length(p), radius, width);
190
+ }
191
+
192
+ void main() {
193
+ vec2 uv = v_uv;
194
+ float t = u_time;
195
+ float edge = u_progress + edgeDisplacement(uv.y, t);
196
+ float flowEdge = u_progress + flowDisplacement(uv.y, t);
197
+ float d = uv.x - edge;
198
+ float fd = uv.x - flowEdge;
199
+
200
+ vec3 rightBase = vec3(0.125, 0.129, 0.145);
201
+ vec3 color = rightBase;
202
+
203
+ float leftMask = 1.0 - smoothstep(-0.001, 0.002, d);
204
+ color = mix(color, u_dark, leftMask * profileMix(0.96, 0.92, 0.96));
205
+
206
+ vec2 flowP = vec2((fd + 0.10) * 6.2, uv.y * 1.95);
207
+ float flowA = fbm(flowP + vec2(-t * 0.22, t * 0.27) + u_seed * 1.7);
208
+ float flowB = fbm(flowP * 1.52 + vec2(t * 0.28, -t * 0.36) + 8.2 + u_seed);
209
+ float flowC = fbm(flowP * 2.25 + vec2(-t * 0.41, t * 0.46) + 19.0);
210
+
211
+ float farCenter = profileMix(-0.060, -0.079, -0.045) + (flowA - 0.5) * profileMix(0.018, 0.022, 0.014);
212
+ float midCenter = profileMix(-0.039, -0.052, -0.030) + (flowB - 0.5) * profileMix(0.013, 0.016, 0.010);
213
+ float hotCenter = profileMix(-0.026, -0.029, -0.023) + (flowC - 0.5) * 0.010;
214
+
215
+ float farBand = gaussian(fd, farCenter, profileMix(0.035, 0.049, 0.030));
216
+ float midBand = gaussian(fd, midCenter, profileMix(0.026, 0.034, 0.026));
217
+ float hotBand = gaussian(fd, hotCenter, profileMix(0.023, 0.027, 0.026));
218
+ float darkTrough = gaussian(fd, profileMix(-0.050, -0.058, -0.044) + (flowB - 0.5) * 0.010, profileMix(0.020, 0.025, 0.021));
219
+
220
+ float ringY = 0.47 + sin(t * 0.58 + u_seed * 2.4) * 0.12;
221
+ vec2 ringP = vec2((fd + 0.086) / 0.078, (uv.y - ringY) / 0.25);
222
+ ringP += vec2((flowB - 0.5) * 0.08, (flowA - 0.5) * 0.06);
223
+ float ringTexture = fbm(ringP * 2.15 + vec2(t * 0.18, -t * 0.14) + u_seed * 1.9);
224
+ float ring = ellipseRing(ringP, 0.66, 0.32) * (0.30 + 0.64 * ringTexture);
225
+ float ringCore = gaussian(length(ringP), 0.25, 0.25);
226
+ float ringPulse = smoothstep(0.58, 0.90, 0.5 + 0.5 * sin(t * 0.82 + u_seed * 4.1));
227
+ float modelRing = ring * ringPulse * (1.0 - step(0.5, u_profile));
228
+ float visualRing = ring * 0.16 * step(1.5, u_profile) * ringPulse;
229
+
230
+ float cloudGate = leftMask * smoothstep(-0.30, -0.008, fd);
231
+ float textureA = smoothstep(0.24, 0.92, flowA * 0.72 + flowB * 0.42);
232
+ float textureB = smoothstep(0.28, 0.94, flowB * 0.68 + flowC * 0.38);
233
+
234
+ vec3 hotColor = u_accentB;
235
+ color += u_accentA * farBand * cloudGate * (0.07 + textureA * profileMix(0.42, 0.24, 0.40));
236
+ color += u_accentB * midBand * cloudGate * (0.15 + textureB * profileMix(0.70, 0.46, 0.68));
237
+ color += hotColor * hotBand * cloudGate * profileMix(0.88, 0.62, 0.84);
238
+ float modelMask = 1.0 - step(0.5, u_profile);
239
+ color += u_accentA * (modelRing + visualRing) * cloudGate * profileMix(0.54, 0.0, 0.34);
240
+ color *= 1.0 - darkTrough * profileMix(0.44, 0.24, 0.24) * cloudGate;
241
+ color *= 1.0 - ringCore * modelMask * ringPulse * 0.44 * cloudGate;
242
+
243
+ float broadHalo = exp(-abs(d) * 96.0);
244
+ float innerHalo = exp(-abs(d) * 176.0);
245
+ float colorCore = exp(-abs(d) * 360.0);
246
+ float sharpCore = exp(-abs(d) * 760.0);
247
+ float leftGate = 1.0 - smoothstep(-0.003, 0.005, d);
248
+
249
+ color += u_accentA * broadHalo * leftGate * profileMix(0.09, 0.04, 0.06);
250
+ color += hotColor * innerHalo * leftGate * profileMix(0.76, 0.72, 0.78);
251
+ color += u_glow * colorCore * profileMix(0.72, 0.34, 0.24);
252
+
253
+ float whiteStrength = profileMix(0.10, 0.0, 0.0);
254
+ color += vec3(1.0, 0.99, 0.91) * sharpCore * whiteStrength;
255
+
256
+ float rightCut = smoothstep(0.001, 0.006, d);
257
+ color = mix(color, rightBase, rightCut);
258
+
259
+ float effectX = (d * 1257.0 + 260.0) / 320.0;
260
+ float atlasPhase = fract(t / 12.0) * u_effectFrames;
261
+ float atlasFrameA = floor(atlasPhase);
262
+ float atlasFrameB = mod(atlasFrameA + 1.0, u_effectFrames);
263
+ float atlasMix = smoothstep(0.0, 1.0, fract(atlasPhase));
264
+ float atlasXA = (atlasFrameA + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
265
+ float atlasXB = (atlasFrameB + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
266
+ vec3 referenceA = texture(u_effect, vec2(atlasXA, uv.y)).rgb;
267
+ vec3 referenceB = texture(u_effect, vec2(atlasXB, uv.y)).rgb;
268
+ vec3 referenceColor = mix(referenceA, referenceB, atlasMix);
269
+ float stripMask = smoothstep(0.0, 0.018, effectX) * (1.0 - smoothstep(0.982, 1.0, effectX));
270
+ float referenceLeft = 1.0 - smoothstep(-0.026, -0.012, d);
271
+ // 参考图集只提供亮度结构,颜色始终由用户 colors(u_dark / u_accentA /
272
+ // u_accentB / u_glow)决定:这样 setColors / colors 属性在 WebGL 路径下
273
+ // 真实生效,改色有可见反馈,而不是被图集整体覆盖。
274
+ float referenceLuma = dot(referenceColor, vec3(0.299, 0.587, 0.114));
275
+ vec3 tintedColor = color * (0.42 + 0.86 * referenceLuma);
276
+ color = mix(color, tintedColor, stripMask * referenceLeft * u_hasEffect);
277
+
278
+ outColor = vec4(clamp(color, 0.0, 1.0), 1.0);
279
+ }`;
280
+
281
+ function compileShader(gl, type, source) {
282
+ const shader = gl.createShader(type);
283
+ gl.shaderSource(shader, source);
284
+ gl.compileShader(shader);
285
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
286
+ const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
287
+ gl.deleteShader(shader);
288
+ throw new Error(message);
289
+ }
290
+ return shader;
291
+ }
292
+
293
+ function createProgram(gl) {
294
+ const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
295
+ const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
296
+ const program = gl.createProgram();
297
+ gl.attachShader(program, vertex);
298
+ gl.attachShader(program, fragment);
299
+ gl.linkProgram(program);
300
+ gl.deleteShader(vertex);
301
+ gl.deleteShader(fragment);
302
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
303
+ const message = gl.getProgramInfoLog(program) || 'Unknown shader link error';
304
+ gl.deleteProgram(program);
305
+ throw new Error(message);
306
+ }
307
+ return program;
308
+ }
309
+
310
+ class ProgressFlowRenderer {
311
+ constructor(canvas, preset, options = {}) {
312
+ const gl = canvas.getContext('webgl2', {
313
+ alpha: false,
314
+ antialias: true,
315
+ premultipliedAlpha: false,
316
+ powerPreference: options.powerPreference || 'high-performance'
317
+ });
318
+ if (!gl) throw new Error('WebGL2 unavailable');
319
+
320
+ this.canvas = canvas;
321
+ this.options = options;
322
+ this.gl = gl;
323
+ this.profile = preset.edgeStyle === 'tide' ? 3 : (PROFILE_INDEX[preset.id] ?? 2);
324
+ this.seed = stringSeed$1(`${preset.id}-shader`) * 13.7 + 1.0;
325
+ this.colors = preset.colors.map(hexToRgb01);
326
+ this.motionData = getProgressMotionData(preset.id, preset.edgeStyle);
327
+ const motionFactor = preset.edgeStyle === 'tide'
328
+ ? MOTION_SCALE_FACTORS.tide
329
+ : (MOTION_SCALE_FACTORS[preset.id] || 1.04);
330
+ this.motionScale = (PROGRESS_MOTION_MAX_PX * motionFactor) / 1257;
331
+ this.disposed = false;
332
+ this.contextLost = false;
333
+ this.setupResources();
334
+ this.bindContextEvents();
335
+ }
336
+
337
+ setupResources() {
338
+ const gl = this.gl;
339
+ this.program = createProgram(gl);
340
+
341
+ this.position = gl.getAttribLocation(this.program, 'a_position');
342
+ this.uniforms = {
343
+ resolution: gl.getUniformLocation(this.program, 'u_resolution'),
344
+ time: gl.getUniformLocation(this.program, 'u_time'),
345
+ progress: gl.getUniformLocation(this.program, 'u_progress'),
346
+ seed: gl.getUniformLocation(this.program, 'u_seed'),
347
+ profile: gl.getUniformLocation(this.program, 'u_profile'),
348
+ motion: gl.getUniformLocation(this.program, 'u_motion'),
349
+ effect: gl.getUniformLocation(this.program, 'u_effect'),
350
+ hasEffect: gl.getUniformLocation(this.program, 'u_hasEffect'),
351
+ effectFrames: gl.getUniformLocation(this.program, 'u_effectFrames'),
352
+ motionDuration: gl.getUniformLocation(this.program, 'u_motionDuration'),
353
+ motionScale: gl.getUniformLocation(this.program, 'u_motionScale'),
354
+ dark: gl.getUniformLocation(this.program, 'u_dark'),
355
+ accentA: gl.getUniformLocation(this.program, 'u_accentA'),
356
+ accentB: gl.getUniformLocation(this.program, 'u_accentB'),
357
+ glow: gl.getUniformLocation(this.program, 'u_glow')
358
+ };
359
+
360
+ this.buffer = gl.createBuffer();
361
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
362
+ gl.bufferData(
363
+ gl.ARRAY_BUFFER,
364
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
365
+ gl.STATIC_DRAW
366
+ );
367
+
368
+ this.motionTexture = gl.createTexture();
369
+ gl.activeTexture(gl.TEXTURE0);
370
+ gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
371
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
372
+ gl.texImage2D(
373
+ gl.TEXTURE_2D,
374
+ 0,
375
+ gl.R8,
376
+ PROGRESS_MOTION_WIDTH,
377
+ PROGRESS_MOTION_HEIGHT,
378
+ 0,
379
+ gl.RED,
380
+ gl.UNSIGNED_BYTE,
381
+ this.motionData
382
+ );
383
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
384
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
385
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
386
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
387
+
388
+ this.effectTexture = gl.createTexture();
389
+ gl.activeTexture(gl.TEXTURE1);
390
+ gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
391
+ gl.texImage2D(
392
+ gl.TEXTURE_2D,
393
+ 0,
394
+ gl.RGB,
395
+ 1,
396
+ 1,
397
+ 0,
398
+ gl.RGB,
399
+ gl.UNSIGNED_BYTE,
400
+ new Uint8Array([32, 33, 38])
401
+ );
402
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
403
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
404
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
405
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
406
+ this.effectUploaded = false;
407
+ }
408
+
409
+ bindContextEvents() {
410
+ this.onContextLost = (event) => {
411
+ event.preventDefault();
412
+ if (this.disposed) return;
413
+ this.contextLost = true;
414
+ if (typeof this.options.onContextLost === 'function') this.options.onContextLost(event);
415
+ };
416
+ this.onContextRestored = () => {
417
+ if (this.disposed) return;
418
+ this.setupResources();
419
+ this.contextLost = false;
420
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
421
+ if (typeof this.options.onContextRestored === 'function') this.options.onContextRestored();
422
+ };
423
+ this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
424
+ this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
425
+ }
426
+
427
+ resize(width, height, dpr) {
428
+ const pixelWidth = Math.max(1, Math.round(width * dpr));
429
+ const pixelHeight = Math.max(1, Math.round(height * dpr));
430
+ if (this.canvas.width !== pixelWidth || this.canvas.height !== pixelHeight) {
431
+ this.canvas.width = pixelWidth;
432
+ this.canvas.height = pixelHeight;
433
+ }
434
+ this.gl.viewport(0, 0, pixelWidth, pixelHeight);
435
+ }
436
+
437
+ draw(time, progress, effectImage = null) {
438
+ if (this.disposed || this.contextLost) return;
439
+ const gl = this.gl;
440
+ gl.useProgram(this.program);
441
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
442
+ gl.enableVertexAttribArray(this.position);
443
+ gl.vertexAttribPointer(this.position, 2, gl.FLOAT, false, 0, 0);
444
+
445
+ gl.uniform2f(this.uniforms.resolution, this.canvas.width, this.canvas.height);
446
+ gl.uniform1f(this.uniforms.time, time);
447
+ gl.uniform1f(this.uniforms.progress, progress / 100);
448
+ gl.uniform1f(this.uniforms.seed, this.seed);
449
+ gl.uniform1f(this.uniforms.profile, this.profile);
450
+ gl.activeTexture(gl.TEXTURE0);
451
+ gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
452
+ gl.uniform1i(this.uniforms.motion, 0);
453
+ gl.uniform1f(this.uniforms.motionDuration, PROGRESS_MOTION_DURATION);
454
+ gl.uniform1f(this.uniforms.motionScale, this.motionScale);
455
+
456
+ let hasEffect = this.effectUploaded ? 1 : 0;
457
+ const effectWidth = effectImage && (effectImage.naturalWidth || effectImage.width || 0);
458
+ if (!this.effectUploaded && effectImage && effectWidth > 0) {
459
+ gl.activeTexture(gl.TEXTURE1);
460
+ gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
461
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
462
+ try {
463
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, effectImage);
464
+ this.effectUploaded = true;
465
+ hasEffect = 1;
466
+ } catch (error) {
467
+ console.warn('[画境观屿] 参考纹理图集上传失败,继续使用程序化降级。', error);
468
+ }
469
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
470
+ }
471
+ gl.activeTexture(gl.TEXTURE1);
472
+ gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
473
+ gl.uniform1i(this.uniforms.effect, 1);
474
+ gl.uniform1f(this.uniforms.hasEffect, hasEffect);
475
+ gl.uniform1f(this.uniforms.effectFrames, 24);
476
+
477
+ gl.uniform3fv(this.uniforms.dark, this.colors[0]);
478
+ gl.uniform3fv(this.uniforms.accentA, this.colors[1]);
479
+ gl.uniform3fv(this.uniforms.accentB, this.colors[2]);
480
+ gl.uniform3fv(this.uniforms.glow, this.colors[3]);
481
+
482
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
483
+ }
484
+
485
+ setColors(colors) {
486
+ this.colors = colors.map(hexToRgb01);
487
+ }
488
+
489
+ dispose() {
490
+ this.disposed = true;
491
+ const gl = this.gl;
492
+ this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
493
+ this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
494
+ gl.deleteBuffer(this.buffer);
495
+ gl.deleteTexture(this.motionTexture);
496
+ gl.deleteTexture(this.effectTexture);
497
+ gl.deleteProgram(this.program);
498
+ const lose = gl.getExtension('WEBGL_lose_context');
499
+ if (lose) lose.loseContext();
500
+ }
501
+ }
502
+
503
+ function createProgressFlowRenderer(canvas, preset, options = {}) {
504
+ try {
505
+ return new ProgressFlowRenderer(canvas, preset, options);
506
+ } catch (error) {
507
+ console.warn('[画境观屿] 进度流体 WebGL2 不可用,使用 Canvas 2D 降级。', error);
508
+ return null;
509
+ }
510
+ }
511
+
512
+ const PROGRESS_REFERENCE_DURATION = 12;
513
+ const PROGRESS_REFERENCE_FRAME_COUNT = 24;
514
+
515
+ const FRAME_WIDTH = 64;
516
+ const FRAME_HEIGHT = 32;
517
+ const CACHE = Object.create(null);
518
+
519
+ const PALETTES = {
520
+ 'model-training': ['#20131f', '#ff3f94', '#ff8a3d', '#fff06a'],
521
+ 'agent-migration': ['#111a31', '#245bff', '#00cfff', '#5dffe6'],
522
+ 'visual-training': ['#1f172b', '#7042ff', '#42f58d', '#c4ff8a'],
523
+ 'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff'],
524
+ 'botanic-lab': ['#102a23', '#287a58', '#9bd65b', '#f2ffc4']
525
+ };
526
+
527
+ function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
528
+ context.save();
529
+ context.translate(x, y);
530
+ context.scale(1, radiusY / radiusX);
531
+ const gradient = context.createRadialGradient(0, 0, 0, 0, 0, radiusX);
532
+ const alphaHex = (value) => {
533
+ const text = Math.round(value).toString(16);
534
+ return text.length === 1 ? `0${text}` : text;
535
+ };
536
+ gradient.addColorStop(0, `${color}${alphaHex(alpha * 255)}`);
537
+ gradient.addColorStop(0.46, `${color}${alphaHex(alpha * 0.42 * 255)}`);
538
+ gradient.addColorStop(1, `${color}00`);
539
+ context.fillStyle = gradient;
540
+ context.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
541
+ context.restore();
542
+ }
543
+
544
+ function createAtlas(id) {
545
+ const palette = PALETTES[id] || PALETTES['visual-training'];
546
+ const canvas = document.createElement('canvas');
547
+ canvas.width = FRAME_WIDTH * PROGRESS_REFERENCE_FRAME_COUNT;
548
+ canvas.height = FRAME_HEIGHT;
549
+ const context = canvas.getContext('2d');
550
+
551
+ for (let frame = 0; frame < PROGRESS_REFERENCE_FRAME_COUNT; frame += 1) {
552
+ const phase = (frame / PROGRESS_REFERENCE_FRAME_COUNT) * Math.PI * 2;
553
+ const left = frame * FRAME_WIDTH;
554
+ context.save();
555
+ context.translate(left, 0);
556
+ context.fillStyle = palette[0];
557
+ context.fillRect(0, 0, FRAME_WIDTH, FRAME_HEIGHT);
558
+ context.globalCompositeOperation = 'screen';
559
+
560
+ 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);
561
+ drawCloud(context, 45 + Math.cos(phase * 0.72) * 4, 23 + Math.sin(phase * 0.54) * 4, 22, 15, palette[2], 0.44);
562
+ 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);
563
+
564
+ context.globalCompositeOperation = 'source-over';
565
+ const trough = context.createRadialGradient(41, 16, 1, 41, 16, 17);
566
+ trough.addColorStop(0, 'rgba(5,6,11,0.64)');
567
+ trough.addColorStop(0.58, 'rgba(6,7,12,0.26)');
568
+ trough.addColorStop(1, 'rgba(6,7,12,0)');
569
+ context.fillStyle = trough;
570
+ context.fillRect(20, 0, 44, FRAME_HEIGHT);
571
+ context.restore();
572
+ }
573
+
574
+ // A canvas is a valid TexImageSource. Keeping it directly avoids a PNG
575
+ // encode -> base64 allocation -> Image decode round trip at startup.
576
+ return { image: canvas, ready: true };
577
+ }
578
+
579
+ function getProgressReferenceAtlas(id) {
580
+ if (!CACHE[id]) CACHE[id] = createAtlas(id);
581
+ return CACHE[id];
582
+ }
583
+
584
+ /**
585
+ * Attach a WebGL2 fluid overlay to a progress capsule root.
586
+ *
587
+ * @param {object} params
588
+ * @param {HTMLElement} params.root progress capsule root element
589
+ * @param {HTMLCanvasElement} params.canvas 2D fallback canvas (kept beneath)
590
+ * @param {object} params.preset progress preset
591
+ * @param {() => number} params.getProgress reads the current progress value
592
+ * @returns {{ update(flowTime: number): void, setDprCap(cap: number): void, dispose(): void } | null}
593
+ */
594
+ function attachProgressFlowOverlay({
595
+ root,
596
+ canvas,
597
+ preset,
598
+ getProgress,
599
+ dprCap = 2,
600
+ powerPreference = 'high-performance',
601
+ onContextLost,
602
+ onContextRestored
603
+ }) {
604
+ const overlay = document.createElement('canvas');
605
+ overlay.className = 'hj-progress-canvas hj-progress-overlay';
606
+ overlay.setAttribute('aria-hidden', 'true');
607
+ canvas.insertAdjacentElement('afterend', overlay);
608
+
609
+ const renderer = createProgressFlowRenderer(overlay, preset, {
610
+ powerPreference,
611
+ onContextLost,
612
+ onContextRestored
613
+ });
614
+ if (!renderer) {
615
+ overlay.remove();
616
+ return null;
617
+ }
618
+
619
+ const atlas = getProgressReferenceAtlas(preset.id);
620
+ root.classList.add('has-webgl-progress');
621
+
622
+ const resize = () => {
623
+ const bounds = root.getBoundingClientRect();
624
+ const width = Math.max(1, bounds.width);
625
+ const height = Math.max(1, bounds.height);
626
+ const dpr = Math.min(window.devicePixelRatio || 1, dprCap);
627
+ overlay.style.width = `${width}px`;
628
+ overlay.style.height = `${height}px`;
629
+ renderer.resize(width, height, dpr);
630
+ };
631
+
632
+ let resizeObserver = null;
633
+ let onWindowResize = null;
634
+ if (typeof ResizeObserver !== 'undefined') {
635
+ resizeObserver = new ResizeObserver(resize);
636
+ resizeObserver.observe(root);
637
+ } else {
638
+ onWindowResize = resize;
639
+ window.addEventListener('resize', onWindowResize);
640
+ }
641
+ resize();
642
+
643
+ return {
644
+ update(flowTime) {
645
+ const effectTime = flowTime % PROGRESS_REFERENCE_DURATION;
646
+ const progress = getProgress();
647
+ renderer.draw(
648
+ effectTime,
649
+ Number.isFinite(progress) ? progress : preset.initialProgress,
650
+ atlas.ready ? atlas.image : null
651
+ );
652
+ },
653
+ setColors(colors) {
654
+ renderer.setColors(colors);
655
+ },
656
+ setDprCap(cap) {
657
+ dprCap = cap;
658
+ resize();
659
+ },
660
+ dispose() {
661
+ if (resizeObserver) resizeObserver.disconnect();
662
+ if (onWindowResize) window.removeEventListener('resize', onWindowResize);
663
+ renderer.dispose();
664
+ overlay.remove();
665
+ root.classList.remove('has-webgl-progress');
666
+ }
667
+ };
668
+ }
669
+
670
+ function normalizeRange(min, max, fallbackMin = 0, fallbackMax = 100) {
671
+ let nextMin = toFiniteNumber(min);
672
+ let nextMax = toFiniteNumber(max);
673
+ if (nextMin === null) nextMin = fallbackMin;
674
+ if (nextMax === null) nextMax = fallbackMax;
675
+ if (nextMin > nextMax) {
676
+ const swap = nextMin;
677
+ nextMin = nextMax;
678
+ nextMax = swap;
679
+ }
680
+ return [nextMin, nextMax];
681
+ }
682
+
683
+ function clampProgressValue(value, min, max) {
684
+ const number = toFiniteNumber(value);
685
+ if (number === null) return null;
686
+ return Math.min(Math.max(number, min), max);
687
+ }
688
+
689
+ function normalizeProgressStep(step, fallback = 'any') {
690
+ if (step === 'any' || step == null) return 'any';
691
+ const increment = toFiniteNumber(step);
692
+ return increment !== null && increment > 0 ? increment : fallback;
693
+ }
694
+
695
+ function snapProgressValue(value, min, max, step = 'any') {
696
+ const clamped = clampProgressValue(value, min, max);
697
+ if (clamped === null) return null;
698
+ if (step === 'any' || step == null) return clamped;
699
+ const increment = normalizeProgressStep(step);
700
+ if (increment === 'any') return clamped;
701
+ const snapped = min + Math.round((clamped - min) / increment) * increment;
702
+ return Math.min(Math.max(Number(snapped.toFixed(12)), min), max);
703
+ }
704
+
705
+ function progressRatio(value, min, max) {
706
+ const range = max - min;
707
+ if (!Number.isFinite(range) || range <= 0) return 0;
708
+ return Math.min(Math.max((value - min) / range, 0), 1);
709
+ }
710
+
711
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
712
+ const SUPPORTS_CTX_FILTER = typeof CanvasRenderingContext2D !== 'undefined' && 'filter' in CanvasRenderingContext2D.prototype;
713
+
714
+ function stringSeed(value) {
715
+ let hash = 2166136261;
716
+ for (const character of value) {
717
+ hash ^= character.charCodeAt(0);
718
+ hash = Math.imul(hash, 16777619);
719
+ }
720
+ return (hash >>> 0) / 4294967295;
721
+ }
722
+
723
+ function interpolate(template, values) {
724
+ return template.replace(/\{(\w+)\}/g, (_, key) => (values[key] !== undefined ? values[key] : ''));
725
+ }
726
+
727
+ const FLOW_PROFILES = {
728
+ 'model-training': {
729
+ cycles: [0.98, 3.05, 5.45],
730
+ amplitudes: [11.2, 11.0, 5.2],
731
+ speeds: [0.31, -0.53, 0.78],
732
+ bulgeAmplitude: 8.2,
733
+ timeScale: 1.0,
734
+ glowWidth: 5.8,
735
+ haloWidth: 20,
736
+ whiteAlpha: 0.72,
737
+ whiteWidth: 1.15,
738
+ cloudWidth: 0.17,
739
+ autoRange: [25, 66]
740
+ },
741
+ 'agent-migration': {
742
+ cycles: [0.62, 1.55, 2.95],
743
+ amplitudes: [16.0, 7.8, 2.3],
744
+ speeds: [0.25, -0.4, 0.60],
745
+ bulgeAmplitude: 8.4,
746
+ timeScale: 0.82,
747
+ glowWidth: 6.3,
748
+ haloWidth: 21,
749
+ whiteAlpha: 0.18,
750
+ whiteWidth: 0.45,
751
+ cloudWidth: 0.18,
752
+ autoRange: [24, 62]
753
+ },
754
+ 'visual-training': {
755
+ cycles: [0.88, 2.45, 4.35],
756
+ amplitudes: [12.8, 10.2, 4.3],
757
+ speeds: [0.28, -0.47, 0.69],
758
+ bulgeAmplitude: 7.3,
759
+ timeScale: 0.91,
760
+ glowWidth: 6.0,
761
+ haloWidth: 21,
762
+ whiteAlpha: 0.30,
763
+ whiteWidth: 0.55,
764
+ cloudWidth: 0.175,
765
+ autoRange: [20, 75]
766
+ },
767
+ // ponytail: first-pass tide = asymmetric surge on the 2D fallback path.
768
+ // Tune surge/amplitudes after visual QA against the WebGL overlay.
769
+ 'tide': {
770
+ cycles: [0.72, 1.9, 4.2],
771
+ amplitudes: [19.0, 6.5, 1.5],
772
+ speeds: [0.42, -0.5, 0.66],
773
+ bulgeAmplitude: 9.5,
774
+ timeScale: 0.85,
775
+ glowWidth: 6.2,
776
+ haloWidth: 21,
777
+ whiteAlpha: 0.5,
778
+ whiteWidth: 1.0,
779
+ cloudWidth: 0.18,
780
+ autoRange: [18, 70],
781
+ surge: 0.55
782
+ }
783
+ };
784
+
785
+ class ProgressCapsuleController {
786
+ constructor({ root, canvas, valueElement, preset, emitter, options, copy, dirty }) {
787
+ this.root = root;
788
+ this.canvas = canvas;
789
+ this.valueElement = valueElement;
790
+ this.preset = preset;
791
+ this.emitter = emitter;
792
+ this.options = options;
793
+ this.copy = copy;
794
+ this.dirty = dirty;
795
+ this.valueSuffix = options.valueSuffix ?? copy.valueSuffix;
796
+ this.profile = preset.edgeStyle === 'tide'
797
+ ? FLOW_PROFILES.tide
798
+ : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
799
+ [this.min, this.max] = normalizeRange(options.min, options.max);
800
+ this.step = normalizeProgressStep(options.step);
801
+ this.precision = Number.isInteger(Number(options.precision))
802
+ ? clamp(Number(options.precision), 0, 20)
803
+ : 0;
804
+ this.formatValue = typeof options.formatValue === 'function' ? options.formatValue : null;
805
+ this.direction = options.direction === 'rtl' ? 'rtl' : 'ltr';
806
+ this.dprCap = effectiveDprCap(options.quality, options.renderScale);
807
+ this.ctx = canvas.getContext('2d');
808
+ this.value = snapProgressValue(options.value ?? preset.initialProgress, this.min, this.max, this.step);
809
+ if (this.value === null) this.value = clamp(preset.initialProgress, this.min, this.max);
810
+ this.dragging = false;
811
+ this.webglActive = false;
812
+ this.flowTime = stringSeed(preset.id) * 31;
813
+ this.seed = stringSeed(`${preset.id}-reference`) * Math.PI * 2;
814
+ this.randomState = Math.floor(stringSeed(`${preset.id}-auto`) * 0x7fffffff) || 1;
815
+ this.dpr = 1;
816
+ this.width = 0;
817
+ this.height = 0;
818
+ this.handlers = {};
819
+
820
+ this.onResize = () => {
821
+ this.resizeCanvas();
822
+ if (typeof this.options.onResize === 'function') this.options.onResize();
823
+ };
824
+ this.resizeObserver = typeof ResizeObserver !== 'undefined'
825
+ ? new ResizeObserver(this.onResize)
826
+ : null;
827
+ if (this.resizeObserver) this.resizeObserver.observe(this.root);
828
+ else window.addEventListener('resize', this.onResize);
829
+
830
+ this.suppressEvents = true;
831
+ this.setProgress(this.value, 'init');
832
+ this.suppressEvents = false;
833
+ this.bindEvents();
834
+ this.resizeCanvas();
835
+ }
836
+
837
+ random() {
838
+ this.randomState = (Math.imul(this.randomState, 1664525) + 1013904223) >>> 0;
839
+ return this.randomState / 4294967296;
840
+ }
841
+
842
+ displayText() {
843
+ if (this.formatValue) {
844
+ return String(this.formatValue(this.value, {
845
+ min: this.min,
846
+ max: this.max,
847
+ suffix: this.valueSuffix
848
+ }));
849
+ }
850
+ const number = this.precision > 0 ? this.value.toFixed(this.precision) : String(Math.round(this.value));
851
+ return `${number}${this.valueSuffix}`;
852
+ }
853
+
854
+ syncValueDom() {
855
+ const text = this.displayText();
856
+ this.root.style.setProperty('--progress', this.value.toFixed(2));
857
+ this.root.style.setProperty('--progress-ratio', progressRatio(this.value, this.min, this.max).toFixed(4));
858
+ this.root.setAttribute('aria-valuenow', String(this.value));
859
+ this.root.setAttribute('aria-valuetext', text);
860
+ if (this.valueElement) this.valueElement.textContent = text;
861
+ }
862
+
863
+ setProgress(nextValue, source = 'auto') {
864
+ const next = snapProgressValue(nextValue, this.min, this.max, this.step);
865
+ if (next === null) return false;
866
+ this.value = next;
867
+ this.syncValueDom();
868
+ if (!this.suppressEvents) {
869
+ if (this.dirty) this.dirty.value = true;
870
+ this.emitter.emit('change', { value: this.value, source });
871
+ }
872
+ return true;
873
+ }
874
+
875
+ setValueSuffix(suffix) {
876
+ if (this.dirty) this.dirty.valueSuffix = true;
877
+ this.valueSuffix = String(suffix == null ? '' : suffix);
878
+ this.syncValueDom();
879
+ return this;
880
+ }
881
+
882
+ setStep(step) {
883
+ const nextStep = normalizeProgressStep(step, this.step);
884
+ if (nextStep === this.step) return this;
885
+ if (this.dirty) this.dirty.step = true;
886
+ this.step = nextStep;
887
+ const next = snapProgressValue(this.value, this.min, this.max, this.step);
888
+ if (next !== null && next !== this.value) this.setProgress(next, 'prop');
889
+ return this;
890
+ }
891
+
892
+ setPrecision(precision) {
893
+ const value = Number(precision);
894
+ if (!Number.isInteger(value) || value < 0 || value > 20) return this;
895
+ if (this.dirty) this.dirty.precision = true;
896
+ this.precision = value;
897
+ this.syncValueDom();
898
+ return this;
899
+ }
900
+
901
+ setFormatValue(formatter) {
902
+ if (formatter != null && typeof formatter !== 'function') return this;
903
+ if (this.dirty) this.dirty.formatValue = true;
904
+ this.formatValue = formatter || null;
905
+ this.syncValueDom();
906
+ return this;
907
+ }
908
+
909
+ setDirection(direction) {
910
+ if (this.dirty) this.dirty.direction = true;
911
+ this.direction = direction === 'rtl' ? 'rtl' : 'ltr';
912
+ this.root.dataset.direction = this.direction;
913
+ return this;
914
+ }
915
+
916
+ setShowValue(show) {
917
+ if (this.dirty) this.dirty.showValue = true;
918
+ if (this.valueElement) this.valueElement.hidden = show === false;
919
+ return this;
920
+ }
921
+
922
+ resizeCanvas() {
923
+ const bounds = this.root.getBoundingClientRect();
924
+ this.dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
925
+ this.width = Math.max(1, bounds.width);
926
+ this.height = Math.max(1, bounds.height);
927
+ this.canvas.width = Math.round(this.width * this.dpr);
928
+ this.canvas.height = Math.round(this.height * this.dpr);
929
+ this.canvas.style.width = `${this.width}px`;
930
+ this.canvas.style.height = `${this.height}px`;
931
+ this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
932
+ }
933
+
934
+ edgeEnvelope(yRatio) {
935
+ const edge = Math.sin(Math.PI * clamp(yRatio, 0, 1));
936
+ return Math.pow(Math.max(edge, 0), 0.48);
937
+ }
938
+
939
+ localBulge(yRatio, time, index) {
940
+ const direction = index === 0 ? 1 : -1;
941
+ const center = 0.28 + index * 0.40 + Math.sin(time * (0.19 + index * 0.035) + this.seed * (1.1 + index)) * 0.13;
942
+ const width = 0.075 + index * 0.016 + Math.sin(time * 0.13 + this.seed * 2.1) * 0.012;
943
+ const distance = (yRatio - center) / Math.max(width, 0.035);
944
+ const gaussian = Math.exp(-0.5 * distance * distance);
945
+ return gaussian * Math.sin(time * (0.71 + index * 0.09) + this.seed * (2.7 + index)) * this.profile.bulgeAmplitude * direction;
946
+ }
947
+
948
+ edgeOffset(y, time, phase = 0, amplitudeScale = 1) {
949
+ const yRatio = this.height > 0 ? y / this.height : 0;
950
+ const envelope = this.edgeEnvelope(yRatio);
951
+ const scaledTime = time * this.profile.timeScale;
952
+ const phaseTime = this.profile.surge
953
+ ? scaledTime + this.profile.surge * Math.sin(scaledTime * 2)
954
+ : scaledTime;
955
+ let offset = 0;
956
+
957
+ for (let index = 0; index < this.profile.cycles.length; index += 1) {
958
+ const cycle = this.profile.cycles[index];
959
+ const amplitude = this.profile.amplitudes[index];
960
+ const speed = this.profile.speeds[index];
961
+ const amplitudeMotion = 0.74 + 0.26 * Math.sin(
962
+ phaseTime * (0.17 + index * 0.045) + this.seed * (index + 2.4)
963
+ );
964
+ offset += Math.sin(
965
+ yRatio * Math.PI * 2 * cycle + phaseTime * speed * Math.PI * 2 + this.seed * (index + 1) + phase
966
+ ) * amplitude * amplitudeMotion;
967
+ }
968
+
969
+ offset += this.localBulge(yRatio, phaseTime + phase, 0);
970
+ offset += this.localBulge(yRatio, phaseTime - phase * 0.7, 1);
971
+ return offset * envelope * amplitudeScale;
972
+ }
973
+
974
+ createEdgePath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
975
+ const step = Math.max(1.8, this.height / 92);
976
+ ctx.beginPath();
977
+ for (let y = 0; y <= this.height + step; y += step) {
978
+ const x = baseX + this.edgeOffset(y, time, phase, amplitudeScale);
979
+ if (y === 0) ctx.moveTo(x, y);
980
+ else ctx.lineTo(x, y);
981
+ }
982
+ }
983
+
984
+ createFillPath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
985
+ const step = Math.max(1.8, this.height / 92);
986
+ ctx.beginPath();
987
+ ctx.moveTo(0, 0);
988
+ ctx.lineTo(baseX + this.edgeOffset(0, time, phase, amplitudeScale), 0);
989
+ for (let y = step; y <= this.height + step; y += step) {
990
+ ctx.lineTo(baseX + this.edgeOffset(y, time, phase, amplitudeScale), y);
991
+ }
992
+ ctx.lineTo(0, this.height);
993
+ ctx.closePath();
994
+ }
995
+
996
+ drawEllipticalGlow(x, y, radiusX, radiusY, color, alpha) {
997
+ const ctx = this.ctx;
998
+ ctx.save();
999
+ ctx.translate(x, y);
1000
+ ctx.scale(1, radiusY / radiusX);
1001
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
1002
+ gradient.addColorStop(0, hexToRgba(color, alpha));
1003
+ gradient.addColorStop(0.42, hexToRgba(color, alpha * 0.48));
1004
+ gradient.addColorStop(1, hexToRgba(color, 0));
1005
+ ctx.globalCompositeOperation = 'screen';
1006
+ ctx.fillStyle = gradient;
1007
+ ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
1008
+ ctx.restore();
1009
+ }
1010
+
1011
+ drawDarkEllipticalShadow(x, y, radiusX, radiusY, alpha) {
1012
+ const ctx = this.ctx;
1013
+ ctx.save();
1014
+ ctx.translate(x, y);
1015
+ ctx.scale(1, radiusY / radiusX);
1016
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
1017
+ gradient.addColorStop(0, `rgba(6, 6, 11, ${alpha})`);
1018
+ gradient.addColorStop(0.54, `rgba(8, 8, 14, ${alpha * 0.62})`);
1019
+ gradient.addColorStop(1, 'rgba(8, 8, 14, 0)');
1020
+ ctx.globalCompositeOperation = 'source-over';
1021
+ ctx.fillStyle = gradient;
1022
+ ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
1023
+ ctx.restore();
1024
+ }
1025
+
1026
+ drawColorClouds(shoreline, time, accentA, accentB, glow) {
1027
+ this.ctx;
1028
+ const width = this.width;
1029
+ const height = this.height;
1030
+ const scale = this.profile.cloudWidth;
1031
+ const t = time * this.profile.timeScale;
1032
+
1033
+ const upperY = height * (0.28 + Math.sin(t * 0.24 + this.seed) * 0.13);
1034
+ const lowerY = height * (0.70 + Math.cos(t * 0.21 + this.seed * 1.7) * 0.12);
1035
+ const middleY = height * (0.49 + Math.sin(t * 0.31 + this.seed * 2.3) * 0.15);
1036
+
1037
+ const farX = shoreline - width * 0.095;
1038
+ const farRx = Math.max(52, width * scale);
1039
+ const farRy = height * 0.42;
1040
+ this.drawEllipticalGlow(farX, upperY, farRx, farRy, accentA, 0.38);
1041
+ this.drawDarkEllipticalShadow(
1042
+ farX + farRx * 0.16,
1043
+ upperY,
1044
+ farRx * 0.58,
1045
+ farRy * 0.66,
1046
+ 0.74
1047
+ );
1048
+
1049
+ const lowerX = shoreline - width * 0.072;
1050
+ const lowerRx = Math.max(44, width * scale * 0.82);
1051
+ const lowerRy = height * 0.36;
1052
+ this.drawEllipticalGlow(lowerX, lowerY, lowerRx, lowerRy, accentB, 0.31);
1053
+ this.drawDarkEllipticalShadow(
1054
+ lowerX + lowerRx * 0.14,
1055
+ lowerY,
1056
+ lowerRx * 0.54,
1057
+ lowerRy * 0.62,
1058
+ 0.64
1059
+ );
1060
+
1061
+ this.drawEllipticalGlow(
1062
+ shoreline - width * 0.034,
1063
+ middleY,
1064
+ Math.max(30, width * scale * 0.48),
1065
+ height * 0.27,
1066
+ glow,
1067
+ 0.20
1068
+ );
1069
+ }
1070
+
1071
+ drawPathBand({ baseX, time, phase, amplitudeScale, color, alpha, blur, width, composite = 'screen' }) {
1072
+ const ctx = this.ctx;
1073
+ this.createEdgePath(ctx, baseX, time, phase, amplitudeScale);
1074
+ ctx.save();
1075
+ ctx.globalCompositeOperation = composite;
1076
+ ctx.globalAlpha = alpha;
1077
+ // Safari < 18 and some old WebViews ignore ctx.filter; setting it is a
1078
+ // no-op there, so only assign when supported to keep intent explicit.
1079
+ if (SUPPORTS_CTX_FILTER) ctx.filter = `blur(${blur}px)`;
1080
+ ctx.strokeStyle = color;
1081
+ ctx.lineWidth = width;
1082
+ ctx.stroke();
1083
+ ctx.restore();
1084
+ }
1085
+
1086
+ setRange(min, max) {
1087
+ const nextRange = normalizeRange(min, max, this.min, this.max);
1088
+ this.min = nextRange[0];
1089
+ this.max = nextRange[1];
1090
+ this.root.setAttribute('aria-valuemin', String(this.min));
1091
+ this.root.setAttribute('aria-valuemax', String(this.max));
1092
+ const next = snapProgressValue(this.value, this.min, this.max, this.step);
1093
+ if (next !== this.value) this.setProgress(next, 'prop');
1094
+ else this.syncValueDom();
1095
+ return this;
1096
+ }
1097
+
1098
+ drawReferenceFlow() {
1099
+ const ctx = this.ctx;
1100
+ const width = this.width;
1101
+ const height = this.height;
1102
+ if (!ctx || width <= 0 || height <= 0) return;
1103
+
1104
+ const range = Math.max(this.max - this.min, 1);
1105
+ const shoreline = width * Math.min(Math.max((this.value - this.min) / range, 0), 1);
1106
+ const time = this.flowTime;
1107
+ const [dark, accentA, accentB, glow] = this.preset.colors;
1108
+
1109
+ ctx.clearRect(0, 0, width, height);
1110
+ ctx.fillStyle = '#202126';
1111
+ ctx.fillRect(0, 0, width, height);
1112
+
1113
+ this.createFillPath(ctx, shoreline, time, 0, 1);
1114
+ const bodyGradient = ctx.createLinearGradient(0, 0, Math.max(shoreline, 1), 0);
1115
+ bodyGradient.addColorStop(0, dark);
1116
+ bodyGradient.addColorStop(0.74, dark);
1117
+ bodyGradient.addColorStop(0.89, hexToRgba(dark, 0.99));
1118
+ bodyGradient.addColorStop(0.955, hexToRgba(accentA, 0.09));
1119
+ bodyGradient.addColorStop(0.992, hexToRgba(accentB, 0.54));
1120
+ bodyGradient.addColorStop(1, hexToRgba(glow, 0.78));
1121
+ ctx.fillStyle = bodyGradient;
1122
+ ctx.fill();
1123
+
1124
+ this.drawColorClouds(shoreline, time, accentA, accentB, glow);
1125
+
1126
+ this.drawPathBand({
1127
+ baseX: shoreline - width * 0.105,
1128
+ time,
1129
+ phase: 1.42,
1130
+ amplitudeScale: 1.18,
1131
+ color: accentA,
1132
+ alpha: 0.22,
1133
+ blur: 21,
1134
+ width: 54
1135
+ });
1136
+ this.drawPathBand({
1137
+ baseX: shoreline - width * 0.073,
1138
+ time,
1139
+ phase: -0.92,
1140
+ amplitudeScale: 1.06,
1141
+ color: accentB,
1142
+ alpha: 0.30,
1143
+ blur: 15,
1144
+ width: 42
1145
+ });
1146
+ this.drawPathBand({
1147
+ baseX: shoreline - width * 0.047,
1148
+ time,
1149
+ phase: 0.42,
1150
+ amplitudeScale: 0.94,
1151
+ color: 'rgba(5, 5, 10, 0.92)',
1152
+ alpha: 0.72,
1153
+ blur: 12,
1154
+ width: 34,
1155
+ composite: 'source-over'
1156
+ });
1157
+ this.drawPathBand({
1158
+ baseX: shoreline - width * 0.025,
1159
+ time,
1160
+ phase: -0.28,
1161
+ amplitudeScale: 0.96,
1162
+ color: accentB,
1163
+ alpha: 0.66,
1164
+ blur: 8,
1165
+ width: 28
1166
+ });
1167
+
1168
+ ctx.save();
1169
+ this.createFillPath(ctx, shoreline, time, 0, 1);
1170
+ ctx.clip();
1171
+ this.createEdgePath(ctx, shoreline, time, 0, 1);
1172
+
1173
+ ctx.save();
1174
+ ctx.globalCompositeOperation = 'screen';
1175
+ ctx.strokeStyle = hexToRgba(accentA, 0.20);
1176
+ ctx.lineWidth = this.profile.haloWidth;
1177
+ ctx.shadowColor = accentA;
1178
+ ctx.shadowBlur = this.profile.haloWidth * 0.72;
1179
+ ctx.stroke();
1180
+ ctx.restore();
1181
+
1182
+ ctx.save();
1183
+ ctx.globalCompositeOperation = 'screen';
1184
+ ctx.strokeStyle = hexToRgba(accentB, 0.78);
1185
+ ctx.lineWidth = this.profile.glowWidth + 4.2;
1186
+ ctx.shadowColor = accentB;
1187
+ ctx.shadowBlur = 8;
1188
+ ctx.stroke();
1189
+ ctx.restore();
1190
+
1191
+ ctx.save();
1192
+ ctx.globalCompositeOperation = 'screen';
1193
+ ctx.strokeStyle = hexToRgba(glow, 0.88);
1194
+ ctx.lineWidth = this.profile.glowWidth;
1195
+ ctx.shadowColor = glow;
1196
+ ctx.shadowBlur = 5;
1197
+ ctx.stroke();
1198
+ ctx.restore();
1199
+
1200
+ ctx.restore();
1201
+
1202
+ ctx.save();
1203
+ ctx.globalCompositeOperation = 'screen';
1204
+ ctx.strokeStyle = hexToRgba(glow, 0.84);
1205
+ ctx.lineWidth = 2.15;
1206
+ ctx.shadowColor = glow;
1207
+ ctx.shadowBlur = 2.5;
1208
+ this.createEdgePath(ctx, shoreline, time, 0, 1);
1209
+ ctx.stroke();
1210
+ ctx.restore();
1211
+
1212
+ if (this.profile.whiteAlpha > 0.05) {
1213
+ ctx.save();
1214
+ ctx.globalCompositeOperation = 'screen';
1215
+ ctx.strokeStyle = `rgba(255,255,245,${this.profile.whiteAlpha})`;
1216
+ ctx.lineWidth = this.profile.whiteWidth;
1217
+ this.createEdgePath(ctx, shoreline, time, 0, 1);
1218
+ ctx.stroke();
1219
+ ctx.restore();
1220
+ }
1221
+ }
1222
+
1223
+ updateFromPointer(event) {
1224
+ const bounds = this.root.getBoundingClientRect();
1225
+ let ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
1226
+ if (this.direction === 'rtl') ratio = 1 - ratio;
1227
+ this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
1228
+ }
1229
+
1230
+ onKeyDown(event) {
1231
+ const range = this.max - this.min;
1232
+ const configuredStep = Number(this.step);
1233
+ const step = Number.isFinite(configuredStep) && configuredStep > 0
1234
+ ? configuredStep
1235
+ : Math.max(range / 100, 1e-7);
1236
+ const direction = this.direction === 'rtl' ? -1 : 1;
1237
+ const aliases = { Left: 'ArrowLeft', Right: 'ArrowRight', Up: 'ArrowUp', Down: 'ArrowDown' };
1238
+ const keyCodes = {
1239
+ 35: 'End', 36: 'Home', 33: 'PageUp', 34: 'PageDown',
1240
+ 37: 'ArrowLeft', 38: 'ArrowUp', 39: 'ArrowRight', 40: 'ArrowDown'
1241
+ };
1242
+ const key = aliases[event.key] || event.key || keyCodes[event.keyCode];
1243
+ let next = null;
1244
+ if (key === 'ArrowRight') next = this.value + step * direction;
1245
+ else if (key === 'ArrowLeft') next = this.value - step * direction;
1246
+ else if (key === 'ArrowUp') next = this.value + step;
1247
+ else if (key === 'ArrowDown') next = this.value - step;
1248
+ else if (key === 'PageUp') next = this.value + step * 10;
1249
+ else if (key === 'PageDown') next = this.value - step * 10;
1250
+ else if (key === 'Home') next = this.min;
1251
+ else if (key === 'End') next = this.max;
1252
+ if (next === null) return;
1253
+ event.preventDefault();
1254
+ this.setProgress(next, 'keyboard');
1255
+ }
1256
+
1257
+ beginDrag(event) {
1258
+ if (event.button !== undefined && event.button !== 0) return;
1259
+ event.preventDefault();
1260
+ // preventDefault 会吞掉点击聚焦,主动聚焦让根节点获得无障碍焦点。
1261
+ try { this.root.focus({ preventScroll: true }); } catch { this.root.focus(); }
1262
+ this.dragging = true;
1263
+ this.pendingPointer = null;
1264
+ this._dragRaf = 0;
1265
+ this.root.classList.add('is-dragging');
1266
+ try { this.root.setPointerCapture?.(event.pointerId); } catch {}
1267
+ this.emitter.emit('dragstart', { value: this.value });
1268
+ this.updateFromPointer(event);
1269
+ }
1270
+
1271
+ moveDrag(event) {
1272
+ if (!this.dragging) return;
1273
+ event.preventDefault();
1274
+ // rAF 合并:一帧最多一次 setProgress/change,视觉仍每帧更新。
1275
+ this.pendingPointer = event;
1276
+ if (this._dragRaf) return;
1277
+ this._dragRaf = requestAnimationFrame(() => {
1278
+ this._dragRaf = 0;
1279
+ const pending = this.pendingPointer;
1280
+ this.pendingPointer = null;
1281
+ if (pending) this.updateFromPointer(pending);
1282
+ });
1283
+ }
1284
+
1285
+ endDrag(event) {
1286
+ if (!this.dragging) return;
1287
+ this.dragging = false;
1288
+ if (this._dragRaf) {
1289
+ cancelAnimationFrame(this._dragRaf);
1290
+ this._dragRaf = 0;
1291
+ }
1292
+ const pending = this.pendingPointer;
1293
+ this.pendingPointer = null;
1294
+ if (pending) this.updateFromPointer(pending);
1295
+ this.root.classList.remove('is-dragging');
1296
+ try {
1297
+ if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
1298
+ this.root.releasePointerCapture(event.pointerId);
1299
+ }
1300
+ } catch {}
1301
+ this.emitter.emit('dragend', { value: this.value });
1302
+ }
1303
+
1304
+ bindEvents() {
1305
+ if (this.options.draggable !== false) {
1306
+ this.handlers.pointerdown = (event) => this.beginDrag(event);
1307
+ this.handlers.pointermove = (event) => this.moveDrag(event);
1308
+ this.handlers.pointerup = (event) => this.endDrag(event);
1309
+ this.handlers.pointercancel = (event) => this.endDrag(event);
1310
+ this.root.addEventListener('pointerdown', this.handlers.pointerdown);
1311
+ this.root.addEventListener('pointermove', this.handlers.pointermove);
1312
+ this.root.addEventListener('pointerup', this.handlers.pointerup);
1313
+ this.root.addEventListener('pointercancel', this.handlers.pointercancel);
1314
+ }
1315
+
1316
+ if (this.options.keyboard !== false) {
1317
+ this.handlers.keydown = (event) => this.onKeyDown(event);
1318
+ this.root.addEventListener('keydown', this.handlers.keydown);
1319
+ }
1320
+
1321
+ }
1322
+
1323
+ update(delta, paused) {
1324
+ if (!paused) this.flowTime += delta;
1325
+ }
1326
+
1327
+ draw() {
1328
+ // Performance fix: when the WebGL overlay is active the 2D layer is
1329
+ // hidden behind it, so drawing it every frame would be wasted CPU.
1330
+ if (this.webglActive) return;
1331
+ this.drawReferenceFlow();
1332
+ }
1333
+
1334
+ randomize() {
1335
+ this.flowTime = this.random() * 40;
1336
+ return this.flowTime;
1337
+ }
1338
+
1339
+ setColors(colors) {
1340
+ if (!Array.isArray(colors) || colors.length !== 4) return;
1341
+ this.preset.colors = [...colors];
1342
+ }
1343
+
1344
+ dispose() {
1345
+ if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
1346
+ if (this.resizeObserver) this.resizeObserver.disconnect();
1347
+ else window.removeEventListener('resize', this.onResize);
1348
+ for (const name of Object.keys(this.handlers)) {
1349
+ const handler = this.handlers[name];
1350
+ this.root.removeEventListener(name, handler);
1351
+ }
1352
+ this.handlers = {};
1353
+ }
1354
+ }
1355
+
1356
+ /**
1357
+ * Mount a fluid progress capsule into `container`.
1358
+ *
1359
+ * Options: preset (literary name), width, height, value/modelValue, min, max,
1360
+ * step, draggable, keyboard, direction, precision/formatValue, colors,
1361
+ * edgeStyle, textRatio (0-100, text region
1362
+ * width in percent), text (HTML string or DOM nodes for the text slot),
1363
+ * colorContent (HTML string or DOM nodes for the color slot),
1364
+ * showValue (show/hide the right-side percentage), quality, renderScale,
1365
+ * powerPreference, fps, paused/static, respectReducedMotion, cssVars.
1366
+ */
1367
+ function createProgressCapsule(container, options = {}) {
1368
+ if (!container || typeof container.appendChild !== 'function') {
1369
+ throw new Error('createProgressCapsule: container element is required');
1370
+ }
1371
+
1372
+ const preset = { ...getPreset('progress', options.preset ?? '星火') };
1373
+ const merged = normalizeOptions(DEFAULTS.progress, preset, options);
1374
+ [merged.min, merged.max] = normalizeRange(merged.min, merged.max);
1375
+ if (merged.modelValue !== undefined) merged.value = merged.modelValue;
1376
+ const normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
1377
+ if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
1378
+ const initialEdgeStyle = merged.edgeStyle === 'tide' || merged.edgeStyle === 'flow'
1379
+ ? merged.edgeStyle
1380
+ : preset.edgeStyle;
1381
+ preset.edgeStyle = initialEdgeStyle;
1382
+ merged.edgeStyle = initialEdgeStyle;
1383
+ merged.colors = [...preset.colors];
1384
+ const optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
1385
+ let colorOverride = optionColors.length === 4 && optionColors.every(Boolean)
1386
+ ? [...optionColors]
1387
+ : null;
1388
+ let edgeStyleOverride = options.edgeStyle === 'flow' || options.edgeStyle === 'tide'
1389
+ ? merged.edgeStyle
1390
+ : null;
1391
+ const copy = COPY;
1392
+ let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
1393
+ // Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
1394
+ // 这里统一按 true 处理。
1395
+ const disabled = merged.disabled === true || merged.disabled === '' || merged.disabled === 'true';
1396
+ const readonly = merged.readonly === true || merged.readonly === '' || merged.readonly === 'true';
1397
+ const locked = disabled || readonly;
1398
+ const effectiveDraggable = locked ? false : merged.draggable !== false;
1399
+ const effectiveKeyboard = locked ? false : merged.keyboard !== false;
1400
+ const dirty = {
1401
+ preset: false,
1402
+ colors: false,
1403
+ textRatio: false,
1404
+ cssVars: false,
1405
+ edgeStyle: false,
1406
+ value: false,
1407
+ step: false,
1408
+ precision: false,
1409
+ formatValue: false,
1410
+ direction: false,
1411
+ showValue: false,
1412
+ valueSuffix: false,
1413
+ quality: false,
1414
+ renderScale: false,
1415
+ paused: false,
1416
+ static: false,
1417
+ fps: false
1418
+ };
1419
+
1420
+ const root = document.createElement('div');
1421
+ root.className = 'hj-capsule-root hj-progress-root';
1422
+ root.setAttribute('role', 'slider');
1423
+ root.setAttribute('data-draggable', String(effectiveDraggable));
1424
+ root.dataset.direction = merged.direction === 'rtl' ? 'rtl' : 'ltr';
1425
+ if (disabled) {
1426
+ root.setAttribute('aria-disabled', 'true');
1427
+ root.classList.add('is-disabled');
1428
+ }
1429
+ if (readonly) {
1430
+ root.setAttribute('aria-readonly', 'true');
1431
+ root.classList.add('is-readonly');
1432
+ }
1433
+ root.setAttribute('tabindex', locked ? '-1' : (effectiveKeyboard ? '0' : '-1'));
1434
+ root.setAttribute('aria-orientation', 'horizontal');
1435
+ root.setAttribute('aria-valuemin', String(merged.min ?? 0));
1436
+ root.setAttribute('aria-valuemax', String(merged.max ?? 100));
1437
+ root.setAttribute('aria-valuenow', String(preset.initialProgress));
1438
+ const updateAria = () => {
1439
+ root.setAttribute(
1440
+ 'aria-label',
1441
+ customLabel || interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
1442
+ );
1443
+ };
1444
+ updateAria();
1445
+ const canvas = document.createElement('canvas');
1446
+ canvas.className = 'hj-progress-canvas';
1447
+ canvas.setAttribute('aria-hidden', 'true');
1448
+
1449
+ // Slot containers are always present but transparent by default; they only
1450
+ // provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
1451
+ const fillContent = (layer, content) => {
1452
+ layer.innerHTML = '';
1453
+ if (content == null) return;
1454
+ if (typeof content === 'string') {
1455
+ layer.innerHTML = content;
1456
+ return;
1457
+ }
1458
+ const nodes = Array.isArray(content) ? content : [content];
1459
+ for (const node of nodes) {
1460
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
1461
+ }
1462
+ };
1463
+
1464
+ const textLayer = document.createElement('div');
1465
+ textLayer.className = 'hj-progress-text hj-progress-copy';
1466
+
1467
+ const visualLayer = document.createElement('div');
1468
+ visualLayer.className = 'hj-progress-visual';
1469
+
1470
+ fillContent(textLayer, options.text);
1471
+ fillContent(visualLayer, options.colorContent);
1472
+
1473
+ const valueElement = document.createElement('div');
1474
+ valueElement.className = 'hj-progress-value';
1475
+ valueElement.setAttribute('aria-hidden', 'true');
1476
+ valueElement.hidden = merged.showValue === false;
1477
+
1478
+ root.appendChild(canvas);
1479
+ root.appendChild(textLayer);
1480
+ root.appendChild(visualLayer);
1481
+ root.appendChild(valueElement);
1482
+ container.appendChild(root);
1483
+
1484
+ const emitter = createEmitter();
1485
+ let manuallyPaused = merged.paused === true;
1486
+ let reducedPaused = false;
1487
+ let staticMode = merged.static === true;
1488
+ let contextLost = false;
1489
+ let disposed = false;
1490
+ let renderOnce = () => {};
1491
+ const onContextLost = () => {
1492
+ contextLost = true;
1493
+ emitter.emit('contextlost', {});
1494
+ };
1495
+ const onContextRestored = () => {
1496
+ contextLost = false;
1497
+ emitter.emit('contextrestored', {});
1498
+ renderOnce();
1499
+ wakeScheduler();
1500
+ };
1501
+
1502
+ const applySize = () => {
1503
+ root.style.width = parseSize(merged.width);
1504
+ root.style.height = parseSize(merged.height);
1505
+ const vars = merged.cssVars || {};
1506
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1507
+ if (merged.textRatio !== undefined) {
1508
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
1509
+ }
1510
+ };
1511
+ applySize();
1512
+
1513
+ const controller = new ProgressCapsuleController({
1514
+ root,
1515
+ canvas,
1516
+ valueElement,
1517
+ preset,
1518
+ emitter,
1519
+ options: {
1520
+ ...merged,
1521
+ draggable: effectiveDraggable,
1522
+ keyboard: effectiveKeyboard,
1523
+ onResize: () => {
1524
+ if (manuallyPaused || reducedPaused || staticMode) renderOnce();
1525
+ }
1526
+ },
1527
+ copy,
1528
+ dirty
1529
+ });
1530
+
1531
+ let overlay = null;
1532
+ if (merged.renderer !== 'canvas2d') {
1533
+ overlay = attachProgressFlowOverlay({
1534
+ root,
1535
+ canvas,
1536
+ preset,
1537
+ getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
1538
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
1539
+ powerPreference: merged.powerPreference,
1540
+ onContextLost,
1541
+ onContextRestored
1542
+ });
1543
+ }
1544
+ if (overlay) controller.webglActive = true;
1545
+ else if (merged.renderer !== 'canvas2d') {
1546
+ nextTick(() => {
1547
+ if (disposed) return;
1548
+ emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
1549
+ });
1550
+ }
1551
+
1552
+ const visibility = createVisibilityGuard(root, wakeScheduler);
1553
+ const motionPreference = createReducedMotionPreference(
1554
+ merged.respectReducedMotion,
1555
+ (matches) => {
1556
+ reducedPaused = matches;
1557
+ if (matches) renderOnce();
1558
+ else wakeScheduler();
1559
+ }
1560
+ );
1561
+ reducedPaused = motionPreference.matches();
1562
+ const isMotionPaused = () => manuallyPaused || reducedPaused || staticMode || contextLost;
1563
+ let flowTime = 0;
1564
+ const frameGate = createFrameGate(merged.fps);
1565
+ renderOnce = () => {
1566
+ controller.draw();
1567
+ if (overlay) overlay.update(flowTime);
1568
+ };
1569
+ renderOnce();
1570
+ const offPausedChange = emitter.on('change', () => {
1571
+ if (isMotionPaused()) renderOnce();
1572
+ });
1573
+ const unsubscribe = subscribeScheduler(
1574
+ (delta, now) => {
1575
+ flowTime += delta;
1576
+ controller.update(delta, false);
1577
+ if (frameGate.shouldDraw(delta)) {
1578
+ controller.draw();
1579
+ if (overlay) overlay.update(flowTime);
1580
+ }
1581
+ },
1582
+ () => isMotionPaused() || !visibility.isVisible()
1583
+ );
1584
+
1585
+ nextTick(() => {
1586
+ if (disposed) return;
1587
+ emitter.emit('ready', { preset: { ...preset } });
1588
+ });
1589
+
1590
+ const syncDom = () => {
1591
+ updateAria();
1592
+ };
1593
+
1594
+ return {
1595
+ element: root,
1596
+ canvas,
1597
+ preset,
1598
+ on: emitter.on,
1599
+ off: emitter.off,
1600
+ setValue(value, source = 'prop') {
1601
+ controller.setProgress(value, source);
1602
+ return this;
1603
+ },
1604
+ getValue() {
1605
+ return controller.value;
1606
+ },
1607
+ setRange(min, max) {
1608
+ controller.setRange(min, max);
1609
+ if (isMotionPaused()) renderOnce();
1610
+ return this;
1611
+ },
1612
+ setPreset(ref) {
1613
+ const next = getPreset('progress', ref);
1614
+ dirty.preset = true;
1615
+ Object.assign(preset, next);
1616
+ if (colorOverride) preset.colors = [...colorOverride];
1617
+ if (edgeStyleOverride) preset.edgeStyle = edgeStyleOverride;
1618
+ const nextColors = preset.colors.map(normalizeColor);
1619
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
1620
+ controller.preset = preset;
1621
+ controller.profile = preset.edgeStyle === 'tide'
1622
+ ? FLOW_PROFILES.tide
1623
+ : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
1624
+ controller.flowTime = stringSeed(next.id) * 31;
1625
+ controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
1626
+ syncDom();
1627
+ if (overlay) {
1628
+ overlay.dispose();
1629
+ contextLost = false;
1630
+ overlay = attachProgressFlowOverlay({
1631
+ root,
1632
+ canvas,
1633
+ preset,
1634
+ getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
1635
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
1636
+ powerPreference: merged.powerPreference,
1637
+ onContextLost,
1638
+ onContextRestored
1639
+ });
1640
+ }
1641
+ controller.webglActive = Boolean(overlay);
1642
+ controller.resizeCanvas();
1643
+ if (isMotionPaused()) renderOnce();
1644
+ emitter.emit('presetchange', { preset: { ...preset } });
1645
+ return this;
1646
+ },
1647
+ setEdgeStyle(edgeStyle) {
1648
+ const value = String(edgeStyle || '').toLowerCase();
1649
+ if (value !== 'flow' && value !== 'tide') return this;
1650
+ dirty.edgeStyle = true;
1651
+ edgeStyleOverride = value;
1652
+ preset.edgeStyle = value;
1653
+ controller.profile = value === 'tide'
1654
+ ? FLOW_PROFILES.tide
1655
+ : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
1656
+ if (overlay) {
1657
+ overlay.dispose();
1658
+ contextLost = false;
1659
+ overlay = attachProgressFlowOverlay({
1660
+ root,
1661
+ canvas,
1662
+ preset,
1663
+ getProgress: () => progressRatio(controller.value, controller.min, controller.max) * 100,
1664
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
1665
+ powerPreference: merged.powerPreference,
1666
+ onContextLost,
1667
+ onContextRestored
1668
+ });
1669
+ }
1670
+ controller.webglActive = Boolean(overlay);
1671
+ controller.resizeCanvas();
1672
+ if (isMotionPaused()) renderOnce();
1673
+ return this;
1674
+ },
1675
+ setText(content) {
1676
+ fillContent(textLayer, content);
1677
+ return this;
1678
+ },
1679
+ setColorContent(content) {
1680
+ fillContent(visualLayer, content);
1681
+ return this;
1682
+ },
1683
+ setTextRatio(ratio) {
1684
+ const value = Number(ratio);
1685
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
1686
+ dirty.textRatio = true;
1687
+ merged.textRatio = value;
1688
+ root.style.setProperty('--hj-text-width', `${value}%`);
1689
+ return this;
1690
+ },
1691
+ setCssVars(vars) {
1692
+ if (!vars || typeof vars !== 'object') return this;
1693
+ dirty.cssVars = true;
1694
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
1695
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1696
+ return this;
1697
+ },
1698
+ setLabel(label) {
1699
+ customLabel = typeof label === 'string' && label ? label : null;
1700
+ updateAria();
1701
+ return this;
1702
+ },
1703
+ setValueSuffix(suffix) {
1704
+ controller.setValueSuffix(suffix);
1705
+ return this;
1706
+ },
1707
+ setStep(step) {
1708
+ controller.setStep(step);
1709
+ return this;
1710
+ },
1711
+ setPrecision(precision) {
1712
+ controller.setPrecision(precision);
1713
+ return this;
1714
+ },
1715
+ setFormatValue(formatter) {
1716
+ controller.setFormatValue(formatter);
1717
+ return this;
1718
+ },
1719
+ setDirection(direction) {
1720
+ controller.setDirection(direction);
1721
+ return this;
1722
+ },
1723
+ setShowValue(show) {
1724
+ controller.setShowValue(show);
1725
+ return this;
1726
+ },
1727
+ setColors(colors) {
1728
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
1729
+ if (next.length !== 4 || next.some((color) => !color)) return this;
1730
+ dirty.colors = true;
1731
+ colorOverride = [...next];
1732
+ preset.colors = [...next];
1733
+ controller.setColors(next);
1734
+ if (overlay) overlay.setColors(next);
1735
+ controller.resizeCanvas();
1736
+ if (isMotionPaused()) renderOnce();
1737
+ return this;
1738
+ },
1739
+ setSize(width, height) {
1740
+ if (width !== undefined) {
1741
+ parseSize(width);
1742
+ merged.width = width;
1743
+ }
1744
+ if (height !== undefined) {
1745
+ parseSize(height);
1746
+ merged.height = height;
1747
+ }
1748
+ applySize();
1749
+ controller.resizeCanvas();
1750
+ if (isMotionPaused()) renderOnce();
1751
+ return this;
1752
+ },
1753
+ randomize() {
1754
+ flowTime = controller.randomize();
1755
+ if (isMotionPaused()) renderOnce();
1756
+ return this;
1757
+ },
1758
+ pause() {
1759
+ dirty.paused = true;
1760
+ manuallyPaused = true;
1761
+ renderOnce();
1762
+ return this;
1763
+ },
1764
+ resume() {
1765
+ dirty.paused = true;
1766
+ manuallyPaused = false;
1767
+ wakeScheduler();
1768
+ return this;
1769
+ },
1770
+ setPaused(value) {
1771
+ return value ? this.pause() : this.resume();
1772
+ },
1773
+ setStatic(value) {
1774
+ dirty.static = true;
1775
+ staticMode = value === true;
1776
+ merged.static = staticMode;
1777
+ if (staticMode) renderOnce();
1778
+ else wakeScheduler();
1779
+ return this;
1780
+ },
1781
+ setFps(fps) {
1782
+ dirty.fps = true;
1783
+ merged.fps = frameGate.setFps(fps);
1784
+ wakeScheduler();
1785
+ return this;
1786
+ },
1787
+ setQuality(quality) {
1788
+ dirty.quality = true;
1789
+ merged.quality = quality;
1790
+ controller.dprCap = effectiveDprCap(quality, merged.renderScale);
1791
+ controller.resizeCanvas();
1792
+ if (overlay) overlay.setDprCap(controller.dprCap);
1793
+ if (isMotionPaused()) renderOnce();
1794
+ return this;
1795
+ },
1796
+ setRenderScale(renderScale) {
1797
+ const value = Number(renderScale);
1798
+ if (!Number.isFinite(value)) return this;
1799
+ dirty.renderScale = true;
1800
+ merged.renderScale = Math.min(1, Math.max(0.25, value));
1801
+ controller.dprCap = effectiveDprCap(merged.quality, merged.renderScale);
1802
+ controller.resizeCanvas();
1803
+ if (overlay) overlay.setDprCap(controller.dprCap);
1804
+ if (isMotionPaused()) renderOnce();
1805
+ return this;
1806
+ },
1807
+ dispose() {
1808
+ disposed = true;
1809
+ unsubscribe();
1810
+ offPausedChange();
1811
+ visibility.dispose();
1812
+ motionPreference.dispose();
1813
+ if (overlay) overlay.dispose();
1814
+ controller.dispose();
1815
+ root.remove();
1816
+ },
1817
+ get textRatio() { return merged.textRatio; },
1818
+ get cssVars() { return merged.cssVars; },
1819
+ get min() { return controller.min; },
1820
+ get max() { return controller.max; },
1821
+ get step() { return controller.step; },
1822
+ get precision() { return controller.precision; },
1823
+ get formatValue() { return controller.formatValue; },
1824
+ get direction() { return controller.direction; },
1825
+ get showValue() { return !valueElement.hidden; },
1826
+ get valueSuffix() { return controller.valueSuffix; },
1827
+ get quality() { return merged.quality; },
1828
+ get renderScale() { return merged.renderScale; },
1829
+ get paused() { return manuallyPaused; },
1830
+ get static() { return staticMode; },
1831
+ get fps() { return frameGate.getFps(); },
1832
+ dirty
1833
+ };
1834
+ }
1835
+
1836
+ export { ProgressCapsuleController as P, ProgressFlowRenderer as a, createProgressCapsule as c };