@5even7/dlc-ui 0.1.0

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/dist/vue3.mjs ADDED
@@ -0,0 +1,2428 @@
1
+ import { h } from 'vue';
2
+
3
+ /**
4
+ * Tiny event emitter. Accepts any event name so the API stays open for
5
+ * future events (hover, click, custom) without breaking changes.
6
+ */
7
+ function createEmitter() {
8
+ // Plain object storage (no Map/Set) so the legacy build has no API
9
+ // dependencies beyond what IE11 provides.
10
+ const listeners = Object.create(null);
11
+
12
+ return {
13
+ on(event, fn) {
14
+ if (!listeners[event]) listeners[event] = [];
15
+ listeners[event].push(fn);
16
+ return () => {
17
+ const current = listeners[event];
18
+ if (current) {
19
+ const index = current.indexOf(fn);
20
+ if (index !== -1) current.splice(index, 1);
21
+ }
22
+ };
23
+ },
24
+ off(event, fn) {
25
+ const current = listeners[event];
26
+ if (!current) return false;
27
+ const index = current.indexOf(fn);
28
+ if (index === -1) return false;
29
+ current.splice(index, 1);
30
+ return true;
31
+ },
32
+ emit(event, ...args) {
33
+ const current = listeners[event];
34
+ if (!current) return;
35
+ for (const fn of current.slice()) fn(...args);
36
+ }
37
+ };
38
+ }
39
+
40
+ const UNIT_PATTERN = /^[\d.]+(?:px|%|vw|vh|vmin|vmax|em|rem)$/;
41
+
42
+ /**
43
+ * Normalize a size option to a CSS length string.
44
+ * Accepts numbers (treated as px) or strings with px/%, vw/vh/vmin/vmax, em/rem.
45
+ */
46
+ function parseSize(value) {
47
+ if (typeof value === 'number') {
48
+ if (!Number.isFinite(value) || value < 0) throw new Error(`Invalid size: ${value}`);
49
+ return `${value}px`;
50
+ }
51
+ if (typeof value !== 'string') throw new Error(`Invalid size: ${String(value)}`);
52
+ const trimmed = value.trim();
53
+ if (!trimmed) throw new Error('Invalid size: empty string');
54
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) return `${trimmed}px`;
55
+ if (!UNIT_PATTERN.test(trimmed)) {
56
+ throw new Error(`Invalid size or unsupported unit: "${value}" (use px, %, vw, vh, vmin, vmax, em, rem)`);
57
+ }
58
+ return trimmed;
59
+ }
60
+
61
+ const QUALITY_TIERS = {
62
+ low: { dpr: 1 },
63
+ medium: { dpr: 1.5 },
64
+ high: { dpr: 2 }
65
+ };
66
+
67
+ function autoDprCap() {
68
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
69
+ const memory = nav && typeof nav.deviceMemory === 'number' ? nav.deviceMemory : 8;
70
+ const cores = nav && typeof nav.hardwareConcurrency === 'number' ? nav.hardwareConcurrency : 8;
71
+ return memory <= 4 || cores <= 4 ? 1.5 : 2;
72
+ }
73
+
74
+ function dprCapFor(quality) {
75
+ if (quality === 'auto') return autoDprCap();
76
+ const tier = QUALITY_TIERS[quality] || QUALITY_TIERS.medium;
77
+ return tier.dpr;
78
+ }
79
+
80
+ /**
81
+ * Merge defaults < preset < user. Unknown user keys are preserved so the
82
+ * component API can grow (new props, cssVars, callbacks) without a breaking
83
+ * change.
84
+ */
85
+ function normalizeOptions(defaults, preset, user) {
86
+ // undefined means "not provided" (e.g. Vue $props with unset props):
87
+ // drop those keys before merging so defaults/preset values survive.
88
+ const omitUndefined = (source) => {
89
+ const result = {};
90
+ for (const key of Object.keys(source)) {
91
+ if (source[key] !== undefined) result[key] = source[key];
92
+ }
93
+ return result;
94
+ };
95
+ const presetOptions = omitUndefined(preset && typeof preset === 'object' ? preset : {});
96
+ const userOptions = omitUndefined(user && typeof user === 'object' ? user : {});
97
+ return { ...defaults, ...presetOptions, ...userOptions };
98
+ }
99
+
100
+ /**
101
+ * 公共色板:NC-01~NC-06 的四色组(底色 / 主色 / 辅色 / 高光色)。
102
+ * Capsule 预置与 dlc-color 等组件共用,新增色板只改这里。
103
+ */
104
+ const PALETTES$1 = {
105
+ original: ['#FFF3EA', '#F5B27A', '#F67BC6', '#A978E8'],
106
+ ocean: ['#EAF6FF', '#8FD0FF', '#3B87F6', '#6B58E9'],
107
+ klein: ['#EDF2FF', '#2F58D5', '#1B2040', '#E07A43'],
108
+ ultraviolet: ['#F2EEFF', '#B99AF1', '#8F74DB', '#D7D85C'],
109
+ chrome: ['#F5F6F8', '#B9C0CC', '#7F8793', '#4A4F59'],
110
+ plus: ['#FFF0E6', '#F6C26B', '#F98A64', '#E86D74']
111
+ };
112
+
113
+ /**
114
+ * Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
115
+ * 颜色引用公共色板,seed/speed 决定形态与流速。
116
+ */
117
+ const CAPSULE_PRESETS = [
118
+ { id: 'original', code: 'NC-01', name: 'ORIGINAL', group: 'warm', seed: 1.7, speed: 0.5, colors: [...PALETTES$1.original] },
119
+ { id: 'ocean', code: 'NC-02', name: 'OCEAN', group: 'cold', seed: 8.2, speed: 0.48, colors: [...PALETTES$1.ocean] },
120
+ { id: 'klein', code: 'NC-03', name: 'KLEIN', group: 'cold', seed: 14.1, speed: 0.49, colors: [...PALETTES$1.klein] },
121
+ { id: 'ultraviolet', code: 'NC-04', name: 'ULTRAVIOLET', group: 'cold', seed: 23.4, speed: 0.47, colors: [...PALETTES$1.ultraviolet] },
122
+ { id: 'chrome', code: 'NC-05', name: 'CHROME', group: 'cold', seed: 37.8, speed: 0.42, colors: [...PALETTES$1.chrome] },
123
+ { id: 'plus', code: 'NC-06', name: 'PLUS', group: 'warm', seed: 51.3, speed: 0.5, colors: [...PALETTES$1.plus] }
124
+ ];
125
+
126
+ const PROGRESS_PRESETS = [
127
+ {
128
+ id: 'model-training',
129
+ code: 'NC-10',
130
+ name: 'MODEL TRAINING',
131
+ subtitle: 'SHA 4.5 + 100 2026 TKN',
132
+ group: 'progress',
133
+ initialProgress: 43,
134
+ edgeStyle: 'flow',
135
+ colors: ['#2B1025', '#FF3F94', '#FF8A3D', '#FFF06A']
136
+ },
137
+ {
138
+ id: 'agent-migration',
139
+ code: 'NC-11',
140
+ name: 'AGENT MIGRATION',
141
+ subtitle: 'TRANSFERRING PROTOCOL',
142
+ group: 'progress',
143
+ initialProgress: 33,
144
+ edgeStyle: 'flow',
145
+ colors: ['#101C37', '#245BFF', '#00CFFF', '#5DFFE6']
146
+ },
147
+ {
148
+ id: 'visual-training',
149
+ code: 'NC-12',
150
+ name: 'VISUAL TRAINING',
151
+ subtitle: 'GENERATING POWER ++',
152
+ group: 'progress',
153
+ initialProgress: 58,
154
+ edgeStyle: 'flow',
155
+ colors: ['#21142D', '#7042FF', '#42F58D', '#C4FF8A']
156
+ },
157
+ {
158
+ id: 'tide',
159
+ code: 'NC-13',
160
+ name: 'TIDE',
161
+ subtitle: 'MOON PULL / COAST',
162
+ group: 'progress',
163
+ initialProgress: 30,
164
+ edgeStyle: 'tide',
165
+ colors: ['#0A2239', '#2E9BFF', '#7FE3FF', '#EAF9FF']
166
+ }
167
+ ];
168
+
169
+ [...CAPSULE_PRESETS, ...PROGRESS_PRESETS];
170
+
171
+ function validatePreset(preset) {
172
+ return Boolean(
173
+ preset &&
174
+ typeof preset.id === 'string' &&
175
+ typeof preset.code === 'string' &&
176
+ typeof preset.name === 'string' &&
177
+ Number.isFinite(preset.seed) &&
178
+ Number.isFinite(preset.speed) &&
179
+ Array.isArray(preset.colors) &&
180
+ preset.colors.length === 4
181
+ );
182
+ }
183
+
184
+ function validateProgressPreset(preset) {
185
+ return Boolean(
186
+ preset &&
187
+ typeof preset.id === 'string' &&
188
+ typeof preset.code === 'string' &&
189
+ typeof preset.name === 'string' &&
190
+ preset.group === 'progress' &&
191
+ Number.isFinite(preset.initialProgress) &&
192
+ ['flow', 'tide'].indexOf(preset.edgeStyle || 'flow') !== -1 &&
193
+ Array.isArray(preset.colors) &&
194
+ preset.colors.length === 4
195
+ );
196
+ }
197
+
198
+ function getPreset(kind, ref) {
199
+ const list = kind === 'capsule' ? CAPSULE_PRESETS : kind === 'progress' ? PROGRESS_PRESETS : null;
200
+ if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
201
+ if (ref && typeof ref === 'object') {
202
+ const valid = kind === 'capsule' ? validatePreset(ref) : validateProgressPreset(ref);
203
+ if (!valid) throw new Error(`Invalid ${kind} preset object`);
204
+ return ref;
205
+ }
206
+ const key = String(ref).trim().toLowerCase();
207
+ const found = list.find(
208
+ (preset) =>
209
+ preset.id.toLowerCase() === key ||
210
+ preset.code.toLowerCase() === key ||
211
+ preset.name.toLowerCase() === key
212
+ );
213
+ if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
214
+ return found;
215
+ }
216
+
217
+ const DEFAULTS = {
218
+ capsule: {
219
+ width: '100%',
220
+ height: 160,
221
+ quality: 'auto',
222
+ renderer: 'auto',
223
+ respectReducedMotion: true,
224
+ interactive: true,
225
+ mouseColor: true,
226
+ showCopy: true
227
+ },
228
+ progress: {
229
+ width: 454,
230
+ height: 104,
231
+ min: 0,
232
+ max: 100,
233
+ draggable: true,
234
+ keyboard: true,
235
+ edgeStyle: 'flow',
236
+ quality: 'auto',
237
+ renderer: 'auto',
238
+ showCopy: true,
239
+ respectReducedMotion: true
240
+ }
241
+ };
242
+
243
+ /**
244
+ * All user-facing copy lives here so wording/brand changes never require a
245
+ * global search. Templates use {brand} {code} {name} placeholders.
246
+ */
247
+ const COPY = {
248
+ brandName: '画境观屿',
249
+ dragLabel: 'DRAG',
250
+ valueSuffix: '%',
251
+ progressAria: '{brand} {code} 加载进度',
252
+ capsuleAria: '打开 {name} 沉浸预览'
253
+ };
254
+
255
+ function hexToRgb01$1(hex) {
256
+ const normalized = hex.replace('#', '');
257
+ const value = Number.parseInt(normalized, 16);
258
+ return [
259
+ ((value >> 16) & 255) / 255,
260
+ ((value >> 8) & 255) / 255,
261
+ (value & 255) / 255
262
+ ];
263
+ }
264
+
265
+ function hexToRgba(hex, alpha = 1) {
266
+ const normalized = hex.replace('#', '');
267
+ const value = Number.parseInt(normalized, 16);
268
+ const red = (value >> 16) & 255;
269
+ const green = (value >> 8) & 255;
270
+ const blue = value & 255;
271
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
272
+ }
273
+
274
+ const VERTEX_SHADER$1 = `#version 300 es
275
+ in vec2 a_position;
276
+ out vec2 v_uv;
277
+ void main() {
278
+ v_uv = a_position * 0.5 + 0.5;
279
+ gl_Position = vec4(a_position, 0.0, 1.0);
280
+ }`;
281
+
282
+ const FRAGMENT_SHADER$1 = `#version 300 es
283
+ precision highp float;
284
+
285
+ in vec2 v_uv;
286
+ out vec4 outColor;
287
+
288
+ uniform vec2 u_resolution;
289
+ uniform float u_time;
290
+ uniform float u_seed;
291
+ uniform float u_motion;
292
+ uniform vec2 u_pointer;
293
+ uniform vec3 u_colorA;
294
+ uniform vec3 u_colorB;
295
+ uniform vec3 u_colorC;
296
+ uniform vec3 u_colorD;
297
+
298
+ float hash21(vec2 p) {
299
+ p = fract(p * vec2(123.34, 456.21));
300
+ p += dot(p, p + 45.32 + u_seed);
301
+ return fract(p.x * p.y);
302
+ }
303
+
304
+ float noise(vec2 p) {
305
+ vec2 i = floor(p);
306
+ vec2 f = fract(p);
307
+ f = f * f * (3.0 - 2.0 * f);
308
+ float a = hash21(i);
309
+ float b = hash21(i + vec2(1.0, 0.0));
310
+ float c = hash21(i + vec2(0.0, 1.0));
311
+ float d = hash21(i + vec2(1.0, 1.0));
312
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
313
+ }
314
+
315
+ float fbm(vec2 p) {
316
+ float value = 0.0;
317
+ float amplitude = 0.52;
318
+ mat2 rotation = mat2(0.80, 0.60, -0.60, 0.80);
319
+ for (int i = 0; i < 6; i++) {
320
+ value += amplitude * noise(p);
321
+ p = rotation * p * 2.03 + 17.7;
322
+ amplitude *= 0.5;
323
+ }
324
+ return value;
325
+ }
326
+
327
+ float gaussian(float value, float center, float width) {
328
+ return exp(-pow(value - center, 2.0) / max(width, 0.0001));
329
+ }
330
+
331
+ vec3 palette(float t) {
332
+ t = clamp(t, 0.0, 1.0);
333
+ vec3 shadow = mix(u_colorA, u_colorB, smoothstep(0.06, 0.62, t));
334
+ vec3 body = mix(u_colorB, u_colorC, smoothstep(0.30, 0.82, t));
335
+ vec3 highlight = mix(u_colorC, u_colorD, smoothstep(0.74, 1.0, t));
336
+ vec3 restrained = mix(shadow, body, smoothstep(0.26, 0.72, t));
337
+ return mix(restrained, highlight, smoothstep(0.78, 0.97, t));
338
+ }
339
+
340
+ vec3 renderNebula(vec2 uv, vec2 p, vec2 pointer, float distanceToPointer, float t) {
341
+ vec2 delta = p - pointer;
342
+ float influence = exp(-distanceToPointer * 4.6) * u_motion;
343
+ float angle = influence * 1.7;
344
+ mat2 swirl = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
345
+ p = pointer + swirl * delta;
346
+ p += normalize(delta + 0.0001) * influence * 0.08;
347
+
348
+ vec2 drift = vec2(t * 0.22, -t * 0.13);
349
+ vec2 q = vec2(
350
+ fbm(p * 1.35 + drift + u_seed),
351
+ fbm(p * 1.35 + vec2(5.2, 1.3) - drift * 0.85)
352
+ );
353
+ vec2 r = vec2(
354
+ fbm(p * 2.0 + 3.6 * q + vec2(1.7, 9.2) + t * 0.10),
355
+ fbm(p * 2.0 + 3.0 * q + vec2(8.3, 2.8) - t * 0.085)
356
+ );
357
+
358
+ float cloud = fbm(p * 1.7 + 4.2 * r);
359
+ float veins = fbm(p * 4.0 - 2.0 * q + t * 0.065);
360
+ float nebula = smoothstep(0.18, 0.91, cloud * 0.9 + veins * 0.22);
361
+
362
+ vec3 color = palette(nebula);
363
+ color += u_colorD * pow(max(cloud - 0.63, 0.0), 2.0) * 1.05;
364
+ color *= 0.78 + 0.34 * smoothstep(0.15, 0.9, veins);
365
+
366
+ vec2 starGrid = floor((uv + vec2(u_seed * 0.013, 0.0)) * vec2(132.0, 58.0));
367
+ vec2 starCell = fract(uv * vec2(132.0, 58.0)) - 0.5;
368
+ float starRandom = hash21(starGrid);
369
+ float starShape = smoothstep(0.075, 0.0, length(starCell));
370
+ float starMask = step(0.989, starRandom) * starShape;
371
+ float twinkle = 0.35 + 0.65 * sin(t * (1.0 + starRandom * 2.4) + starRandom * 40.0) * 0.5 + 0.5;
372
+ color += starMask * twinkle * mix(u_colorC, u_colorD, starRandom) * 1.05;
373
+
374
+ float pointerGlow = exp(-distanceToPointer * 7.0) * u_motion;
375
+ color += u_colorD * pointerGlow * 0.28;
376
+ return color;
377
+ }
378
+
379
+ void main() {
380
+ vec2 uv = v_uv;
381
+ vec2 p = uv - 0.5;
382
+ p.x *= u_resolution.x / max(u_resolution.y, 1.0);
383
+
384
+ vec2 pointer = u_pointer - 0.5;
385
+ pointer.x *= u_resolution.x / max(u_resolution.y, 1.0);
386
+ float distanceToPointer = length(p - pointer);
387
+
388
+ vec3 color = renderNebula(uv, p, pointer, distanceToPointer, u_time);
389
+ float vignette = smoothstep(0.94, 0.18, length((uv - 0.5) * vec2(1.0, 1.35)));
390
+ color *= 0.70 + vignette * 0.42;
391
+ color = pow(max(color, vec3(0.0)), vec3(0.88));
392
+
393
+ outColor = vec4(color, 1.0);
394
+ }`;
395
+
396
+ function compileShader$1(gl, type, source) {
397
+ const shader = gl.createShader(type);
398
+ gl.shaderSource(shader, source);
399
+ gl.compileShader(shader);
400
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
401
+ const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
402
+ gl.deleteShader(shader);
403
+ throw new Error(message);
404
+ }
405
+ return shader;
406
+ }
407
+
408
+ function createProgram$1(gl) {
409
+ const vertex = compileShader$1(gl, gl.VERTEX_SHADER, VERTEX_SHADER$1);
410
+ const fragment = compileShader$1(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER$1);
411
+ const program = gl.createProgram();
412
+ gl.attachShader(program, vertex);
413
+ gl.attachShader(program, fragment);
414
+ gl.linkProgram(program);
415
+ gl.deleteShader(vertex);
416
+ gl.deleteShader(fragment);
417
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
418
+ const message = gl.getProgramInfoLog(program) || 'Unknown program link error';
419
+ gl.deleteProgram(program);
420
+ throw new Error(message);
421
+ }
422
+ return program;
423
+ }
424
+
425
+ class CosmicRenderer {
426
+ constructor(canvas, preset, options = {}) {
427
+ this.canvas = canvas;
428
+ this.preset = { ...preset };
429
+ this.options = { dprCap: 1.75, mouseColor: true, ...options };
430
+ this.gl = canvas.getContext('webgl2', {
431
+ alpha: false,
432
+ antialias: false,
433
+ depth: false,
434
+ powerPreference: 'high-performance',
435
+ preserveDrawingBuffer: false
436
+ });
437
+ if (!this.gl) throw new Error('WebGL2 is not available');
438
+
439
+ this.program = createProgram$1(this.gl);
440
+ this.locations = this.#getLocations();
441
+ this.pointer = [0.72, 0.45];
442
+ this.pointerTarget = [...this.pointer];
443
+ this.motion = 0;
444
+ this.motionTarget = 0;
445
+ this.timeOffset = preset.seed * 0.73;
446
+ this.visible = true;
447
+ this.disposed = false;
448
+
449
+ this.#setupGeometry();
450
+ this.#bindEvents();
451
+ this.resize();
452
+ }
453
+
454
+ #getLocations() {
455
+ const gl = this.gl;
456
+ const uniform = (name) => gl.getUniformLocation(this.program, name);
457
+ return {
458
+ position: gl.getAttribLocation(this.program, 'a_position'),
459
+ resolution: uniform('u_resolution'),
460
+ time: uniform('u_time'),
461
+ seed: uniform('u_seed'),
462
+ motion: uniform('u_motion'),
463
+ pointer: uniform('u_pointer'),
464
+ colorA: uniform('u_colorA'),
465
+ colorB: uniform('u_colorB'),
466
+ colorC: uniform('u_colorC'),
467
+ colorD: uniform('u_colorD')
468
+ };
469
+ }
470
+
471
+ #setupGeometry() {
472
+ const gl = this.gl;
473
+ const vertices = new Float32Array([-1, -1, 3, -1, -1, 3]);
474
+ this.buffer = gl.createBuffer();
475
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
476
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
477
+ }
478
+
479
+ #bindEvents() {
480
+ if (this.options.mouseColor === false) return;
481
+ this.eventTarget = this.options.eventTarget || this.canvas.parentElement || this.canvas;
482
+ this.onPointerMove = (event) => {
483
+ const rect = this.canvas.getBoundingClientRect();
484
+ this.pointerTarget[0] = (event.clientX - rect.left) / Math.max(rect.width, 1);
485
+ this.pointerTarget[1] = 1 - (event.clientY - rect.top) / Math.max(rect.height, 1);
486
+ this.motionTarget = 1;
487
+ };
488
+ this.onPointerLeave = () => { this.motionTarget = 0; };
489
+ this.eventTarget.addEventListener('pointermove', this.onPointerMove, { passive: true });
490
+ this.eventTarget.addEventListener('pointerdown', this.onPointerMove, { passive: true });
491
+ this.eventTarget.addEventListener('pointerleave', this.onPointerLeave, { passive: true });
492
+ }
493
+
494
+ setPreset(preset) {
495
+ this.preset = { ...preset };
496
+ this.timeOffset = preset.seed * 0.73;
497
+ }
498
+
499
+ setDprCap(cap) {
500
+ this.options.dprCap = cap;
501
+ this.resize();
502
+ }
503
+
504
+ randomize() {
505
+ this.preset.seed = Math.random() * 100;
506
+ this.timeOffset = Math.random() * 40;
507
+ }
508
+
509
+ resize() {
510
+ const dpr = Math.min(window.devicePixelRatio || 1, this.options.dprCap);
511
+ const rect = this.canvas.getBoundingClientRect();
512
+ const width = Math.max(2, Math.round(rect.width * dpr));
513
+ const height = Math.max(2, Math.round(rect.height * dpr));
514
+ if (this.canvas.width !== width || this.canvas.height !== height) {
515
+ this.canvas.width = width;
516
+ this.canvas.height = height;
517
+ this.gl.viewport(0, 0, width, height);
518
+ }
519
+ }
520
+
521
+ draw(elapsedSeconds, paused = false) {
522
+ if (this.disposed || !this.visible) return;
523
+ this.resize();
524
+ const gl = this.gl;
525
+ this.pointer[0] += (this.pointerTarget[0] - this.pointer[0]) * 0.08;
526
+ this.pointer[1] += (this.pointerTarget[1] - this.pointer[1]) * 0.08;
527
+ this.motion += (this.motionTarget - this.motion) * 0.07;
528
+
529
+ gl.useProgram(this.program);
530
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
531
+ gl.enableVertexAttribArray(this.locations.position);
532
+ gl.vertexAttribPointer(this.locations.position, 2, gl.FLOAT, false, 0, 0);
533
+
534
+ const colors = this.preset.colors.map(hexToRgb01$1);
535
+ gl.uniform2f(this.locations.resolution, this.canvas.width, this.canvas.height);
536
+ gl.uniform1f(this.locations.time, this.timeOffset + (paused ? 0 : elapsedSeconds * this.preset.speed));
537
+ gl.uniform1f(this.locations.seed, this.preset.seed);
538
+ gl.uniform1f(this.locations.motion, this.motion);
539
+ gl.uniform2f(this.locations.pointer, this.pointer[0], this.pointer[1]);
540
+ gl.uniform3fv(this.locations.colorA, colors[0]);
541
+ gl.uniform3fv(this.locations.colorB, colors[1]);
542
+ gl.uniform3fv(this.locations.colorC, colors[2]);
543
+ gl.uniform3fv(this.locations.colorD, colors[3]);
544
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
545
+ }
546
+
547
+ dispose() {
548
+ this.disposed = true;
549
+ const target = this.eventTarget || this.canvas;
550
+ target.removeEventListener('pointermove', this.onPointerMove);
551
+ target.removeEventListener('pointerdown', this.onPointerMove);
552
+ target.removeEventListener('pointerleave', this.onPointerLeave);
553
+ this.gl.deleteBuffer(this.buffer);
554
+ this.gl.deleteProgram(this.program);
555
+ const lose = this.gl.getExtension('WEBGL_lose_context');
556
+ if (lose) lose.loseContext();
557
+ }
558
+ }
559
+
560
+ function rgb(color, alpha = 1) {
561
+ const [r, g, b] = hexToRgb01$1(color).map((value) => Math.round(value * 255));
562
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
563
+ }
564
+
565
+ class FallbackRenderer {
566
+ constructor(canvas, preset) {
567
+ this.canvas = canvas;
568
+ this.preset = preset;
569
+ this.context = canvas.getContext('2d');
570
+ this.visible = true;
571
+ }
572
+
573
+ resize() {
574
+ const rect = this.canvas.getBoundingClientRect();
575
+ const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
576
+ const width = Math.max(2, Math.round(rect.width * dpr));
577
+ const height = Math.max(2, Math.round(rect.height * dpr));
578
+ if (this.canvas.width !== width || this.canvas.height !== height) {
579
+ this.canvas.width = width;
580
+ this.canvas.height = height;
581
+ }
582
+ }
583
+
584
+ drawNebula(time) {
585
+ const ctx = this.context;
586
+ const { width, height } = this.canvas;
587
+ const gradient = ctx.createLinearGradient(0, 0, width, height);
588
+ gradient.addColorStop(0, this.preset.colors[0]);
589
+ gradient.addColorStop(0.38, this.preset.colors[1]);
590
+ gradient.addColorStop(0.72, this.preset.colors[2]);
591
+ gradient.addColorStop(1, this.preset.colors[3]);
592
+ ctx.fillStyle = gradient;
593
+ ctx.fillRect(0, 0, width, height);
594
+
595
+ ctx.globalCompositeOperation = 'screen';
596
+ for (let index = 0; index < 6; index += 1) {
597
+ const x = (0.5 + 0.45 * Math.sin(time * 0.32 + index * 1.7)) * width;
598
+ const y = (0.5 + 0.4 * Math.cos(time * 0.25 + index)) * height;
599
+ const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
600
+ const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
601
+ glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
602
+ glow.addColorStop(1, rgb(this.preset.colors[index % 4], 0));
603
+ ctx.fillStyle = glow;
604
+ ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
605
+ }
606
+ ctx.globalCompositeOperation = 'source-over';
607
+ }
608
+
609
+ draw(time) {
610
+ if (!this.visible) return;
611
+ this.resize();
612
+ this.drawNebula(time);
613
+ }
614
+
615
+ setPreset(preset) {
616
+ this.preset = preset;
617
+ }
618
+
619
+ randomize() {}
620
+ dispose() {}
621
+ }
622
+
623
+ /**
624
+ * Document-level shared rAF scheduler. Every component instance subscribes
625
+ * its own frame callback; the whole page runs ONE animation loop (like the
626
+ * original demo), which avoids jank from many competing rAF loops.
627
+ */
628
+ const subscribers = [];
629
+ let running = false;
630
+ let rafId = 0;
631
+ let last = 0;
632
+
633
+ function tick(now) {
634
+ if (!running) return;
635
+ const delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
636
+ last = now;
637
+ // Schedule the next frame BEFORE running callbacks so one throwing
638
+ // subscriber can never kill the whole animation loop.
639
+ rafId = requestAnimationFrame(tick);
640
+ {
641
+ for (const item of subscribers.slice()) {
642
+ try {
643
+ if (!item.isPaused()) item.onFrame(delta, now);
644
+ } catch (error) {
645
+ console.warn('[dlc-ui] frame error:', error);
646
+ }
647
+ }
648
+ }
649
+ if (subscribers.length === 0) {
650
+ cancelAnimationFrame(rafId);
651
+ running = false;
652
+ rafId = 0;
653
+ }
654
+ }
655
+
656
+ function start() {
657
+ if (running) return;
658
+ running = true;
659
+ last = 0;
660
+ rafId = requestAnimationFrame(tick);
661
+ }
662
+
663
+ function subscribeScheduler(onFrame, isPaused) {
664
+ const item = { onFrame, isPaused };
665
+ subscribers.push(item);
666
+ start();
667
+ return () => {
668
+ const index = subscribers.indexOf(item);
669
+ if (index !== -1) subscribers.splice(index, 1);
670
+ if (subscribers.length === 0 && rafId) {
671
+ cancelAnimationFrame(rafId);
672
+ running = false;
673
+ rafId = 0;
674
+ }
675
+ };
676
+ }
677
+
678
+ /**
679
+ * Gates drawing on "element intersects viewport AND the page tab is visible".
680
+ * Falls back to always-visible when IntersectionObserver is unavailable.
681
+ */
682
+ function createVisibilityGuard(element) {
683
+ let intersecting = true;
684
+ let pageVisible = typeof document === 'undefined' || !document.hidden;
685
+ let disposed = false;
686
+ let observer = null;
687
+
688
+ if (typeof IntersectionObserver !== 'undefined') {
689
+ observer = new IntersectionObserver(
690
+ (entries) => {
691
+ intersecting = entries.some((entry) => entry.isIntersecting);
692
+ },
693
+ { rootMargin: '180px' }
694
+ );
695
+ observer.observe(element);
696
+ }
697
+
698
+ const onVisibilityChange = () => {
699
+ pageVisible = typeof document !== 'undefined' && !document.hidden;
700
+ };
701
+ if (typeof document !== 'undefined') {
702
+ document.addEventListener('visibilitychange', onVisibilityChange);
703
+ }
704
+
705
+ return {
706
+ isVisible() {
707
+ return intersecting && pageVisible;
708
+ },
709
+ dispose() {
710
+ if (disposed) return;
711
+ disposed = true;
712
+ if (observer) observer.disconnect();
713
+ if (typeof document !== 'undefined') {
714
+ document.removeEventListener('visibilitychange', onVisibilityChange);
715
+ }
716
+ }
717
+ };
718
+ }
719
+
720
+ function prefersReducedMotion$1() {
721
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
722
+ }
723
+
724
+ /**
725
+ * Mount a cosmic (nebula) capsule into `container`.
726
+ *
727
+ * Options: preset (id/code/name or object), width, height (number=px or
728
+ * string with px/%/vw/vh/em/rem), colors, seed, speed, quality, interactive,
729
+ * respectReducedMotion, copy, cssVars.
730
+ */
731
+ function createCapsule(container, options = {}) {
732
+ if (!container || typeof container.appendChild !== 'function') {
733
+ throw new Error('createCapsule: container element is required');
734
+ }
735
+
736
+ const preset = { ...getPreset('capsule', options.preset ?? 'NC-01') };
737
+ const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
738
+ const copy = { ...COPY, ...(merged.copy || {}) };
739
+
740
+ const root = document.createElement('div');
741
+ root.className = 'hj-capsule-root hj-capsule-cosmic';
742
+ root.dataset.group = preset.group;
743
+ root.dataset.mode = 'nebula';
744
+ root.dataset.theme = preset.theme || 'light';
745
+ root.setAttribute('aria-label', copy.capsuleAria.replace('{name}', preset.name));
746
+
747
+ const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
748
+
749
+ const copyText = (key, fallback) => {
750
+ const value = copy[key];
751
+ return value === undefined ? fallback : value;
752
+ };
753
+
754
+ function buildCopyHtml() {
755
+ const codeText = copyText('code', preset.code);
756
+ const nameText = copyText('name', preset.name);
757
+ const subtitleText = copyText('subtitle', preset.subtitle);
758
+ const stateText = copyText('state', 'LIVE COSMIC STUDY');
759
+ let html = '';
760
+ if (codeText) html += `<span class="hj-capsule-code">${codeText}</span>`;
761
+ if (nameText) html += `<span class="hj-capsule-name">${nameText}</span>`;
762
+ if (subtitleText) html += `<span class="hj-capsule-brand">${subtitleText}</span>`;
763
+ if (stateText) html += `<span class="hj-capsule-state">${stateText}</span>`;
764
+ return html;
765
+ }
766
+
767
+ const copyLayer = document.createElement('div');
768
+ copyLayer.className = 'hj-capsule-copy';
769
+ const renderCopy = () => {
770
+ if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
771
+ };
772
+ renderCopy();
773
+
774
+ const isHexColor = (value) => typeof value === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value);
775
+ if (copy.background) {
776
+ root.style.setProperty('--hj-copy-bg', isHexColor(copy.background)
777
+ ? `linear-gradient(90deg, ${copy.background} 0%, ${copy.background} 74%, ${hexToRgba(copy.background, 0.97)} 84%, ${hexToRgba(copy.background, 0)} 100%)`
778
+ : copy.background);
779
+ }
780
+ if (copy.color) root.style.setProperty('--hj-copy-color', copy.color);
781
+
782
+ const canvas = document.createElement('canvas');
783
+ canvas.className = 'hj-capsule-canvas';
784
+ canvas.setAttribute('aria-hidden', 'true');
785
+
786
+ if (copyEnabled) root.appendChild(copyLayer);
787
+ root.appendChild(canvas);
788
+ container.appendChild(root);
789
+
790
+ const emitter = createEmitter();
791
+ let paused = merged.respectReducedMotion && prefersReducedMotion$1();
792
+
793
+ let renderer;
794
+ const useWebgl = merged.renderer !== 'canvas2d';
795
+ if (useWebgl) {
796
+ try {
797
+ renderer = new CosmicRenderer(canvas, merged, { dprCap: dprCapFor(merged.quality) });
798
+ } catch (error) {
799
+ renderer = new FallbackRenderer(canvas, merged);
800
+ emitter.emit('error', { message: String(error && error.message ? error.message : error) });
801
+ }
802
+ } else {
803
+ renderer = new FallbackRenderer(canvas, merged);
804
+ }
805
+
806
+ const applySize = () => {
807
+ root.style.width = parseSize(merged.width);
808
+ root.style.height = parseSize(merged.height);
809
+ const vars = merged.cssVars || {};
810
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
811
+ renderer.resize();
812
+ };
813
+ applySize();
814
+
815
+ const resizeObserver = typeof ResizeObserver !== 'undefined'
816
+ ? new ResizeObserver(() => renderer.resize())
817
+ : null;
818
+ if (resizeObserver) resizeObserver.observe(root);
819
+ else window.addEventListener('resize', renderer.resize);
820
+
821
+ const visibility = createVisibilityGuard(root);
822
+
823
+ if (merged.interactive !== false) {
824
+ root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
825
+ root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
826
+ }
827
+ root.addEventListener('click', (event) => {
828
+ emitter.emit('click', { event, preset: { ...preset } });
829
+ });
830
+ root.addEventListener('pointerdown', (event) => emitter.emit('pointerdown', { event, preset: { ...preset } }));
831
+ root.addEventListener('pointerup', (event) => emitter.emit('pointerup', { event, preset: { ...preset } }));
832
+ root.addEventListener('dblclick', (event) => emitter.emit('dblclick', { event, preset: { ...preset } }));
833
+
834
+ let animationTime = 0;
835
+ const unsubscribe = subscribeScheduler(
836
+ (delta) => {
837
+ animationTime += delta;
838
+ if (visibility.isVisible()) renderer.draw(animationTime);
839
+ },
840
+ () => paused
841
+ );
842
+
843
+ emitter.emit('ready', { preset: { ...preset } });
844
+
845
+ const syncCopy = () => {
846
+ root.dataset.mode = 'nebula';
847
+ root.dataset.theme = preset.theme || 'light';
848
+ renderCopy();
849
+ };
850
+
851
+ return {
852
+ element: root,
853
+ canvas,
854
+ preset,
855
+ on: emitter.on,
856
+ off: emitter.off,
857
+ setPreset(ref) {
858
+ const next = getPreset('capsule', ref);
859
+ Object.assign(preset, next);
860
+ renderer.setPreset(next);
861
+ syncCopy();
862
+ renderer.resize();
863
+ emitter.emit('presetchange', { preset: { ...next } });
864
+ return this;
865
+ },
866
+ setColors(colors) {
867
+ if (!Array.isArray(colors) || colors.length !== 4) return this;
868
+ preset.colors = colors;
869
+ renderer.setPreset({ ...preset, colors });
870
+ return this;
871
+ },
872
+ setSize(width, height) {
873
+ if (width !== undefined) merged.width = width;
874
+ if (height !== undefined) merged.height = height;
875
+ applySize();
876
+ return this;
877
+ },
878
+ randomize() {
879
+ renderer.randomize();
880
+ return this;
881
+ },
882
+ pause() {
883
+ paused = true;
884
+ return this;
885
+ },
886
+ resume() {
887
+ paused = false;
888
+ return this;
889
+ },
890
+ setQuality(quality) {
891
+ merged.quality = quality;
892
+ if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
893
+ return this;
894
+ },
895
+ dispose() {
896
+ unsubscribe();
897
+ visibility.dispose();
898
+ if (resizeObserver) resizeObserver.disconnect();
899
+ else window.removeEventListener('resize', renderer.resize);
900
+ renderer.dispose();
901
+ root.remove();
902
+ }
903
+ };
904
+ }
905
+
906
+ const PROGRESS_MOTION_WIDTH = 240;
907
+ const PROGRESS_MOTION_HEIGHT = 80;
908
+ const PROGRESS_MOTION_DURATION = 12.0;
909
+ const PROGRESS_MOTION_MAX_PX = 40.0;
910
+
911
+ const PROFILE_CONFIG = {
912
+ 'model-training': { seed: 0.37, broad: 0.58, middle: 0.25, detail: 0.13, lobe: 0.24 },
913
+ 'agent-migration': { seed: 1.71, broad: 0.72, middle: 0.10, detail: 0.03, lobe: 0.18 },
914
+ 'visual-training': { seed: 2.83, broad: 0.66, middle: 0.16, detail: 0.06, lobe: 0.23 },
915
+ // ponytail: first-pass tide = asymmetric time warp on the same motion pipeline.
916
+ // Refine (foam line / wash streaks) in the shader after visual QA.
917
+ 'tide': { seed: 4.12, broad: 0.82, middle: 0.07, detail: 0.02, lobe: 0.30, warp: 0.5 }
918
+ };
919
+
920
+ const CACHE$1 = Object.create(null);
921
+
922
+ function gaussian(value, center, width) {
923
+ const delta = (value - center) / Math.max(width, 0.001);
924
+ return Math.exp(-delta * delta);
925
+ }
926
+
927
+ function createMotionData(id, edgeStyle = 'flow') {
928
+ const profile = edgeStyle === 'tide'
929
+ ? PROFILE_CONFIG.tide
930
+ : (PROFILE_CONFIG[id] || PROFILE_CONFIG['visual-training']);
931
+ const data = new Uint8Array(PROGRESS_MOTION_WIDTH * PROGRESS_MOTION_HEIGHT);
932
+
933
+ for (let x = 0; x < PROGRESS_MOTION_WIDTH; x += 1) {
934
+ let time = (x / PROGRESS_MOTION_WIDTH) * Math.PI * 2;
935
+ if (profile.warp) time += profile.warp * Math.sin(time * 2);
936
+ const centerA = 0.28 + Math.sin(time * 0.53 + profile.seed) * 0.13;
937
+ const centerB = 0.70 + Math.cos(time * 0.47 + profile.seed * 1.7) * 0.12;
938
+
939
+ for (let y = 0; y < PROGRESS_MOTION_HEIGHT; y += 1) {
940
+ const ratio = y / Math.max(PROGRESS_MOTION_HEIGHT - 1, 1);
941
+ const envelope = Math.pow(Math.max(Math.sin(Math.PI * ratio), 0), 0.48);
942
+ const broad = Math.sin(ratio * Math.PI * 2 * 1.35 + time * 0.58 + profile.seed) * profile.broad;
943
+ const middle = Math.sin(ratio * Math.PI * 2 * 3.2 - time * 0.91 + profile.seed * 2.1) * profile.middle;
944
+ const detail = Math.sin(ratio * Math.PI * 2 * 6.1 + time * 1.31 + profile.seed * 3.2) * profile.detail;
945
+ const lobes = (
946
+ gaussian(ratio, centerA, 0.09) * Math.sin(time * 1.11 + profile.seed * 4.0) -
947
+ gaussian(ratio, centerB, 0.10) * Math.cos(time * 0.97 + profile.seed * 3.3)
948
+ ) * profile.lobe;
949
+ const normalized = Math.max(-1, Math.min(1, (broad + middle + detail + lobes) * envelope));
950
+ data[y * PROGRESS_MOTION_WIDTH + x] = Math.round((normalized * 0.5 + 0.5) * 255);
951
+ }
952
+ }
953
+
954
+ return data;
955
+ }
956
+
957
+ function getProgressMotionData(id, edgeStyle = 'flow') {
958
+ const key = `${id}:${edgeStyle}`;
959
+ if (!CACHE$1[key]) CACHE$1[key] = createMotionData(id, edgeStyle);
960
+ return CACHE$1[key];
961
+ }
962
+
963
+ function hexToRgb01(hex) {
964
+ const value = Number.parseInt(hex.replace('#', ''), 16);
965
+ return [
966
+ ((value >> 16) & 255) / 255,
967
+ ((value >> 8) & 255) / 255,
968
+ (value & 255) / 255
969
+ ];
970
+ }
971
+
972
+ function stringSeed$1(value) {
973
+ let hash = 2166136261;
974
+ for (const character of value) {
975
+ hash ^= character.charCodeAt(0);
976
+ hash = Math.imul(hash, 16777619);
977
+ }
978
+ return (hash >>> 0) / 4294967295;
979
+ }
980
+
981
+ const PROFILE_INDEX = {
982
+ 'model-training': 0,
983
+ 'agent-migration': 1,
984
+ 'visual-training': 2,
985
+ 'tide': 3
986
+ };
987
+
988
+ const MOTION_SCALE_FACTORS = {
989
+ 'model-training': 1.05,
990
+ 'agent-migration': 1.04,
991
+ 'visual-training': 1.04,
992
+ 'tide': 1.18
993
+ };
994
+
995
+ const VERTEX_SHADER = `#version 300 es
996
+ in vec2 a_position;
997
+ out vec2 v_uv;
998
+ void main() {
999
+ v_uv = a_position * 0.5 + 0.5;
1000
+ gl_Position = vec4(a_position, 0.0, 1.0);
1001
+ }`;
1002
+
1003
+ const FRAGMENT_SHADER = `#version 300 es
1004
+ precision highp float;
1005
+
1006
+ in vec2 v_uv;
1007
+ out vec4 outColor;
1008
+
1009
+ uniform vec2 u_resolution;
1010
+ uniform float u_time;
1011
+ uniform float u_progress;
1012
+ uniform float u_seed;
1013
+ uniform float u_profile;
1014
+ uniform sampler2D u_motion;
1015
+ uniform sampler2D u_effect;
1016
+ uniform float u_hasEffect;
1017
+ uniform float u_effectFrames;
1018
+ uniform float u_motionDuration;
1019
+ uniform float u_motionScale;
1020
+ uniform vec3 u_dark;
1021
+ uniform vec3 u_accentA;
1022
+ uniform vec3 u_accentB;
1023
+ uniform vec3 u_glow;
1024
+
1025
+ float hash21(vec2 p) {
1026
+ p = fract(p * vec2(123.34, 456.21));
1027
+ p += dot(p, p + 45.32 + u_seed * 11.7);
1028
+ return fract(p.x * p.y);
1029
+ }
1030
+
1031
+ float noise(vec2 p) {
1032
+ vec2 i = floor(p);
1033
+ vec2 f = fract(p);
1034
+ f = f * f * (3.0 - 2.0 * f);
1035
+ float a = hash21(i);
1036
+ float b = hash21(i + vec2(1.0, 0.0));
1037
+ float c = hash21(i + vec2(0.0, 1.0));
1038
+ float d = hash21(i + vec2(1.0, 1.0));
1039
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
1040
+ }
1041
+
1042
+ float fbm(vec2 p) {
1043
+ float value = 0.0;
1044
+ float amplitude = 0.55;
1045
+ mat2 rotation = mat2(0.82, 0.57, -0.57, 0.82);
1046
+ for (int i = 0; i < 6; i++) {
1047
+ value += noise(p) * amplitude;
1048
+ p = rotation * p * 2.02 + 13.7;
1049
+ amplitude *= 0.48;
1050
+ }
1051
+ return value;
1052
+ }
1053
+
1054
+ float gaussian(float value, float center, float width) {
1055
+ float delta = (value - center) / max(width, 0.0001);
1056
+ return exp(-delta * delta);
1057
+ }
1058
+
1059
+ float profileMix(float model, float agent, float visual) {
1060
+ if (u_profile < 0.5) return model;
1061
+ if (u_profile < 1.5) return agent;
1062
+ return visual;
1063
+ }
1064
+
1065
+ float motionSample(float y, float t) {
1066
+ float phase = fract(t / max(u_motionDuration, 0.001));
1067
+ float captured = texture(u_motion, vec2(phase, 1.0 - clamp(y, 0.0, 1.0))).r;
1068
+ return (captured * 2.0 - 1.0) * u_motionScale;
1069
+ }
1070
+
1071
+ float edgeDisplacement(float y, float t) {
1072
+ return motionSample(y, t);
1073
+ }
1074
+
1075
+ float flowDisplacement(float y, float t) {
1076
+ return (
1077
+ motionSample(y - 0.024, t) * 0.08 +
1078
+ motionSample(y - 0.012, t) * 0.18 +
1079
+ motionSample(y, t) * 0.48 +
1080
+ motionSample(y + 0.012, t) * 0.18 +
1081
+ motionSample(y + 0.024, t) * 0.08
1082
+ );
1083
+ }
1084
+
1085
+ float ellipseRing(vec2 p, float radius, float width) {
1086
+ return gaussian(length(p), radius, width);
1087
+ }
1088
+
1089
+ void main() {
1090
+ vec2 uv = v_uv;
1091
+ float t = u_time;
1092
+ float edge = u_progress + edgeDisplacement(uv.y, t);
1093
+ float flowEdge = u_progress + flowDisplacement(uv.y, t);
1094
+ float d = uv.x - edge;
1095
+ float fd = uv.x - flowEdge;
1096
+
1097
+ vec3 rightBase = vec3(0.125, 0.129, 0.145);
1098
+ vec3 color = rightBase;
1099
+
1100
+ float leftMask = 1.0 - smoothstep(-0.001, 0.002, d);
1101
+ color = mix(color, u_dark, leftMask * profileMix(0.96, 0.92, 0.96));
1102
+
1103
+ vec2 flowP = vec2((fd + 0.10) * 6.2, uv.y * 1.95);
1104
+ float flowA = fbm(flowP + vec2(-t * 0.22, t * 0.27) + u_seed * 1.7);
1105
+ float flowB = fbm(flowP * 1.52 + vec2(t * 0.28, -t * 0.36) + 8.2 + u_seed);
1106
+ float flowC = fbm(flowP * 2.25 + vec2(-t * 0.41, t * 0.46) + 19.0);
1107
+
1108
+ float farCenter = profileMix(-0.060, -0.079, -0.045) + (flowA - 0.5) * profileMix(0.018, 0.022, 0.014);
1109
+ float midCenter = profileMix(-0.039, -0.052, -0.030) + (flowB - 0.5) * profileMix(0.013, 0.016, 0.010);
1110
+ float hotCenter = profileMix(-0.026, -0.029, -0.023) + (flowC - 0.5) * 0.010;
1111
+
1112
+ float farBand = gaussian(fd, farCenter, profileMix(0.035, 0.049, 0.030));
1113
+ float midBand = gaussian(fd, midCenter, profileMix(0.026, 0.034, 0.026));
1114
+ float hotBand = gaussian(fd, hotCenter, profileMix(0.023, 0.027, 0.026));
1115
+ float darkTrough = gaussian(fd, profileMix(-0.050, -0.058, -0.044) + (flowB - 0.5) * 0.010, profileMix(0.020, 0.025, 0.021));
1116
+
1117
+ float ringY = 0.47 + sin(t * 0.58 + u_seed * 2.4) * 0.12;
1118
+ vec2 ringP = vec2((fd + 0.086) / 0.078, (uv.y - ringY) / 0.25);
1119
+ ringP += vec2((flowB - 0.5) * 0.08, (flowA - 0.5) * 0.06);
1120
+ float ringTexture = fbm(ringP * 2.15 + vec2(t * 0.18, -t * 0.14) + u_seed * 1.9);
1121
+ float ring = ellipseRing(ringP, 0.66, 0.32) * (0.30 + 0.64 * ringTexture);
1122
+ float ringCore = gaussian(length(ringP), 0.25, 0.25);
1123
+ float ringPulse = smoothstep(0.58, 0.90, 0.5 + 0.5 * sin(t * 0.82 + u_seed * 4.1));
1124
+ float modelRing = ring * ringPulse * (1.0 - step(0.5, u_profile));
1125
+ float visualRing = ring * 0.16 * step(1.5, u_profile) * ringPulse;
1126
+
1127
+ float cloudGate = leftMask * smoothstep(-0.30, -0.008, fd);
1128
+ float textureA = smoothstep(0.24, 0.92, flowA * 0.72 + flowB * 0.42);
1129
+ float textureB = smoothstep(0.28, 0.94, flowB * 0.68 + flowC * 0.38);
1130
+
1131
+ vec3 hotColor = u_accentB;
1132
+ color += u_accentA * farBand * cloudGate * (0.07 + textureA * profileMix(0.42, 0.24, 0.40));
1133
+ color += u_accentB * midBand * cloudGate * (0.15 + textureB * profileMix(0.70, 0.46, 0.68));
1134
+ color += hotColor * hotBand * cloudGate * profileMix(0.88, 0.62, 0.84);
1135
+ float modelMask = 1.0 - step(0.5, u_profile);
1136
+ color += u_accentA * (modelRing + visualRing) * cloudGate * profileMix(0.54, 0.0, 0.34);
1137
+ color *= 1.0 - darkTrough * profileMix(0.44, 0.24, 0.24) * cloudGate;
1138
+ color *= 1.0 - ringCore * modelMask * ringPulse * 0.44 * cloudGate;
1139
+
1140
+ float broadHalo = exp(-abs(d) * 96.0);
1141
+ float innerHalo = exp(-abs(d) * 176.0);
1142
+ float colorCore = exp(-abs(d) * 360.0);
1143
+ float sharpCore = exp(-abs(d) * 760.0);
1144
+ float leftGate = 1.0 - smoothstep(-0.003, 0.005, d);
1145
+
1146
+ color += u_accentA * broadHalo * leftGate * profileMix(0.09, 0.04, 0.06);
1147
+ color += hotColor * innerHalo * leftGate * profileMix(0.76, 0.72, 0.78);
1148
+ color += u_glow * colorCore * profileMix(0.72, 0.34, 0.24);
1149
+
1150
+ float whiteStrength = profileMix(0.10, 0.0, 0.0);
1151
+ color += vec3(1.0, 0.99, 0.91) * sharpCore * whiteStrength;
1152
+
1153
+ float rightCut = smoothstep(0.001, 0.006, d);
1154
+ color = mix(color, rightBase, rightCut);
1155
+
1156
+ float effectX = (d * 1257.0 + 260.0) / 320.0;
1157
+ float atlasPhase = fract(t / 12.0) * u_effectFrames;
1158
+ float atlasFrameA = floor(atlasPhase);
1159
+ float atlasFrameB = mod(atlasFrameA + 1.0, u_effectFrames);
1160
+ float atlasMix = smoothstep(0.0, 1.0, fract(atlasPhase));
1161
+ float atlasXA = (atlasFrameA + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
1162
+ float atlasXB = (atlasFrameB + clamp(effectX, 0.0, 1.0)) / u_effectFrames;
1163
+ vec3 referenceA = texture(u_effect, vec2(atlasXA, uv.y)).rgb;
1164
+ vec3 referenceB = texture(u_effect, vec2(atlasXB, uv.y)).rgb;
1165
+ vec3 referenceColor = mix(referenceA, referenceB, atlasMix);
1166
+ float stripMask = smoothstep(0.0, 0.018, effectX) * (1.0 - smoothstep(0.982, 1.0, effectX));
1167
+ float referenceLeft = 1.0 - smoothstep(-0.026, -0.012, d);
1168
+ color = mix(color, referenceColor, stripMask * referenceLeft * u_hasEffect);
1169
+
1170
+ outColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1171
+ }`;
1172
+
1173
+ function compileShader(gl, type, source) {
1174
+ const shader = gl.createShader(type);
1175
+ gl.shaderSource(shader, source);
1176
+ gl.compileShader(shader);
1177
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1178
+ const message = gl.getShaderInfoLog(shader) || 'Unknown shader compile error';
1179
+ gl.deleteShader(shader);
1180
+ throw new Error(message);
1181
+ }
1182
+ return shader;
1183
+ }
1184
+
1185
+ function createProgram(gl) {
1186
+ const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
1187
+ const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
1188
+ const program = gl.createProgram();
1189
+ gl.attachShader(program, vertex);
1190
+ gl.attachShader(program, fragment);
1191
+ gl.linkProgram(program);
1192
+ gl.deleteShader(vertex);
1193
+ gl.deleteShader(fragment);
1194
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1195
+ const message = gl.getProgramInfoLog(program) || 'Unknown shader link error';
1196
+ gl.deleteProgram(program);
1197
+ throw new Error(message);
1198
+ }
1199
+ return program;
1200
+ }
1201
+
1202
+ class ProgressFlowRenderer {
1203
+ constructor(canvas, preset) {
1204
+ const gl = canvas.getContext('webgl2', {
1205
+ alpha: false,
1206
+ antialias: true,
1207
+ premultipliedAlpha: false,
1208
+ powerPreference: 'high-performance'
1209
+ });
1210
+ if (!gl) throw new Error('WebGL2 unavailable');
1211
+
1212
+ this.canvas = canvas;
1213
+ this.gl = gl;
1214
+ this.program = createProgram(gl);
1215
+ this.profile = preset.edgeStyle === 'tide' ? 3 : (PROFILE_INDEX[preset.id] ?? 2);
1216
+ this.seed = stringSeed$1(`${preset.id}-shader`) * 13.7 + 1.0;
1217
+ this.colors = preset.colors.map(hexToRgb01);
1218
+ this.motionData = getProgressMotionData(preset.id, preset.edgeStyle);
1219
+ const motionFactor = preset.edgeStyle === 'tide'
1220
+ ? MOTION_SCALE_FACTORS.tide
1221
+ : (MOTION_SCALE_FACTORS[preset.id] || 1.04);
1222
+ this.motionScale = (PROGRESS_MOTION_MAX_PX * motionFactor) / 1257;
1223
+
1224
+ this.position = gl.getAttribLocation(this.program, 'a_position');
1225
+ this.uniforms = {
1226
+ resolution: gl.getUniformLocation(this.program, 'u_resolution'),
1227
+ time: gl.getUniformLocation(this.program, 'u_time'),
1228
+ progress: gl.getUniformLocation(this.program, 'u_progress'),
1229
+ seed: gl.getUniformLocation(this.program, 'u_seed'),
1230
+ profile: gl.getUniformLocation(this.program, 'u_profile'),
1231
+ motion: gl.getUniformLocation(this.program, 'u_motion'),
1232
+ effect: gl.getUniformLocation(this.program, 'u_effect'),
1233
+ hasEffect: gl.getUniformLocation(this.program, 'u_hasEffect'),
1234
+ effectFrames: gl.getUniformLocation(this.program, 'u_effectFrames'),
1235
+ motionDuration: gl.getUniformLocation(this.program, 'u_motionDuration'),
1236
+ motionScale: gl.getUniformLocation(this.program, 'u_motionScale'),
1237
+ dark: gl.getUniformLocation(this.program, 'u_dark'),
1238
+ accentA: gl.getUniformLocation(this.program, 'u_accentA'),
1239
+ accentB: gl.getUniformLocation(this.program, 'u_accentB'),
1240
+ glow: gl.getUniformLocation(this.program, 'u_glow')
1241
+ };
1242
+
1243
+ this.buffer = gl.createBuffer();
1244
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
1245
+ gl.bufferData(
1246
+ gl.ARRAY_BUFFER,
1247
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
1248
+ gl.STATIC_DRAW
1249
+ );
1250
+
1251
+ this.motionTexture = gl.createTexture();
1252
+ gl.activeTexture(gl.TEXTURE0);
1253
+ gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
1254
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
1255
+ gl.texImage2D(
1256
+ gl.TEXTURE_2D,
1257
+ 0,
1258
+ gl.R8,
1259
+ PROGRESS_MOTION_WIDTH,
1260
+ PROGRESS_MOTION_HEIGHT,
1261
+ 0,
1262
+ gl.RED,
1263
+ gl.UNSIGNED_BYTE,
1264
+ this.motionData
1265
+ );
1266
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1267
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1268
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
1269
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1270
+
1271
+ this.effectTexture = gl.createTexture();
1272
+ gl.activeTexture(gl.TEXTURE1);
1273
+ gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
1274
+ gl.texImage2D(
1275
+ gl.TEXTURE_2D,
1276
+ 0,
1277
+ gl.RGB,
1278
+ 1,
1279
+ 1,
1280
+ 0,
1281
+ gl.RGB,
1282
+ gl.UNSIGNED_BYTE,
1283
+ new Uint8Array([32, 33, 38])
1284
+ );
1285
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1286
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1287
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1288
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1289
+ this.effectUploaded = false;
1290
+ }
1291
+
1292
+ resize(width, height, dpr) {
1293
+ const pixelWidth = Math.max(1, Math.round(width * dpr));
1294
+ const pixelHeight = Math.max(1, Math.round(height * dpr));
1295
+ if (this.canvas.width !== pixelWidth || this.canvas.height !== pixelHeight) {
1296
+ this.canvas.width = pixelWidth;
1297
+ this.canvas.height = pixelHeight;
1298
+ }
1299
+ this.gl.viewport(0, 0, pixelWidth, pixelHeight);
1300
+ }
1301
+
1302
+ draw(time, progress, effectImage = null) {
1303
+ const gl = this.gl;
1304
+ gl.useProgram(this.program);
1305
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
1306
+ gl.enableVertexAttribArray(this.position);
1307
+ gl.vertexAttribPointer(this.position, 2, gl.FLOAT, false, 0, 0);
1308
+
1309
+ gl.uniform2f(this.uniforms.resolution, this.canvas.width, this.canvas.height);
1310
+ gl.uniform1f(this.uniforms.time, time);
1311
+ gl.uniform1f(this.uniforms.progress, progress / 100);
1312
+ gl.uniform1f(this.uniforms.seed, this.seed);
1313
+ gl.uniform1f(this.uniforms.profile, this.profile);
1314
+ gl.activeTexture(gl.TEXTURE0);
1315
+ gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
1316
+ gl.uniform1i(this.uniforms.motion, 0);
1317
+ gl.uniform1f(this.uniforms.motionDuration, PROGRESS_MOTION_DURATION);
1318
+ gl.uniform1f(this.uniforms.motionScale, this.motionScale);
1319
+
1320
+ let hasEffect = this.effectUploaded ? 1 : 0;
1321
+ if (!this.effectUploaded && effectImage && effectImage.complete && effectImage.naturalWidth > 0) {
1322
+ gl.activeTexture(gl.TEXTURE1);
1323
+ gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
1324
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
1325
+ try {
1326
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, effectImage);
1327
+ this.effectUploaded = true;
1328
+ hasEffect = 1;
1329
+ } catch (error) {
1330
+ console.warn('[画境观屿] 参考纹理图集上传失败,继续使用程序化降级。', error);
1331
+ }
1332
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
1333
+ }
1334
+ gl.activeTexture(gl.TEXTURE1);
1335
+ gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
1336
+ gl.uniform1i(this.uniforms.effect, 1);
1337
+ gl.uniform1f(this.uniforms.hasEffect, hasEffect);
1338
+ gl.uniform1f(this.uniforms.effectFrames, 24);
1339
+
1340
+ gl.uniform3fv(this.uniforms.dark, this.colors[0]);
1341
+ gl.uniform3fv(this.uniforms.accentA, this.colors[1]);
1342
+ gl.uniform3fv(this.uniforms.accentB, this.colors[2]);
1343
+ gl.uniform3fv(this.uniforms.glow, this.colors[3]);
1344
+
1345
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
1346
+ }
1347
+
1348
+ setColors(colors) {
1349
+ this.colors = colors.map(hexToRgb01);
1350
+ }
1351
+
1352
+ dispose() {
1353
+ const gl = this.gl;
1354
+ gl.deleteBuffer(this.buffer);
1355
+ gl.deleteTexture(this.motionTexture);
1356
+ gl.deleteTexture(this.effectTexture);
1357
+ gl.deleteProgram(this.program);
1358
+ const lose = gl.getExtension('WEBGL_lose_context');
1359
+ if (lose) lose.loseContext();
1360
+ }
1361
+ }
1362
+
1363
+ function createProgressFlowRenderer(canvas, preset) {
1364
+ try {
1365
+ return new ProgressFlowRenderer(canvas, preset);
1366
+ } catch (error) {
1367
+ console.warn('[画境观屿] 进度流体 WebGL2 不可用,使用 Canvas 2D 降级。', error);
1368
+ return null;
1369
+ }
1370
+ }
1371
+
1372
+ const PROGRESS_REFERENCE_DURATION = 12;
1373
+ const PROGRESS_REFERENCE_FRAME_COUNT = 24;
1374
+
1375
+ const FRAME_WIDTH = 64;
1376
+ const FRAME_HEIGHT = 32;
1377
+ const CACHE = Object.create(null);
1378
+
1379
+ const PALETTES = {
1380
+ 'model-training': ['#20131f', '#ff3f94', '#ff8a3d', '#fff06a'],
1381
+ 'agent-migration': ['#111a31', '#245bff', '#00cfff', '#5dffe6'],
1382
+ 'visual-training': ['#1f172b', '#7042ff', '#42f58d', '#c4ff8a'],
1383
+ 'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff']
1384
+ };
1385
+
1386
+ function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
1387
+ context.save();
1388
+ context.translate(x, y);
1389
+ context.scale(1, radiusY / radiusX);
1390
+ const gradient = context.createRadialGradient(0, 0, 0, 0, 0, radiusX);
1391
+ gradient.addColorStop(0, `${color}${Math.round(alpha * 255).toString(16).padStart(2, '0')}`);
1392
+ gradient.addColorStop(0.46, `${color}${Math.round(alpha * 0.42 * 255).toString(16).padStart(2, '0')}`);
1393
+ gradient.addColorStop(1, `${color}00`);
1394
+ context.fillStyle = gradient;
1395
+ context.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
1396
+ context.restore();
1397
+ }
1398
+
1399
+ function createAtlas(id) {
1400
+ const palette = PALETTES[id] || PALETTES['visual-training'];
1401
+ const canvas = document.createElement('canvas');
1402
+ canvas.width = FRAME_WIDTH * PROGRESS_REFERENCE_FRAME_COUNT;
1403
+ canvas.height = FRAME_HEIGHT;
1404
+ const context = canvas.getContext('2d');
1405
+
1406
+ for (let frame = 0; frame < PROGRESS_REFERENCE_FRAME_COUNT; frame += 1) {
1407
+ const phase = (frame / PROGRESS_REFERENCE_FRAME_COUNT) * Math.PI * 2;
1408
+ const left = frame * FRAME_WIDTH;
1409
+ context.save();
1410
+ context.translate(left, 0);
1411
+ context.fillStyle = palette[0];
1412
+ context.fillRect(0, 0, FRAME_WIDTH, FRAME_HEIGHT);
1413
+ context.globalCompositeOperation = 'screen';
1414
+
1415
+ 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);
1416
+ drawCloud(context, 45 + Math.cos(phase * 0.72) * 4, 23 + Math.sin(phase * 0.54) * 4, 22, 15, palette[2], 0.44);
1417
+ 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);
1418
+
1419
+ context.globalCompositeOperation = 'source-over';
1420
+ const trough = context.createRadialGradient(41, 16, 1, 41, 16, 17);
1421
+ trough.addColorStop(0, 'rgba(5,6,11,0.64)');
1422
+ trough.addColorStop(0.58, 'rgba(6,7,12,0.26)');
1423
+ trough.addColorStop(1, 'rgba(6,7,12,0)');
1424
+ context.fillStyle = trough;
1425
+ context.fillRect(20, 0, 44, FRAME_HEIGHT);
1426
+ context.restore();
1427
+ }
1428
+
1429
+ const image = new Image();
1430
+ image.decoding = 'async';
1431
+ const state = { image, ready: false };
1432
+ image.addEventListener('load', () => { state.ready = true; }, { once: true });
1433
+ image.addEventListener('error', () => { state.ready = false; }, { once: true });
1434
+ image.src = canvas.toDataURL('image/png');
1435
+ return state;
1436
+ }
1437
+
1438
+ function getProgressReferenceAtlas(id) {
1439
+ if (!CACHE[id]) CACHE[id] = createAtlas(id);
1440
+ return CACHE[id];
1441
+ }
1442
+
1443
+ /**
1444
+ * Attach a WebGL2 fluid overlay to a progress capsule root.
1445
+ *
1446
+ * @param {object} params
1447
+ * @param {HTMLElement} params.root progress capsule root element
1448
+ * @param {HTMLCanvasElement} params.canvas 2D fallback canvas (kept beneath)
1449
+ * @param {object} params.preset progress preset
1450
+ * @param {() => number} params.getProgress reads the current progress value
1451
+ * @returns {{ update(flowTime: number): void, dispose(): void } | null}
1452
+ */
1453
+ function attachProgressFlowOverlay({ root, canvas, preset, getProgress }) {
1454
+ const overlay = document.createElement('canvas');
1455
+ overlay.className = 'hj-progress-canvas hj-progress-overlay';
1456
+ overlay.setAttribute('aria-hidden', 'true');
1457
+ canvas.insertAdjacentElement('afterend', overlay);
1458
+
1459
+ const renderer = createProgressFlowRenderer(overlay, preset);
1460
+ if (!renderer) {
1461
+ overlay.remove();
1462
+ return null;
1463
+ }
1464
+
1465
+ const atlas = getProgressReferenceAtlas(preset.id);
1466
+ root.classList.add('has-webgl-progress');
1467
+
1468
+ const resize = () => {
1469
+ const bounds = root.getBoundingClientRect();
1470
+ const width = Math.max(1, bounds.width);
1471
+ const height = Math.max(1, bounds.height);
1472
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1473
+ overlay.style.width = `${width}px`;
1474
+ overlay.style.height = `${height}px`;
1475
+ renderer.resize(width, height, dpr);
1476
+ };
1477
+
1478
+ let resizeObserver = null;
1479
+ let onWindowResize = null;
1480
+ if (typeof ResizeObserver !== 'undefined') {
1481
+ resizeObserver = new ResizeObserver(resize);
1482
+ resizeObserver.observe(root);
1483
+ } else {
1484
+ onWindowResize = resize;
1485
+ window.addEventListener('resize', onWindowResize);
1486
+ }
1487
+ resize();
1488
+
1489
+ return {
1490
+ update(flowTime) {
1491
+ const effectTime = flowTime % PROGRESS_REFERENCE_DURATION;
1492
+ const progress = getProgress();
1493
+ renderer.draw(
1494
+ effectTime,
1495
+ Number.isFinite(progress) ? progress : preset.initialProgress,
1496
+ atlas.ready ? atlas.image : null
1497
+ );
1498
+ },
1499
+ setColors(colors) {
1500
+ renderer.setColors(colors);
1501
+ },
1502
+ dispose() {
1503
+ if (resizeObserver) resizeObserver.disconnect();
1504
+ if (onWindowResize) window.removeEventListener('resize', onWindowResize);
1505
+ renderer.dispose();
1506
+ overlay.remove();
1507
+ root.classList.remove('has-webgl-progress');
1508
+ }
1509
+ };
1510
+ }
1511
+
1512
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
1513
+ const SUPPORTS_CTX_FILTER = typeof CanvasRenderingContext2D !== 'undefined' && 'filter' in CanvasRenderingContext2D.prototype;
1514
+
1515
+ function stringSeed(value) {
1516
+ let hash = 2166136261;
1517
+ for (const character of value) {
1518
+ hash ^= character.charCodeAt(0);
1519
+ hash = Math.imul(hash, 16777619);
1520
+ }
1521
+ return (hash >>> 0) / 4294967295;
1522
+ }
1523
+
1524
+ function interpolate(template, values) {
1525
+ return template.replace(/\{(\w+)\}/g, (_, key) => (values[key] !== undefined ? values[key] : ''));
1526
+ }
1527
+
1528
+ function prefersReducedMotion() {
1529
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
1530
+ }
1531
+
1532
+ const FLOW_PROFILES = {
1533
+ 'model-training': {
1534
+ cycles: [0.98, 3.05, 5.45],
1535
+ amplitudes: [11.2, 11.0, 5.2],
1536
+ speeds: [0.31, -0.53, 0.78],
1537
+ bulgeAmplitude: 8.2,
1538
+ timeScale: 1.0,
1539
+ glowWidth: 5.8,
1540
+ haloWidth: 20,
1541
+ whiteAlpha: 0.72,
1542
+ whiteWidth: 1.15,
1543
+ cloudWidth: 0.17,
1544
+ autoRange: [25, 66]
1545
+ },
1546
+ 'agent-migration': {
1547
+ cycles: [0.62, 1.55, 2.95],
1548
+ amplitudes: [16.0, 7.8, 2.3],
1549
+ speeds: [0.25, -0.4, 0.60],
1550
+ bulgeAmplitude: 8.4,
1551
+ timeScale: 0.82,
1552
+ glowWidth: 6.3,
1553
+ haloWidth: 21,
1554
+ whiteAlpha: 0.18,
1555
+ whiteWidth: 0.45,
1556
+ cloudWidth: 0.18,
1557
+ autoRange: [24, 62]
1558
+ },
1559
+ 'visual-training': {
1560
+ cycles: [0.88, 2.45, 4.35],
1561
+ amplitudes: [12.8, 10.2, 4.3],
1562
+ speeds: [0.28, -0.47, 0.69],
1563
+ bulgeAmplitude: 7.3,
1564
+ timeScale: 0.91,
1565
+ glowWidth: 6.0,
1566
+ haloWidth: 21,
1567
+ whiteAlpha: 0.30,
1568
+ whiteWidth: 0.55,
1569
+ cloudWidth: 0.175,
1570
+ autoRange: [20, 75]
1571
+ },
1572
+ // ponytail: first-pass tide = asymmetric surge on the 2D fallback path.
1573
+ // Tune surge/amplitudes after visual QA against the WebGL overlay.
1574
+ 'tide': {
1575
+ cycles: [0.72, 1.9, 4.2],
1576
+ amplitudes: [19.0, 6.5, 1.5],
1577
+ speeds: [0.42, -0.5, 0.66],
1578
+ bulgeAmplitude: 9.5,
1579
+ timeScale: 0.85,
1580
+ glowWidth: 6.2,
1581
+ haloWidth: 21,
1582
+ whiteAlpha: 0.5,
1583
+ whiteWidth: 1.0,
1584
+ cloudWidth: 0.18,
1585
+ autoRange: [18, 70],
1586
+ surge: 0.55
1587
+ }
1588
+ };
1589
+
1590
+ class ProgressCapsuleController {
1591
+ constructor({ root, canvas, valueElement, preset, emitter, options, copy }) {
1592
+ this.root = root;
1593
+ this.canvas = canvas;
1594
+ this.valueElement = valueElement;
1595
+ this.preset = preset;
1596
+ this.emitter = emitter;
1597
+ this.options = options;
1598
+ this.copy = copy;
1599
+ this.profile = preset.edgeStyle === 'tide'
1600
+ ? FLOW_PROFILES.tide
1601
+ : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
1602
+ this.min = options.min ?? 0;
1603
+ this.max = options.max ?? 100;
1604
+ this.dprCap = dprCapFor(options.quality);
1605
+ this.ctx = canvas.getContext('2d');
1606
+ this.value = clamp(options.value ?? preset.initialProgress, this.min, this.max);
1607
+ this.dragging = false;
1608
+ this.webglActive = false;
1609
+ this.flowTime = stringSeed(preset.id) * 31;
1610
+ this.seed = stringSeed(`${preset.id}-reference`) * Math.PI * 2;
1611
+ this.randomState = Math.floor(stringSeed(`${preset.id}-auto`) * 0x7fffffff) || 1;
1612
+ this.dpr = 1;
1613
+ this.width = 0;
1614
+ this.height = 0;
1615
+ this.handlers = {};
1616
+
1617
+ this.resizeObserver = typeof ResizeObserver !== 'undefined'
1618
+ ? new ResizeObserver(() => this.resizeCanvas())
1619
+ : null;
1620
+ if (this.resizeObserver) this.resizeObserver.observe(this.root);
1621
+ else window.addEventListener('resize', this.resizeCanvas);
1622
+
1623
+ this.suppressEvents = true;
1624
+ this.setProgress(this.value, 'init');
1625
+ this.suppressEvents = false;
1626
+ this.bindEvents();
1627
+ this.resizeCanvas();
1628
+ }
1629
+
1630
+ random() {
1631
+ this.randomState = (Math.imul(this.randomState, 1664525) + 1013904223) >>> 0;
1632
+ return this.randomState / 4294967296;
1633
+ }
1634
+
1635
+ setProgress(nextValue, source = 'auto') {
1636
+ this.value = clamp(nextValue, this.min, this.max);
1637
+ const rounded = Math.round(this.value);
1638
+ this.root.style.setProperty('--progress', this.value.toFixed(2));
1639
+ this.root.setAttribute('aria-valuenow', String(rounded));
1640
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.copy.valueSuffix}`);
1641
+ this.valueElement.textContent = `${rounded}${this.copy.valueSuffix}`;
1642
+ if (!this.suppressEvents) this.emitter.emit('change', { value: this.value, source });
1643
+ }
1644
+
1645
+ resizeCanvas() {
1646
+ const bounds = this.root.getBoundingClientRect();
1647
+ this.dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
1648
+ this.width = Math.max(1, bounds.width);
1649
+ this.height = Math.max(1, bounds.height);
1650
+ this.canvas.width = Math.round(this.width * this.dpr);
1651
+ this.canvas.height = Math.round(this.height * this.dpr);
1652
+ this.canvas.style.width = `${this.width}px`;
1653
+ this.canvas.style.height = `${this.height}px`;
1654
+ this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
1655
+ }
1656
+
1657
+ edgeEnvelope(yRatio) {
1658
+ const edge = Math.sin(Math.PI * clamp(yRatio, 0, 1));
1659
+ return Math.pow(Math.max(edge, 0), 0.48);
1660
+ }
1661
+
1662
+ localBulge(yRatio, time, index) {
1663
+ const direction = index === 0 ? 1 : -1;
1664
+ const center = 0.28 + index * 0.40 + Math.sin(time * (0.19 + index * 0.035) + this.seed * (1.1 + index)) * 0.13;
1665
+ const width = 0.075 + index * 0.016 + Math.sin(time * 0.13 + this.seed * 2.1) * 0.012;
1666
+ const distance = (yRatio - center) / Math.max(width, 0.035);
1667
+ const gaussian = Math.exp(-0.5 * distance * distance);
1668
+ return gaussian * Math.sin(time * (0.71 + index * 0.09) + this.seed * (2.7 + index)) * this.profile.bulgeAmplitude * direction;
1669
+ }
1670
+
1671
+ edgeOffset(y, time, phase = 0, amplitudeScale = 1) {
1672
+ const yRatio = this.height > 0 ? y / this.height : 0;
1673
+ const envelope = this.edgeEnvelope(yRatio);
1674
+ const scaledTime = time * this.profile.timeScale;
1675
+ const phaseTime = this.profile.surge
1676
+ ? scaledTime + this.profile.surge * Math.sin(scaledTime * 2)
1677
+ : scaledTime;
1678
+ let offset = 0;
1679
+
1680
+ for (let index = 0; index < this.profile.cycles.length; index += 1) {
1681
+ const cycle = this.profile.cycles[index];
1682
+ const amplitude = this.profile.amplitudes[index];
1683
+ const speed = this.profile.speeds[index];
1684
+ const amplitudeMotion = 0.74 + 0.26 * Math.sin(
1685
+ phaseTime * (0.17 + index * 0.045) + this.seed * (index + 2.4)
1686
+ );
1687
+ offset += Math.sin(
1688
+ yRatio * Math.PI * 2 * cycle + phaseTime * speed * Math.PI * 2 + this.seed * (index + 1) + phase
1689
+ ) * amplitude * amplitudeMotion;
1690
+ }
1691
+
1692
+ offset += this.localBulge(yRatio, phaseTime + phase, 0);
1693
+ offset += this.localBulge(yRatio, phaseTime - phase * 0.7, 1);
1694
+ return offset * envelope * amplitudeScale;
1695
+ }
1696
+
1697
+ createEdgePath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
1698
+ const step = Math.max(1.8, this.height / 92);
1699
+ ctx.beginPath();
1700
+ for (let y = 0; y <= this.height + step; y += step) {
1701
+ const x = baseX + this.edgeOffset(y, time, phase, amplitudeScale);
1702
+ if (y === 0) ctx.moveTo(x, y);
1703
+ else ctx.lineTo(x, y);
1704
+ }
1705
+ }
1706
+
1707
+ createFillPath(ctx, baseX, time, phase = 0, amplitudeScale = 1) {
1708
+ const step = Math.max(1.8, this.height / 92);
1709
+ ctx.beginPath();
1710
+ ctx.moveTo(0, 0);
1711
+ ctx.lineTo(baseX + this.edgeOffset(0, time, phase, amplitudeScale), 0);
1712
+ for (let y = step; y <= this.height + step; y += step) {
1713
+ ctx.lineTo(baseX + this.edgeOffset(y, time, phase, amplitudeScale), y);
1714
+ }
1715
+ ctx.lineTo(0, this.height);
1716
+ ctx.closePath();
1717
+ }
1718
+
1719
+ drawEllipticalGlow(x, y, radiusX, radiusY, color, alpha) {
1720
+ const ctx = this.ctx;
1721
+ ctx.save();
1722
+ ctx.translate(x, y);
1723
+ ctx.scale(1, radiusY / radiusX);
1724
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
1725
+ gradient.addColorStop(0, hexToRgba(color, alpha));
1726
+ gradient.addColorStop(0.42, hexToRgba(color, alpha * 0.48));
1727
+ gradient.addColorStop(1, hexToRgba(color, 0));
1728
+ ctx.globalCompositeOperation = 'screen';
1729
+ ctx.fillStyle = gradient;
1730
+ ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
1731
+ ctx.restore();
1732
+ }
1733
+
1734
+ drawDarkEllipticalShadow(x, y, radiusX, radiusY, alpha) {
1735
+ const ctx = this.ctx;
1736
+ ctx.save();
1737
+ ctx.translate(x, y);
1738
+ ctx.scale(1, radiusY / radiusX);
1739
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radiusX);
1740
+ gradient.addColorStop(0, `rgba(6, 6, 11, ${alpha})`);
1741
+ gradient.addColorStop(0.54, `rgba(8, 8, 14, ${alpha * 0.62})`);
1742
+ gradient.addColorStop(1, 'rgba(8, 8, 14, 0)');
1743
+ ctx.globalCompositeOperation = 'source-over';
1744
+ ctx.fillStyle = gradient;
1745
+ ctx.fillRect(-radiusX, -radiusX, radiusX * 2, radiusX * 2);
1746
+ ctx.restore();
1747
+ }
1748
+
1749
+ drawColorClouds(shoreline, time, accentA, accentB, glow) {
1750
+ this.ctx;
1751
+ const width = this.width;
1752
+ const height = this.height;
1753
+ const scale = this.profile.cloudWidth;
1754
+ const t = time * this.profile.timeScale;
1755
+
1756
+ const upperY = height * (0.28 + Math.sin(t * 0.24 + this.seed) * 0.13);
1757
+ const lowerY = height * (0.70 + Math.cos(t * 0.21 + this.seed * 1.7) * 0.12);
1758
+ const middleY = height * (0.49 + Math.sin(t * 0.31 + this.seed * 2.3) * 0.15);
1759
+
1760
+ const farX = shoreline - width * 0.095;
1761
+ const farRx = Math.max(52, width * scale);
1762
+ const farRy = height * 0.42;
1763
+ this.drawEllipticalGlow(farX, upperY, farRx, farRy, accentA, 0.38);
1764
+ this.drawDarkEllipticalShadow(
1765
+ farX + farRx * 0.16,
1766
+ upperY,
1767
+ farRx * 0.58,
1768
+ farRy * 0.66,
1769
+ 0.74
1770
+ );
1771
+
1772
+ const lowerX = shoreline - width * 0.072;
1773
+ const lowerRx = Math.max(44, width * scale * 0.82);
1774
+ const lowerRy = height * 0.36;
1775
+ this.drawEllipticalGlow(lowerX, lowerY, lowerRx, lowerRy, accentB, 0.31);
1776
+ this.drawDarkEllipticalShadow(
1777
+ lowerX + lowerRx * 0.14,
1778
+ lowerY,
1779
+ lowerRx * 0.54,
1780
+ lowerRy * 0.62,
1781
+ 0.64
1782
+ );
1783
+
1784
+ this.drawEllipticalGlow(
1785
+ shoreline - width * 0.034,
1786
+ middleY,
1787
+ Math.max(30, width * scale * 0.48),
1788
+ height * 0.27,
1789
+ glow,
1790
+ 0.20
1791
+ );
1792
+ }
1793
+
1794
+ drawPathBand({ baseX, time, phase, amplitudeScale, color, alpha, blur, width, composite = 'screen' }) {
1795
+ const ctx = this.ctx;
1796
+ this.createEdgePath(ctx, baseX, time, phase, amplitudeScale);
1797
+ ctx.save();
1798
+ ctx.globalCompositeOperation = composite;
1799
+ ctx.globalAlpha = alpha;
1800
+ // Safari < 18 and some old WebViews ignore ctx.filter; setting it is a
1801
+ // no-op there, so only assign when supported to keep intent explicit.
1802
+ if (SUPPORTS_CTX_FILTER) ctx.filter = `blur(${blur}px)`;
1803
+ ctx.strokeStyle = color;
1804
+ ctx.lineWidth = width;
1805
+ ctx.stroke();
1806
+ ctx.restore();
1807
+ }
1808
+
1809
+ setRange(min, max) {
1810
+ this.min = min;
1811
+ this.max = max;
1812
+ const clamped = clamp(this.value, this.min, this.max);
1813
+ if (clamped !== this.value) this.setProgress(clamped, 'prop');
1814
+ }
1815
+
1816
+ drawReferenceFlow() {
1817
+ const ctx = this.ctx;
1818
+ const width = this.width;
1819
+ const height = this.height;
1820
+ if (!ctx || width <= 0 || height <= 0) return;
1821
+
1822
+ const shoreline = width * (this.value / 100);
1823
+ const time = this.flowTime;
1824
+ const [dark, accentA, accentB, glow] = this.preset.colors;
1825
+
1826
+ ctx.clearRect(0, 0, width, height);
1827
+ ctx.fillStyle = '#202126';
1828
+ ctx.fillRect(0, 0, width, height);
1829
+
1830
+ this.createFillPath(ctx, shoreline, time, 0, 1);
1831
+ const bodyGradient = ctx.createLinearGradient(0, 0, Math.max(shoreline, 1), 0);
1832
+ bodyGradient.addColorStop(0, dark);
1833
+ bodyGradient.addColorStop(0.74, dark);
1834
+ bodyGradient.addColorStop(0.89, hexToRgba(dark, 0.99));
1835
+ bodyGradient.addColorStop(0.955, hexToRgba(accentA, 0.09));
1836
+ bodyGradient.addColorStop(0.992, hexToRgba(accentB, 0.54));
1837
+ bodyGradient.addColorStop(1, hexToRgba(glow, 0.78));
1838
+ ctx.fillStyle = bodyGradient;
1839
+ ctx.fill();
1840
+
1841
+ this.drawColorClouds(shoreline, time, accentA, accentB, glow);
1842
+
1843
+ this.drawPathBand({
1844
+ baseX: shoreline - width * 0.105,
1845
+ time,
1846
+ phase: 1.42,
1847
+ amplitudeScale: 1.18,
1848
+ color: accentA,
1849
+ alpha: 0.22,
1850
+ blur: 21,
1851
+ width: 54
1852
+ });
1853
+ this.drawPathBand({
1854
+ baseX: shoreline - width * 0.073,
1855
+ time,
1856
+ phase: -0.92,
1857
+ amplitudeScale: 1.06,
1858
+ color: accentB,
1859
+ alpha: 0.30,
1860
+ blur: 15,
1861
+ width: 42
1862
+ });
1863
+ this.drawPathBand({
1864
+ baseX: shoreline - width * 0.047,
1865
+ time,
1866
+ phase: 0.42,
1867
+ amplitudeScale: 0.94,
1868
+ color: 'rgba(5, 5, 10, 0.92)',
1869
+ alpha: 0.72,
1870
+ blur: 12,
1871
+ width: 34,
1872
+ composite: 'source-over'
1873
+ });
1874
+ this.drawPathBand({
1875
+ baseX: shoreline - width * 0.025,
1876
+ time,
1877
+ phase: -0.28,
1878
+ amplitudeScale: 0.96,
1879
+ color: accentB,
1880
+ alpha: 0.66,
1881
+ blur: 8,
1882
+ width: 28
1883
+ });
1884
+
1885
+ ctx.save();
1886
+ this.createFillPath(ctx, shoreline, time, 0, 1);
1887
+ ctx.clip();
1888
+ this.createEdgePath(ctx, shoreline, time, 0, 1);
1889
+
1890
+ ctx.save();
1891
+ ctx.globalCompositeOperation = 'screen';
1892
+ ctx.strokeStyle = hexToRgba(accentA, 0.20);
1893
+ ctx.lineWidth = this.profile.haloWidth;
1894
+ ctx.shadowColor = accentA;
1895
+ ctx.shadowBlur = this.profile.haloWidth * 0.72;
1896
+ ctx.stroke();
1897
+ ctx.restore();
1898
+
1899
+ ctx.save();
1900
+ ctx.globalCompositeOperation = 'screen';
1901
+ ctx.strokeStyle = hexToRgba(accentB, 0.78);
1902
+ ctx.lineWidth = this.profile.glowWidth + 4.2;
1903
+ ctx.shadowColor = accentB;
1904
+ ctx.shadowBlur = 8;
1905
+ ctx.stroke();
1906
+ ctx.restore();
1907
+
1908
+ ctx.save();
1909
+ ctx.globalCompositeOperation = 'screen';
1910
+ ctx.strokeStyle = hexToRgba(glow, 0.88);
1911
+ ctx.lineWidth = this.profile.glowWidth;
1912
+ ctx.shadowColor = glow;
1913
+ ctx.shadowBlur = 5;
1914
+ ctx.stroke();
1915
+ ctx.restore();
1916
+
1917
+ ctx.restore();
1918
+
1919
+ ctx.save();
1920
+ ctx.globalCompositeOperation = 'screen';
1921
+ ctx.strokeStyle = hexToRgba(glow, 0.84);
1922
+ ctx.lineWidth = 2.15;
1923
+ ctx.shadowColor = glow;
1924
+ ctx.shadowBlur = 2.5;
1925
+ this.createEdgePath(ctx, shoreline, time, 0, 1);
1926
+ ctx.stroke();
1927
+ ctx.restore();
1928
+
1929
+ if (this.profile.whiteAlpha > 0.05) {
1930
+ ctx.save();
1931
+ ctx.globalCompositeOperation = 'screen';
1932
+ ctx.strokeStyle = `rgba(255,255,245,${this.profile.whiteAlpha})`;
1933
+ ctx.lineWidth = this.profile.whiteWidth;
1934
+ this.createEdgePath(ctx, shoreline, time, 0, 1);
1935
+ ctx.stroke();
1936
+ ctx.restore();
1937
+ }
1938
+ }
1939
+
1940
+ updateFromPointer(event) {
1941
+ const bounds = this.root.getBoundingClientRect();
1942
+ const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
1943
+ this.setProgress(ratio * 100, 'drag');
1944
+ }
1945
+
1946
+ beginDrag(event) {
1947
+ if (event.button !== undefined && event.button !== 0) return;
1948
+ event.preventDefault();
1949
+ this.dragging = true;
1950
+ this.root.classList.add('is-dragging');
1951
+ try { this.root.setPointerCapture?.(event.pointerId); } catch {}
1952
+ this.emitter.emit('dragstart', { value: this.value });
1953
+ this.updateFromPointer(event);
1954
+ }
1955
+
1956
+ moveDrag(event) {
1957
+ if (!this.dragging) return;
1958
+ event.preventDefault();
1959
+ this.updateFromPointer(event);
1960
+ }
1961
+
1962
+ endDrag(event) {
1963
+ if (!this.dragging) return;
1964
+ this.dragging = false;
1965
+ this.root.classList.remove('is-dragging');
1966
+ try {
1967
+ if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
1968
+ this.root.releasePointerCapture(event.pointerId);
1969
+ }
1970
+ } catch {}
1971
+ this.emitter.emit('dragend', { value: this.value });
1972
+ }
1973
+
1974
+ bindEvents() {
1975
+ if (this.options.draggable !== false) {
1976
+ this.handlers.pointerdown = (event) => this.beginDrag(event);
1977
+ this.handlers.pointermove = (event) => this.moveDrag(event);
1978
+ this.handlers.pointerup = (event) => this.endDrag(event);
1979
+ this.handlers.pointercancel = (event) => this.endDrag(event);
1980
+ this.root.addEventListener('pointerdown', this.handlers.pointerdown);
1981
+ this.root.addEventListener('pointermove', this.handlers.pointermove);
1982
+ this.root.addEventListener('pointerup', this.handlers.pointerup);
1983
+ this.root.addEventListener('pointercancel', this.handlers.pointercancel);
1984
+ }
1985
+
1986
+ if (this.options.keyboard !== false) {
1987
+ this.handlers.keydown = (event) => {
1988
+ const actions = {
1989
+ ArrowLeft: -2,
1990
+ ArrowDown: -2,
1991
+ ArrowRight: 2,
1992
+ ArrowUp: 2,
1993
+ PageDown: -10,
1994
+ PageUp: 10
1995
+ };
1996
+ if (event.key === 'Home') {
1997
+ event.preventDefault();
1998
+ this.setProgress(this.min, 'keyboard');
1999
+ } else if (event.key === 'End') {
2000
+ event.preventDefault();
2001
+ this.setProgress(this.max, 'keyboard');
2002
+ } else if (actions[event.key]) {
2003
+ event.preventDefault();
2004
+ this.setProgress(this.value + actions[event.key], 'keyboard');
2005
+ } else {
2006
+ return;
2007
+ }
2008
+ };
2009
+ this.root.addEventListener('keydown', this.handlers.keydown);
2010
+ }
2011
+ }
2012
+
2013
+ update(delta, paused) {
2014
+ if (!paused) this.flowTime += delta;
2015
+ }
2016
+
2017
+ draw() {
2018
+ // Performance fix: when the WebGL overlay is active the 2D layer is
2019
+ // hidden behind it, so drawing it every frame would be wasted CPU.
2020
+ if (this.webglActive) return;
2021
+ this.drawReferenceFlow();
2022
+ }
2023
+
2024
+ randomize() {
2025
+ this.flowTime = this.random() * 40;
2026
+ }
2027
+
2028
+ setColors(colors) {
2029
+ if (!Array.isArray(colors) || colors.length !== 4) return;
2030
+ this.preset = { ...this.preset, colors };
2031
+ }
2032
+
2033
+ dispose() {
2034
+ if (this.resizeObserver) this.resizeObserver.disconnect();
2035
+ else window.removeEventListener('resize', this.resizeCanvas);
2036
+ for (const name of Object.keys(this.handlers)) {
2037
+ const handler = this.handlers[name];
2038
+ this.root.removeEventListener(name, handler);
2039
+ }
2040
+ this.handlers = {};
2041
+ }
2042
+ }
2043
+
2044
+ /**
2045
+ * Mount a fluid progress capsule into `container`.
2046
+ *
2047
+ * Options: preset (id/code/name or object), width, height, value, min, max,
2048
+ * draggable, keyboard, colors, edgeStyle, quality,
2049
+ * respectReducedMotion, copy, cssVars.
2050
+ */
2051
+ function createProgressCapsule(container, options = {}) {
2052
+ if (!container || typeof container.appendChild !== 'function') {
2053
+ throw new Error('createProgressCapsule: container element is required');
2054
+ }
2055
+
2056
+ const preset = { ...getPreset('progress', options.preset ?? 'NC-10') };
2057
+ const merged = normalizeOptions(DEFAULTS.progress, preset, options);
2058
+ const copy = { ...COPY, ...(merged.copy || {}) };
2059
+
2060
+ const root = document.createElement('div');
2061
+ root.className = 'hj-capsule-root hj-progress-root';
2062
+ root.setAttribute('role', 'slider');
2063
+ root.setAttribute('tabindex', '0');
2064
+ root.setAttribute('data-draggable', String(merged.draggable !== false));
2065
+ root.setAttribute('aria-valuemin', String(merged.min ?? 0));
2066
+ root.setAttribute('aria-valuemax', String(merged.max ?? 100));
2067
+ root.setAttribute('aria-valuenow', String(preset.initialProgress));
2068
+ root.setAttribute(
2069
+ 'aria-label',
2070
+ interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2071
+ );
2072
+ if (merged.keyboard === false) root.setAttribute('tabindex', '-1');
2073
+
2074
+ const canvas = document.createElement('canvas');
2075
+ canvas.className = 'hj-progress-canvas';
2076
+ canvas.setAttribute('aria-hidden', 'true');
2077
+
2078
+ const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
2079
+
2080
+ const copyText = (key, fallback) => {
2081
+ const value = copy[key];
2082
+ return value === undefined ? fallback : value;
2083
+ };
2084
+
2085
+ function buildCopyHtml() {
2086
+ let html = '';
2087
+ const brandText = copy.brandName || '';
2088
+ const subtitleText = copyText('subtitle', preset.subtitle);
2089
+ if (brandText) html += `<span class="hj-progress-name">${brandText}</span>`;
2090
+ if (subtitleText) html += `<span class="hj-progress-subtitle">${subtitleText}</span>`;
2091
+ return html;
2092
+ }
2093
+
2094
+ const copyLayer = document.createElement('div');
2095
+ copyLayer.className = 'hj-progress-copy';
2096
+ const renderCopy = () => {
2097
+ if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
2098
+ };
2099
+ renderCopy();
2100
+
2101
+ if (copy.background) {
2102
+ root.style.setProperty('--hj-progress-copy-bg', copy.background);
2103
+ root.style.setProperty('--hj-progress-copy-pad', '6px 12px');
2104
+ }
2105
+ if (copy.color) root.style.setProperty('--hj-progress-copy-color', copy.color);
2106
+
2107
+ const valueElement = document.createElement('div');
2108
+ valueElement.className = 'hj-progress-value';
2109
+ valueElement.setAttribute('aria-hidden', 'true');
2110
+
2111
+ const dragLabel = document.createElement('span');
2112
+ dragLabel.className = 'hj-progress-drag-label';
2113
+ dragLabel.setAttribute('aria-hidden', 'true');
2114
+ dragLabel.textContent = copy.dragLabel;
2115
+
2116
+ root.appendChild(canvas);
2117
+ if (copyEnabled) root.appendChild(copyLayer);
2118
+ root.appendChild(valueElement);
2119
+ root.appendChild(dragLabel);
2120
+ container.appendChild(root);
2121
+
2122
+ const emitter = createEmitter();
2123
+ let paused = merged.respectReducedMotion && prefersReducedMotion();
2124
+
2125
+ const applySize = () => {
2126
+ root.style.width = parseSize(merged.width);
2127
+ root.style.height = parseSize(merged.height);
2128
+ const vars = merged.cssVars || {};
2129
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2130
+ };
2131
+ applySize();
2132
+
2133
+ const controller = new ProgressCapsuleController({
2134
+ root,
2135
+ canvas,
2136
+ valueElement,
2137
+ preset,
2138
+ emitter,
2139
+ options: merged,
2140
+ copy
2141
+ });
2142
+
2143
+ let overlay = null;
2144
+ if (merged.renderer !== 'canvas2d') {
2145
+ overlay = attachProgressFlowOverlay({
2146
+ root,
2147
+ canvas,
2148
+ preset,
2149
+ getProgress: () => controller.value
2150
+ });
2151
+ }
2152
+ if (overlay) controller.webglActive = true;
2153
+
2154
+ const visibility = createVisibilityGuard(root);
2155
+ let flowTime = 0;
2156
+ const unsubscribe = subscribeScheduler(
2157
+ (delta, now) => {
2158
+ flowTime += delta;
2159
+ controller.update(delta, false);
2160
+ if (visibility.isVisible()) {
2161
+ controller.draw();
2162
+ if (overlay) overlay.update(flowTime);
2163
+ }
2164
+ },
2165
+ () => paused
2166
+ );
2167
+
2168
+ emitter.emit('ready', { preset: { ...preset } });
2169
+
2170
+ const syncDom = () => {
2171
+ root.setAttribute(
2172
+ 'aria-label',
2173
+ interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2174
+ );
2175
+ renderCopy();
2176
+ };
2177
+
2178
+ return {
2179
+ element: root,
2180
+ canvas,
2181
+ preset,
2182
+ on: emitter.on,
2183
+ off: emitter.off,
2184
+ setValue(value, source = 'prop') {
2185
+ controller.setProgress(value, source);
2186
+ return this;
2187
+ },
2188
+ getValue() {
2189
+ return controller.value;
2190
+ },
2191
+ setRange(min, max) {
2192
+ controller.setRange(min, max);
2193
+ return this;
2194
+ },
2195
+ setPreset(ref) {
2196
+ const next = getPreset('progress', ref);
2197
+ Object.assign(preset, next);
2198
+ controller.preset = next;
2199
+ controller.profile = next.edgeStyle === 'tide'
2200
+ ? FLOW_PROFILES.tide
2201
+ : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
2202
+ controller.flowTime = stringSeed(next.id) * 31;
2203
+ controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
2204
+ syncDom();
2205
+ if (overlay) {
2206
+ overlay.dispose();
2207
+ overlay = attachProgressFlowOverlay({ root, canvas, preset: next, getProgress: () => controller.value });
2208
+ }
2209
+ controller.webglActive = Boolean(overlay);
2210
+ controller.resizeCanvas();
2211
+ emitter.emit('presetchange', { preset: { ...next } });
2212
+ return this;
2213
+ },
2214
+ setColors(colors) {
2215
+ if (!Array.isArray(colors) || colors.length !== 4) return this;
2216
+ controller.setColors(colors);
2217
+ if (overlay) overlay.setColors(colors);
2218
+ controller.resizeCanvas();
2219
+ return this;
2220
+ },
2221
+ setSize(width, height) {
2222
+ if (width !== undefined) merged.width = width;
2223
+ if (height !== undefined) merged.height = height;
2224
+ applySize();
2225
+ controller.resizeCanvas();
2226
+ return this;
2227
+ },
2228
+ randomize() {
2229
+ controller.randomize();
2230
+ return this;
2231
+ },
2232
+ pause() {
2233
+ paused = true;
2234
+ return this;
2235
+ },
2236
+ resume() {
2237
+ paused = false;
2238
+ return this;
2239
+ },
2240
+ setQuality(quality) {
2241
+ merged.quality = quality;
2242
+ controller.dprCap = dprCapFor(quality);
2243
+ controller.resizeCanvas();
2244
+ return this;
2245
+ },
2246
+ dispose() {
2247
+ unsubscribe();
2248
+ visibility.dispose();
2249
+ if (overlay) overlay.dispose();
2250
+ controller.dispose();
2251
+ root.remove();
2252
+ }
2253
+ };
2254
+ }
2255
+
2256
+ /**
2257
+ * Shared Options-API wrapper factory. The only framework difference is the
2258
+ * render function: Vue2 passes `h` as an argument, Vue3 needs the imported
2259
+ * `h` (see vue2.js / vue3.js). Lifecycle options cover both.
2260
+ */
2261
+
2262
+ const CAPSULE_EVENTS = [
2263
+ 'ready', 'error', 'click', 'pointerdown', 'pointerup', 'dblclick',
2264
+ 'pointerenter', 'pointerleave', 'presetchange'
2265
+ ];
2266
+ const PROGRESS_EVENTS = ['ready', 'error', 'change', 'dragstart', 'dragend', 'presetchange'];
2267
+
2268
+ function hookEvents(controller, events, emit) {
2269
+ for (const event of events) {
2270
+ controller.on(event, (payload) => emit(event, payload));
2271
+ }
2272
+ }
2273
+
2274
+ function makeWrapper({ name, props, events, create, render, methods, watch }) {
2275
+ const internalMethods = {
2276
+ mountController() {
2277
+ this.controller = create(this.$el, this.$props);
2278
+ hookEvents(this.controller, events, (event, payload) => this.$emit(event, payload));
2279
+ },
2280
+ recreate() {
2281
+ if (this.controller) this.controller.dispose();
2282
+ if (this.$el) this.$el.innerHTML = '';
2283
+ this.mountController();
2284
+ },
2285
+ scheduleRecreate() {
2286
+ if (this._recreateQueued) return;
2287
+ this._recreateQueued = true;
2288
+ this.$nextTick(() => {
2289
+ this._recreateQueued = false;
2290
+ this.recreate();
2291
+ });
2292
+ }
2293
+ };
2294
+
2295
+ return {
2296
+ name,
2297
+ props,
2298
+ emits: events,
2299
+ render,
2300
+ mounted() {
2301
+ this.mountController();
2302
+ },
2303
+ beforeUnmount() {
2304
+ if (this.controller) this.controller.dispose();
2305
+ },
2306
+ beforeDestroy() {
2307
+ if (this.controller) this.controller.dispose();
2308
+ },
2309
+ watch: {
2310
+ preset(value) { if (value && this.controller) this.controller.setPreset(value); },
2311
+ colors(value) { if (value && this.controller) this.controller.setColors(value); },
2312
+ width(value) { if (value !== undefined && this.controller) this.controller.setSize(value); },
2313
+ height(value) { if (value !== undefined && this.controller) this.controller.setSize(undefined, value); },
2314
+ quality(value) { if (value && this.controller) this.controller.setQuality(value); },
2315
+ ...(watch || {})
2316
+ },
2317
+ methods: { ...internalMethods, ...(methods || {}) }
2318
+ };
2319
+ }
2320
+
2321
+ function makeCapsuleWrapper(createCapsule, render) {
2322
+ return makeWrapper({
2323
+ name: 'CosmicCapsule',
2324
+ props: [
2325
+ 'preset', 'width', 'height', 'colors', 'seed', 'speed', 'interactive',
2326
+ 'mouseColor', 'renderer', 'quality', 'respectReducedMotion', 'copy', 'cssVars'
2327
+ ],
2328
+ events: CAPSULE_EVENTS,
2329
+ create: createCapsule,
2330
+ render,
2331
+ watch: {
2332
+ seed(value) {
2333
+ const seed = Number(value);
2334
+ if (this.controller && Number.isFinite(seed)) {
2335
+ this.controller.setPreset({ ...this.controller.preset, seed });
2336
+ }
2337
+ },
2338
+ speed(value) {
2339
+ const speed = Number(value);
2340
+ if (this.controller && Number.isFinite(speed)) {
2341
+ this.controller.setPreset({ ...this.controller.preset, speed });
2342
+ }
2343
+ },
2344
+ interactive() {
2345
+ this.scheduleRecreate();
2346
+ },
2347
+ mouseColor() {
2348
+ this.scheduleRecreate();
2349
+ },
2350
+ renderer() {
2351
+ this.scheduleRecreate();
2352
+ }
2353
+ },
2354
+ methods: {
2355
+ randomize() { return this.controller && this.controller.randomize(); },
2356
+ pause() { return this.controller && this.controller.pause(); },
2357
+ resume() { return this.controller && this.controller.resume(); },
2358
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
2359
+ setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
2360
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
2361
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
2362
+ getController() { return this.controller; }
2363
+ }
2364
+ });
2365
+ }
2366
+
2367
+ function makeProgressWrapper(createProgressCapsule, render) {
2368
+ return makeWrapper({
2369
+ name: 'ProgressCapsule',
2370
+ props: [
2371
+ 'preset', 'width', 'height', 'value', 'min', 'max',
2372
+ 'draggable', 'keyboard', 'edgeStyle', 'colors', 'quality', 'renderer',
2373
+ 'respectReducedMotion', 'copy', 'cssVars'
2374
+ ],
2375
+ events: PROGRESS_EVENTS,
2376
+ create: createProgressCapsule,
2377
+ render,
2378
+ watch: {
2379
+ value(value) {
2380
+ if (value !== undefined && this.controller) this.controller.setValue(value);
2381
+ },
2382
+ min(value) {
2383
+ if (value !== undefined && this.controller) this.controller.setRange(value, this.controller.max);
2384
+ },
2385
+ max(value) {
2386
+ if (value !== undefined && this.controller) this.controller.setRange(this.controller.min, value);
2387
+ },
2388
+ edgeStyle(value) {
2389
+ if (value && this.controller) this.controller.setPreset({ ...this.controller.preset, edgeStyle: value });
2390
+ },
2391
+ draggable() {
2392
+ this.scheduleRecreate();
2393
+ },
2394
+ keyboard() {
2395
+ this.scheduleRecreate();
2396
+ },
2397
+ renderer() {
2398
+ this.scheduleRecreate();
2399
+ }
2400
+ },
2401
+ methods: {
2402
+ setValue(value) { return this.controller && this.controller.setValue(value); },
2403
+ getValue() { return this.controller ? this.controller.getValue() : undefined; },
2404
+ setRange(min, max) { return this.controller && this.controller.setRange(min, max); },
2405
+ randomize() { return this.controller && this.controller.randomize(); },
2406
+ pause() { return this.controller && this.controller.pause(); },
2407
+ resume() { return this.controller && this.controller.resume(); },
2408
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
2409
+ setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
2410
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
2411
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
2412
+ getController() { return this.controller; }
2413
+ }
2414
+ });
2415
+ }
2416
+
2417
+ // Vue3 does NOT pass `h` to the render option; it must be imported.
2418
+ const render = () => h('div', { class: 'hj-vue-host' });
2419
+
2420
+ const CosmicCapsule = makeCapsuleWrapper(createCapsule, render);
2421
+ const ProgressCapsule = makeProgressWrapper(createProgressCapsule, render);
2422
+
2423
+ var vue3 = {
2424
+ CosmicCapsule,
2425
+ ProgressCapsule
2426
+ };
2427
+
2428
+ export { CosmicCapsule, ProgressCapsule, vue3 as default };