@5even7/dlc-ui 0.1.0 → 0.2.1

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 CHANGED
@@ -114,50 +114,50 @@ const PALETTES$1 = {
114
114
  * Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
115
115
  * 颜色引用公共色板,seed/speed 决定形态与流速。
116
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] }
117
+ const CAPSULE_PRESETS = [
118
+ { id: 'original', code: 'NC-01', name: '初光', group: 'warm', seed: 1.7, speed: 0.5, colors: [...PALETTES$1.original] },
119
+ { id: 'ocean', code: 'NC-02', name: '沧溟', group: 'cold', seed: 8.2, speed: 0.48, colors: [...PALETTES$1.ocean] },
120
+ { id: 'klein', code: 'NC-03', name: '玄夜', group: 'cold', seed: 14.1, speed: 0.49, colors: [...PALETTES$1.klein] },
121
+ { id: 'ultraviolet', code: 'NC-04', name: '紫霄', group: 'cold', seed: 23.4, speed: 0.47, colors: [...PALETTES$1.ultraviolet] },
122
+ { id: 'chrome', code: 'NC-05', name: '银汉', group: 'cold', seed: 37.8, speed: 0.42, colors: [...PALETTES$1.chrome] },
123
+ { id: 'plus', code: 'NC-06', name: '熔金', group: 'warm', seed: 51.3, speed: 0.5, colors: [...PALETTES$1.plus] }
124
124
  ];
125
125
 
126
126
  const PROGRESS_PRESETS = [
127
- {
128
- id: 'model-training',
129
- code: 'NC-10',
130
- name: 'MODEL TRAINING',
127
+ {
128
+ id: 'model-training',
129
+ code: 'NC-10',
130
+ name: '星火',
131
131
  subtitle: 'SHA 4.5 + 100 2026 TKN',
132
132
  group: 'progress',
133
133
  initialProgress: 43,
134
134
  edgeStyle: 'flow',
135
135
  colors: ['#2B1025', '#FF3F94', '#FF8A3D', '#FFF06A']
136
136
  },
137
- {
138
- id: 'agent-migration',
139
- code: 'NC-11',
140
- name: 'AGENT MIGRATION',
137
+ {
138
+ id: 'agent-migration',
139
+ code: 'NC-11',
140
+ name: '迁流',
141
141
  subtitle: 'TRANSFERRING PROTOCOL',
142
142
  group: 'progress',
143
143
  initialProgress: 33,
144
144
  edgeStyle: 'flow',
145
145
  colors: ['#101C37', '#245BFF', '#00CFFF', '#5DFFE6']
146
146
  },
147
- {
148
- id: 'visual-training',
149
- code: 'NC-12',
150
- name: 'VISUAL TRAINING',
147
+ {
148
+ id: 'visual-training',
149
+ code: 'NC-12',
150
+ name: '幻境',
151
151
  subtitle: 'GENERATING POWER ++',
152
152
  group: 'progress',
153
153
  initialProgress: 58,
154
154
  edgeStyle: 'flow',
155
155
  colors: ['#21142D', '#7042FF', '#42F58D', '#C4FF8A']
156
156
  },
157
- {
158
- id: 'tide',
159
- code: 'NC-13',
160
- name: 'TIDE',
157
+ {
158
+ id: 'tide',
159
+ code: 'NC-13',
160
+ name: '汐潮',
161
161
  subtitle: 'MOON PULL / COAST',
162
162
  group: 'progress',
163
163
  initialProgress: 30,
@@ -166,109 +166,284 @@ const PROGRESS_PRESETS = [
166
166
  }
167
167
  ];
168
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;
169
+ [...CAPSULE_PRESETS, ...PROGRESS_PRESETS];
170
+
171
+ /**
172
+ * Resolve a preset by its literary name (e.g. '沧溟').
173
+ * Matching is case-insensitive and ignores surrounding whitespace,
174
+ * so '沧溟' and ' 沧溟 ' both work.
175
+ */
176
+ function getPreset(kind, ref) {
177
+ const list = kind === 'capsule' ? CAPSULE_PRESETS : kind === 'progress' ? PROGRESS_PRESETS : null;
178
+ if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
179
+ const key = String(ref).trim().toLowerCase();
180
+ const found = list.find(
181
+ (preset) => preset.name.toLowerCase() === key
182
+ );
183
+ if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
184
+ return found;
215
185
  }
216
186
 
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,
187
+ const DEFAULTS = {
188
+ capsule: {
189
+ width: '100%',
190
+ height: 160,
191
+ quality: 'auto',
192
+ renderer: 'auto',
193
+ respectReducedMotion: true,
194
+ mouseColor: true,
195
+ textRatio: 39
196
+ },
197
+ progress: {
198
+ width: 454,
199
+ height: 104,
200
+ min: 0,
232
201
  max: 100,
233
- draggable: true,
234
- keyboard: true,
235
- edgeStyle: 'flow',
236
- quality: 'auto',
237
- renderer: 'auto',
238
- showCopy: true,
239
- respectReducedMotion: true
240
- }
202
+ draggable: true,
203
+ keyboard: true,
204
+ disabled: false,
205
+ valueSuffix: '%',
206
+ edgeStyle: 'flow',
207
+ quality: 'auto',
208
+ renderer: 'auto',
209
+ textRatio: 54,
210
+ showValue: true,
211
+ respectReducedMotion: true
212
+ }
241
213
  };
242
214
 
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: '%',
215
+ /**
216
+ * Internal accessibility labels and small UI text. Since the copy API was
217
+ * removed (slots own the text area), these are no longer user-overridable.
218
+ * Templates use {brand} {code} {name} placeholders.
219
+ */
220
+ const COPY = {
221
+ brandName: '画境观屿',
222
+ valueSuffix: '%',
251
223
  progressAria: '{brand} {code} 加载进度',
252
224
  capsuleAria: '打开 {name} 沉浸预览'
253
225
  };
254
226
 
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})`;
227
+ /**
228
+ * Color helpers. `normalizeColor` converts any supported CSS color
229
+ * (hex / named / rgb() / rgba()) into a 6-digit hex string, so renderers can
230
+ * keep working with #rrggbb only. rgba() alpha is intentionally dropped:
231
+ * the WebGL shader has no per-color alpha channel, and the component is
232
+ * opaque by design use component-level `opacity` for transparency.
233
+ */
234
+
235
+ const NAMED_COLORS = {
236
+ aliceblue: '#f0f8ff',
237
+ antiquewhite: '#faebd7',
238
+ aqua: '#00ffff',
239
+ aquamarine: '#7fffd4',
240
+ azure: '#f0ffff',
241
+ beige: '#f5f5dc',
242
+ bisque: '#ffe4c4',
243
+ black: '#000000',
244
+ blanchedalmond: '#ffebcd',
245
+ blue: '#0000ff',
246
+ blueviolet: '#8a2be2',
247
+ brown: '#a52a2a',
248
+ burlywood: '#deb887',
249
+ cadetblue: '#5f9ea0',
250
+ chartreuse: '#7fff00',
251
+ chocolate: '#d2691e',
252
+ coral: '#ff7f50',
253
+ cornflowerblue: '#6495ed',
254
+ cornsilk: '#fff8dc',
255
+ crimson: '#dc143c',
256
+ cyan: '#00ffff',
257
+ darkblue: '#00008b',
258
+ darkcyan: '#008b8b',
259
+ darkgoldenrod: '#b8860b',
260
+ darkgray: '#a9a9a9',
261
+ darkgreen: '#006400',
262
+ darkgrey: '#a9a9a9',
263
+ darkkhaki: '#bdb76b',
264
+ darkmagenta: '#8b008b',
265
+ darkolivegreen: '#556b2f',
266
+ darkorange: '#ff8c00',
267
+ darkorchid: '#9932cc',
268
+ darkred: '#8b0000',
269
+ darksalmon: '#e9967a',
270
+ darkseagreen: '#8fbc8f',
271
+ darkslateblue: '#483d8b',
272
+ darkslategray: '#2f4f4f',
273
+ darkslategrey: '#2f4f4f',
274
+ darkturquoise: '#00ced1',
275
+ darkviolet: '#9400d3',
276
+ deeppink: '#ff1493',
277
+ deepskyblue: '#00bfff',
278
+ dimgray: '#696969',
279
+ dimgrey: '#696969',
280
+ dodgerblue: '#1e90ff',
281
+ firebrick: '#b22222',
282
+ floralwhite: '#fffaf0',
283
+ forestgreen: '#228b22',
284
+ fuchsia: '#ff00ff',
285
+ gainsboro: '#dcdcdc',
286
+ ghostwhite: '#f8f8ff',
287
+ gold: '#ffd700',
288
+ goldenrod: '#daa520',
289
+ gray: '#808080',
290
+ green: '#008000',
291
+ greenyellow: '#adff2f',
292
+ grey: '#808080',
293
+ honeydew: '#f0fff0',
294
+ hotpink: '#ff69b4',
295
+ indianred: '#cd5c5c',
296
+ indigo: '#4b0082',
297
+ ivory: '#fffff0',
298
+ khaki: '#f0e68c',
299
+ lavender: '#e6e6fa',
300
+ lavenderblush: '#fff0f5',
301
+ lawngreen: '#7cfc00',
302
+ lemonchiffon: '#fffacd',
303
+ lightblue: '#add8e6',
304
+ lightcoral: '#f08080',
305
+ lightcyan: '#e0ffff',
306
+ lightgoldenrodyellow: '#fafad2',
307
+ lightgray: '#d3d3d3',
308
+ lightgreen: '#90ee90',
309
+ lightgrey: '#d3d3d3',
310
+ lightpink: '#ffb6c1',
311
+ lightsalmon: '#ffa07a',
312
+ lightseagreen: '#20b2aa',
313
+ lightskyblue: '#87cefa',
314
+ lightslategray: '#778899',
315
+ lightslategrey: '#778899',
316
+ lightsteelblue: '#b0c4de',
317
+ lightyellow: '#ffffe0',
318
+ lime: '#00ff00',
319
+ limegreen: '#32cd32',
320
+ linen: '#faf0e6',
321
+ magenta: '#ff00ff',
322
+ maroon: '#800000',
323
+ mediumaquamarine: '#66cdaa',
324
+ mediumblue: '#0000cd',
325
+ mediumorchid: '#ba55d3',
326
+ mediumpurple: '#9370db',
327
+ mediumseagreen: '#3cb371',
328
+ mediumslateblue: '#7b68ee',
329
+ mediumspringgreen: '#00fa9a',
330
+ mediumturquoise: '#48d1cc',
331
+ mediumvioletred: '#c71585',
332
+ midnightblue: '#191970',
333
+ mintcream: '#f5fffa',
334
+ mistyrose: '#ffe4e1',
335
+ moccasin: '#ffe4b5',
336
+ navajowhite: '#ffdead',
337
+ navy: '#000080',
338
+ oldlace: '#fdf5e6',
339
+ olive: '#808000',
340
+ olivedrab: '#6b8e23',
341
+ orange: '#ffa500',
342
+ orangered: '#ff4500',
343
+ orchid: '#da70d6',
344
+ palegoldenrod: '#eee8aa',
345
+ palegreen: '#98fb98',
346
+ paleturquoise: '#afeeee',
347
+ palevioletred: '#db7093',
348
+ papayawhip: '#ffefd5',
349
+ peachpuff: '#ffdab9',
350
+ peru: '#cd853f',
351
+ pink: '#ffc0cb',
352
+ plum: '#dda0dd',
353
+ powderblue: '#b0e0e6',
354
+ purple: '#800080',
355
+ rebeccapurple: '#663399',
356
+ red: '#ff0000',
357
+ rosybrown: '#bc8f8f',
358
+ royalblue: '#4169e1',
359
+ saddlebrown: '#8b4513',
360
+ salmon: '#fa8072',
361
+ sandybrown: '#f4a460',
362
+ seagreen: '#2e8b57',
363
+ seashell: '#fff5ee',
364
+ sienna: '#a0522d',
365
+ silver: '#c0c0c0',
366
+ skyblue: '#87ceeb',
367
+ slateblue: '#6a5acd',
368
+ slategray: '#708090',
369
+ slategrey: '#708090',
370
+ snow: '#fffafa',
371
+ springgreen: '#00ff7f',
372
+ steelblue: '#4682b4',
373
+ tan: '#d2b48c',
374
+ teal: '#008080',
375
+ thistle: '#d8bfd8',
376
+ tomato: '#ff6347',
377
+ turquoise: '#40e0d0',
378
+ violet: '#ee82ee',
379
+ wheat: '#f5deb3',
380
+ white: '#ffffff',
381
+ whitesmoke: '#f5f5f5',
382
+ yellow: '#ffff00',
383
+ yellowgreen: '#9acd32'
384
+ };
385
+
386
+ function clampChannel(value) {
387
+ return Math.min(255, Math.max(0, Math.round(value)));
388
+ }
389
+
390
+ function normalizeRgbArgs(args) {
391
+ const parts = args.split(/[,\s/]+/).filter(Boolean);
392
+ if (parts.length < 3) return null;
393
+ const to255 = (value) => {
394
+ if (typeof value !== 'string' || !value) return null;
395
+ if (value.endsWith('%')) return clampChannel((parseFloat(value) / 100) * 255);
396
+ const number = Number(value);
397
+ return Number.isFinite(number) ? clampChannel(number) : null;
398
+ };
399
+ const red = to255(parts[0]);
400
+ const green = to255(parts[1]);
401
+ const blue = to255(parts[2]);
402
+ if (red === null || green === null || blue === null) return null;
403
+ return `#${[red, green, blue].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
404
+ }
405
+
406
+ /**
407
+ * Convert any supported CSS color into `#rrggbb`. Returns null when the
408
+ * value cannot be parsed. Supports: #rgb / #rrggbb (case-insensitive),
409
+ * CSS named colors, rgb() / rgba() with comma or modern space syntax,
410
+ * including percentages. Alpha in rgba() is ignored.
411
+ */
412
+ function normalizeColor(value) {
413
+ if (typeof value !== 'string') return null;
414
+ const input = value.trim();
415
+ if (!input) return null;
416
+ const lower = input.toLowerCase();
417
+ if (NAMED_COLORS[lower]) return NAMED_COLORS[lower];
418
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(input)) {
419
+ const hex = input.slice(1).toLowerCase();
420
+ if (hex.length === 3) return `#${hex.split('').map((c) => c + c).join('')}`;
421
+ return `#${hex}`;
422
+ }
423
+ const match = input.match(/^rgba?\((.*)\)$/i);
424
+ if (match) return normalizeRgbArgs(match[1]);
425
+ return null;
426
+ }
427
+
428
+ function hexToRgb01$1(color) {
429
+ const hex = normalizeColor(color);
430
+ if (!hex) return [0, 0, 0];
431
+ const value = Number.parseInt(hex.slice(1), 16);
432
+ return [
433
+ ((value >> 16) & 255) / 255,
434
+ ((value >> 8) & 255) / 255,
435
+ (value & 255) / 255
436
+ ];
437
+ }
438
+
439
+ function hexToRgba(color, alpha = 1) {
440
+ const hex = normalizeColor(color);
441
+ if (!hex) return 'rgba(0, 0, 0, 0)';
442
+ const value = Number.parseInt(hex.slice(1), 16);
443
+ const red = (value >> 16) & 255;
444
+ const green = (value >> 8) & 255;
445
+ const blue = value & 255;
446
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
272
447
  }
273
448
 
274
449
  const VERTEX_SHADER$1 = `#version 300 es
@@ -496,12 +671,31 @@ class CosmicRenderer {
496
671
  this.timeOffset = preset.seed * 0.73;
497
672
  }
498
673
 
499
- setDprCap(cap) {
500
- this.options.dprCap = cap;
501
- this.resize();
502
- }
503
-
504
- randomize() {
674
+ setDprCap(cap) {
675
+ this.options.dprCap = cap;
676
+ this.resize();
677
+ }
678
+
679
+ setMouseColor(enabled) {
680
+ this.options.mouseColor = enabled !== false;
681
+ if (this.options.mouseColor) {
682
+ if (this.onPointerMove) return this;
683
+ this.#bindEvents();
684
+ return this;
685
+ }
686
+ if (this.onPointerMove) {
687
+ const target = this.eventTarget;
688
+ target.removeEventListener('pointermove', this.onPointerMove);
689
+ target.removeEventListener('pointerdown', this.onPointerMove);
690
+ target.removeEventListener('pointerleave', this.onPointerLeave);
691
+ this.onPointerMove = null;
692
+ this.onPointerLeave = null;
693
+ }
694
+ this.motionTarget = 0;
695
+ return this;
696
+ }
697
+
698
+ randomize() {
505
699
  this.preset.seed = Math.random() * 100;
506
700
  this.timeOffset = Math.random() * 40;
507
701
  }
@@ -581,10 +775,12 @@ class FallbackRenderer {
581
775
  }
582
776
  }
583
777
 
584
- drawNebula(time) {
585
- const ctx = this.context;
586
- const { width, height } = this.canvas;
587
- const gradient = ctx.createLinearGradient(0, 0, width, height);
778
+ drawNebula(time) {
779
+ const ctx = this.context;
780
+ const { width, height } = this.canvas;
781
+ const t = time * (this.preset.speed || 1);
782
+ const phase = (this.preset.seed || 0) * 0.7;
783
+ const gradient = ctx.createLinearGradient(0, 0, width, height);
588
784
  gradient.addColorStop(0, this.preset.colors[0]);
589
785
  gradient.addColorStop(0.38, this.preset.colors[1]);
590
786
  gradient.addColorStop(0.72, this.preset.colors[2]);
@@ -592,10 +788,10 @@ class FallbackRenderer {
592
788
  ctx.fillStyle = gradient;
593
789
  ctx.fillRect(0, 0, width, height);
594
790
 
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;
791
+ ctx.globalCompositeOperation = 'screen';
792
+ for (let index = 0; index < 6; index += 1) {
793
+ const x = (0.5 + 0.45 * Math.sin(t * 0.32 + index * 1.7 + phase)) * width;
794
+ const y = (0.5 + 0.4 * Math.cos(t * 0.25 + index + phase)) * height;
599
795
  const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
600
796
  const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
601
797
  glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
@@ -616,8 +812,11 @@ class FallbackRenderer {
616
812
  this.preset = preset;
617
813
  }
618
814
 
619
- randomize() {}
620
- dispose() {}
815
+ randomize() {
816
+ if (this.preset) this.preset.seed = Math.random() * 100;
817
+ }
818
+ setMouseColor() {}
819
+ dispose() {}
621
820
  }
622
821
 
623
822
  /**
@@ -717,78 +916,105 @@ function createVisibilityGuard(element) {
717
916
  };
718
917
  }
719
918
 
720
- function prefersReducedMotion$1() {
919
+ /**
920
+ * Run a callback asynchronously, right after the current task and before the
921
+ * next paint. Used for mount-time events (ready / error) so listeners that
922
+ * are attached after create() still receive them. Falls back for legacy
923
+ * browsers that lack queueMicrotask / Promise.
924
+ */
925
+ function nextTick(fn) {
926
+ if (typeof queueMicrotask === 'function') {
927
+ queueMicrotask(fn);
928
+ } else if (typeof Promise !== 'undefined' && typeof Promise.resolve === 'function') {
929
+ Promise.resolve().then(fn);
930
+ } else {
931
+ setTimeout(fn, 0);
932
+ }
933
+ }
934
+
935
+ function prefersReducedMotion$2() {
721
936
  return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
722
937
  }
723
938
 
724
939
  /**
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 = {}) {
940
+ * Mount a cosmic (nebula) capsule into `container`.
941
+ *
942
+ * Options: preset (literary name), width, height (number=px or string with
943
+ * px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
944
+ * width in percent), text (HTML string or DOM nodes for the text slot),
945
+ * colorContent (HTML string or DOM nodes for the color slot), quality,
946
+ * renderer, respectReducedMotion, mouseColor, cssVars.
947
+ */
948
+ function createCapsule(container, options = {}) {
732
949
  if (!container || typeof container.appendChild !== 'function') {
733
950
  throw new Error('createCapsule: container element is required');
734
951
  }
735
952
 
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();
953
+ const preset = { ...getPreset('capsule', options.preset ?? '初光') };
954
+ const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
955
+ const normalizedColors = merged.colors.map(normalizeColor);
956
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
957
+ preset.seed = merged.seed;
958
+ preset.speed = merged.speed;
959
+ let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
960
+
961
+ const root = document.createElement('div');
962
+ root.className = 'hj-capsule-root hj-capsule-cosmic';
963
+ root.dataset.group = preset.group;
964
+ root.dataset.mode = 'nebula';
965
+ root.dataset.theme = preset.theme || 'light';
966
+ root.setAttribute('role', 'img');
967
+ const updateAria = () => {
968
+ root.setAttribute('aria-label', customLabel || COPY.capsuleAria.replace('{name}', preset.name));
969
+ };
970
+ updateAria();
971
+
972
+ // Slot containers are always present but transparent by default: without
973
+ // content they are invisible, so "no slot" behaves like the old
974
+ // showCopy=false. They only provide geometry — no typography, padding or
975
+ // background is imposed on user content (see docs: 插槽 CSS 约定).
976
+ const fillContent = (layer, content) => {
977
+ layer.innerHTML = '';
978
+ if (content == null) return;
979
+ if (typeof content === 'string') {
980
+ layer.innerHTML = content;
981
+ return;
982
+ }
983
+ const nodes = Array.isArray(content) ? content : [content];
984
+ for (const node of nodes) {
985
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
986
+ }
987
+ };
988
+
989
+ const textLayer = document.createElement('div');
990
+ textLayer.className = 'hj-capsule-text hj-capsule-copy';
991
+
992
+ const visualLayer = document.createElement('div');
993
+ visualLayer.className = 'hj-capsule-visual';
994
+
995
+ fillContent(textLayer, options.text);
996
+ fillContent(visualLayer, options.colorContent);
997
+
998
+ const canvas = document.createElement('canvas');
999
+ canvas.className = 'hj-capsule-canvas';
1000
+ canvas.setAttribute('aria-hidden', 'true');
1001
+
1002
+ root.appendChild(textLayer);
1003
+ root.appendChild(visualLayer);
1004
+ root.appendChild(canvas);
1005
+ container.appendChild(root);
1006
+
1007
+ const emitter = createEmitter();
1008
+ let paused = merged.respectReducedMotion && prefersReducedMotion$2();
1009
+ let disposed = false;
1010
+ const dirty = {
1011
+ preset: false,
1012
+ seed: false,
1013
+ speed: false,
1014
+ colors: false,
1015
+ textRatio: false,
1016
+ cssVars: false
1017
+ };
792
1018
 
793
1019
  let renderer;
794
1020
  const useWebgl = merged.renderer !== 'canvas2d';
@@ -797,19 +1023,25 @@ function createCapsule(container, options = {}) {
797
1023
  renderer = new CosmicRenderer(canvas, merged, { dprCap: dprCapFor(merged.quality) });
798
1024
  } catch (error) {
799
1025
  renderer = new FallbackRenderer(canvas, merged);
800
- emitter.emit('error', { message: String(error && error.message ? error.message : error) });
1026
+ nextTick(() => {
1027
+ if (disposed) return;
1028
+ emitter.emit('error', { message: String(error && error.message ? error.message : error) });
1029
+ });
801
1030
  }
802
1031
  } else {
803
1032
  renderer = new FallbackRenderer(canvas, merged);
804
1033
  }
805
1034
 
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
- };
1035
+ const applySize = () => {
1036
+ root.style.width = parseSize(merged.width);
1037
+ root.style.height = parseSize(merged.height);
1038
+ const vars = merged.cssVars || {};
1039
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1040
+ if (merged.textRatio !== undefined) {
1041
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
1042
+ }
1043
+ renderer.resize();
1044
+ };
813
1045
  applySize();
814
1046
 
815
1047
  const resizeObserver = typeof ResizeObserver !== 'undefined'
@@ -820,10 +1052,8 @@ function createCapsule(container, options = {}) {
820
1052
 
821
1053
  const visibility = createVisibilityGuard(root);
822
1054
 
823
- if (merged.interactive !== false) {
824
- root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
825
- root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
826
- }
1055
+ root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
1056
+ root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
827
1057
  root.addEventListener('click', (event) => {
828
1058
  emitter.emit('click', { event, preset: { ...preset } });
829
1059
  });
@@ -840,45 +1070,97 @@ function createCapsule(container, options = {}) {
840
1070
  () => paused
841
1071
  );
842
1072
 
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
- };
1073
+ nextTick(() => {
1074
+ if (disposed) return;
1075
+ emitter.emit('ready', { preset: { ...preset } });
1076
+ });
850
1077
 
851
- return {
1078
+ const syncTheme = () => {
1079
+ root.dataset.mode = 'nebula';
1080
+ root.dataset.theme = preset.theme || 'light';
1081
+ updateAria();
1082
+ };
1083
+
1084
+ return {
852
1085
  element: root,
853
1086
  canvas,
854
1087
  preset,
855
1088
  on: emitter.on,
856
1089
  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;
1090
+ setPreset(ref) {
1091
+ const next = getPreset('capsule', ref);
1092
+ dirty.preset = true;
1093
+ Object.assign(preset, next);
1094
+ const nextColors = preset.colors.map(normalizeColor);
1095
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
1096
+ renderer.setPreset({ ...preset });
1097
+ syncTheme();
1098
+ renderer.resize();
1099
+ emitter.emit('presetchange', { preset: { ...next } });
1100
+ return this;
871
1101
  },
872
- setSize(width, height) {
1102
+ setColors(colors) {
1103
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
1104
+ if (next.length !== 4 || next.some((color) => !color)) return this;
1105
+ dirty.colors = true;
1106
+ preset.colors = next;
1107
+ renderer.setPreset({ ...preset, colors: next });
1108
+ return this;
1109
+ },
1110
+ setSeed(seed) {
1111
+ const value = Number(seed);
1112
+ if (!Number.isFinite(value)) return this;
1113
+ dirty.seed = true;
1114
+ preset.seed = value;
1115
+ renderer.setPreset({ ...preset });
1116
+ return this;
1117
+ },
1118
+ setSpeed(speed) {
1119
+ const value = Number(speed);
1120
+ if (!Number.isFinite(value)) return this;
1121
+ dirty.speed = true;
1122
+ preset.speed = value;
1123
+ renderer.setPreset({ ...preset });
1124
+ return this;
1125
+ },
1126
+ setText(content) {
1127
+ fillContent(textLayer, content);
1128
+ return this;
1129
+ },
1130
+ setColorContent(content) {
1131
+ fillContent(visualLayer, content);
1132
+ return this;
1133
+ },
1134
+ setTextRatio(ratio) {
1135
+ const value = Number(ratio);
1136
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
1137
+ dirty.textRatio = true;
1138
+ merged.textRatio = value;
1139
+ root.style.setProperty('--hj-text-width', `${value}%`);
1140
+ return this;
1141
+ },
1142
+ setCssVars(vars) {
1143
+ if (!vars || typeof vars !== 'object') return this;
1144
+ dirty.cssVars = true;
1145
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
1146
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1147
+ return this;
1148
+ },
1149
+ setLabel(label) {
1150
+ customLabel = typeof label === 'string' && label ? label : null;
1151
+ updateAria();
1152
+ return this;
1153
+ },
1154
+ setSize(width, height) {
873
1155
  if (width !== undefined) merged.width = width;
874
1156
  if (height !== undefined) merged.height = height;
875
1157
  applySize();
876
1158
  return this;
877
1159
  },
878
- randomize() {
879
- renderer.randomize();
880
- return this;
881
- },
1160
+ randomize() {
1161
+ this.setSeed(Math.random() * 100);
1162
+ return this;
1163
+ },
882
1164
  pause() {
883
1165
  paused = true;
884
1166
  return this;
@@ -892,15 +1174,19 @@ function createCapsule(container, options = {}) {
892
1174
  if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
893
1175
  return this;
894
1176
  },
895
- dispose() {
896
- unsubscribe();
1177
+ dispose() {
1178
+ disposed = true;
1179
+ unsubscribe();
897
1180
  visibility.dispose();
898
1181
  if (resizeObserver) resizeObserver.disconnect();
899
1182
  else window.removeEventListener('resize', renderer.resize);
900
- renderer.dispose();
901
- root.remove();
902
- }
903
- };
1183
+ renderer.dispose();
1184
+ root.remove();
1185
+ },
1186
+ textRatio: merged.textRatio,
1187
+ cssVars: merged.cssVars,
1188
+ dirty
1189
+ };
904
1190
  }
905
1191
 
906
1192
  const PROGRESS_MOTION_WIDTH = 240;
@@ -1525,7 +1811,7 @@ function interpolate(template, values) {
1525
1811
  return template.replace(/\{(\w+)\}/g, (_, key) => (values[key] !== undefined ? values[key] : ''));
1526
1812
  }
1527
1813
 
1528
- function prefersReducedMotion() {
1814
+ function prefersReducedMotion$1() {
1529
1815
  return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
1530
1816
  }
1531
1817
 
@@ -1588,14 +1874,16 @@ const FLOW_PROFILES = {
1588
1874
  };
1589
1875
 
1590
1876
  class ProgressCapsuleController {
1591
- constructor({ root, canvas, valueElement, preset, emitter, options, copy }) {
1877
+ constructor({ root, canvas, valueElement, preset, emitter, options, copy, dirty }) {
1592
1878
  this.root = root;
1593
1879
  this.canvas = canvas;
1594
1880
  this.valueElement = valueElement;
1595
1881
  this.preset = preset;
1596
1882
  this.emitter = emitter;
1597
- this.options = options;
1598
- this.copy = copy;
1883
+ this.options = options;
1884
+ this.copy = copy;
1885
+ this.dirty = dirty;
1886
+ this.valueSuffix = options.valueSuffix ?? copy.valueSuffix;
1599
1887
  this.profile = preset.edgeStyle === 'tide'
1600
1888
  ? FLOW_PROFILES.tide
1601
1889
  : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
@@ -1636,11 +1924,22 @@ class ProgressCapsuleController {
1636
1924
  this.value = clamp(nextValue, this.min, this.max);
1637
1925
  const rounded = Math.round(this.value);
1638
1926
  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
- }
1927
+ this.root.setAttribute('aria-valuenow', String(rounded));
1928
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1929
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1930
+ if (!this.suppressEvents) {
1931
+ if (this.dirty) this.dirty.value = true;
1932
+ this.emitter.emit('change', { value: this.value, source });
1933
+ }
1934
+ }
1935
+
1936
+ setValueSuffix(suffix) {
1937
+ this.valueSuffix = String(suffix == null ? '' : suffix);
1938
+ const rounded = Math.round(this.value);
1939
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1940
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1941
+ return this;
1942
+ }
1644
1943
 
1645
1944
  resizeCanvas() {
1646
1945
  const bounds = this.root.getBoundingClientRect();
@@ -1813,13 +2112,14 @@ class ProgressCapsuleController {
1813
2112
  if (clamped !== this.value) this.setProgress(clamped, 'prop');
1814
2113
  }
1815
2114
 
1816
- drawReferenceFlow() {
2115
+ drawReferenceFlow() {
1817
2116
  const ctx = this.ctx;
1818
2117
  const width = this.width;
1819
2118
  const height = this.height;
1820
2119
  if (!ctx || width <= 0 || height <= 0) return;
1821
2120
 
1822
- const shoreline = width * (this.value / 100);
2121
+ const range = Math.max(this.max - this.min, 1);
2122
+ const shoreline = width * Math.min(Math.max((this.value - this.min) / range, 0), 1);
1823
2123
  const time = this.flowTime;
1824
2124
  const [dark, accentA, accentB, glow] = this.preset.colors;
1825
2125
 
@@ -1937,32 +2237,49 @@ class ProgressCapsuleController {
1937
2237
  }
1938
2238
  }
1939
2239
 
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');
2240
+ updateFromPointer(event) {
2241
+ const bounds = this.root.getBoundingClientRect();
2242
+ const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
2243
+ this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
2244
+ }
2245
+
2246
+ beginDrag(event) {
2247
+ if (event.button !== undefined && event.button !== 0) return;
2248
+ event.preventDefault();
2249
+ this.dragging = true;
2250
+ this.pendingPointer = null;
2251
+ this._dragRaf = 0;
2252
+ this.root.classList.add('is-dragging');
2253
+ try { this.root.setPointerCapture?.(event.pointerId); } catch {}
2254
+ this.emitter.emit('dragstart', { value: this.value });
2255
+ this.updateFromPointer(event);
2256
+ }
2257
+
2258
+ moveDrag(event) {
2259
+ if (!this.dragging) return;
2260
+ event.preventDefault();
2261
+ // rAF 合并:一帧最多一次 setProgress/change,视觉仍每帧更新。
2262
+ this.pendingPointer = event;
2263
+ if (this._dragRaf) return;
2264
+ this._dragRaf = requestAnimationFrame(() => {
2265
+ this._dragRaf = 0;
2266
+ const pending = this.pendingPointer;
2267
+ this.pendingPointer = null;
2268
+ if (pending) this.updateFromPointer(pending);
2269
+ });
2270
+ }
2271
+
2272
+ endDrag(event) {
2273
+ if (!this.dragging) return;
2274
+ this.dragging = false;
2275
+ if (this._dragRaf) {
2276
+ cancelAnimationFrame(this._dragRaf);
2277
+ this._dragRaf = 0;
2278
+ }
2279
+ const pending = this.pendingPointer;
2280
+ this.pendingPointer = null;
2281
+ if (pending) this.updateFromPointer(pending);
2282
+ this.root.classList.remove('is-dragging');
1966
2283
  try {
1967
2284
  if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
1968
2285
  this.root.releasePointerCapture(event.pointerId);
@@ -1984,15 +2301,16 @@ class ProgressCapsuleController {
1984
2301
  }
1985
2302
 
1986
2303
  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
- };
2304
+ this.handlers.keydown = (event) => {
2305
+ const step = (this.max - this.min) * 0.02;
2306
+ const actions = {
2307
+ ArrowLeft: -step,
2308
+ ArrowDown: -step,
2309
+ ArrowRight: step,
2310
+ ArrowUp: step,
2311
+ PageDown: -step * 5,
2312
+ PageUp: step * 5
2313
+ };
1996
2314
  if (event.key === 'Home') {
1997
2315
  event.preventDefault();
1998
2316
  this.setProgress(this.min, 'keyboard');
@@ -2030,8 +2348,9 @@ class ProgressCapsuleController {
2030
2348
  this.preset = { ...this.preset, colors };
2031
2349
  }
2032
2350
 
2033
- dispose() {
2034
- if (this.resizeObserver) this.resizeObserver.disconnect();
2351
+ dispose() {
2352
+ if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
2353
+ if (this.resizeObserver) this.resizeObserver.disconnect();
2035
2354
  else window.removeEventListener('resize', this.resizeCanvas);
2036
2355
  for (const name of Object.keys(this.handlers)) {
2037
2356
  const handler = this.handlers[name];
@@ -2042,103 +2361,127 @@ class ProgressCapsuleController {
2042
2361
  }
2043
2362
 
2044
2363
  /**
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 = {}) {
2364
+ * Mount a fluid progress capsule into `container`.
2365
+ *
2366
+ * Options: preset (id/code/name or object), width, height, value, min, max,
2367
+ * draggable, keyboard, colors, edgeStyle, textRatio (0-100, text region
2368
+ * width in percent), text (HTML string or DOM nodes for the text slot),
2369
+ * colorContent (HTML string or DOM nodes for the color slot),
2370
+ * showValue (show/hide the right-side percentage), quality,
2371
+ * respectReducedMotion, cssVars.
2372
+ */
2373
+ function createProgressCapsule(container, options = {}) {
2052
2374
  if (!container || typeof container.appendChild !== 'function') {
2053
2375
  throw new Error('createProgressCapsule: container element is required');
2054
2376
  }
2055
2377
 
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));
2378
+ const preset = { ...getPreset('progress', options.preset ?? '星火') };
2379
+ const merged = normalizeOptions(DEFAULTS.progress, preset, options);
2380
+ const normalizedColors = merged.colors.map(normalizeColor);
2381
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2382
+ preset.edgeStyle = merged.edgeStyle;
2383
+ const copy = COPY;
2384
+ const customLabel = typeof options.label === 'string' && options.label ? options.label : null;
2385
+ // Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
2386
+ // 这里统一按 true 处理。
2387
+ const disabled = merged.disabled === true || merged.disabled === '' || merged.disabled === 'true';
2388
+ const effectiveDraggable = disabled ? false : merged.draggable !== false;
2389
+ const effectiveKeyboard = disabled ? false : merged.keyboard !== false;
2390
+ const dirty = {
2391
+ preset: false,
2392
+ colors: false,
2393
+ textRatio: false,
2394
+ cssVars: false,
2395
+ edgeStyle: false,
2396
+ value: false
2397
+ };
2398
+
2399
+ const root = document.createElement('div');
2400
+ root.className = 'hj-capsule-root hj-progress-root';
2401
+ root.setAttribute('role', 'slider');
2402
+ root.setAttribute('tabindex', '0');
2403
+ root.setAttribute('data-draggable', String(effectiveDraggable));
2404
+ if (disabled) {
2405
+ root.setAttribute('aria-disabled', 'true');
2406
+ root.classList.add('is-disabled');
2407
+ }
2065
2408
  root.setAttribute('aria-valuemin', String(merged.min ?? 0));
2066
2409
  root.setAttribute('aria-valuemax', String(merged.max ?? 100));
2067
2410
  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');
2411
+ const updateAria = () => {
2412
+ root.setAttribute(
2413
+ 'aria-label',
2414
+ customLabel || interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2415
+ );
2416
+ };
2417
+ updateAria();
2418
+ if (!effectiveKeyboard) root.setAttribute('tabindex', '-1');
2073
2419
 
2074
2420
  const canvas = document.createElement('canvas');
2075
2421
  canvas.className = 'hj-progress-canvas';
2076
2422
  canvas.setAttribute('aria-hidden', 'true');
2077
2423
 
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
- };
2424
+ // Slot containers are always present but transparent by default; they only
2425
+ // provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
2426
+ const fillContent = (layer, content) => {
2427
+ layer.innerHTML = '';
2428
+ if (content == null) return;
2429
+ if (typeof content === 'string') {
2430
+ layer.innerHTML = content;
2431
+ return;
2432
+ }
2433
+ const nodes = Array.isArray(content) ? content : [content];
2434
+ for (const node of nodes) {
2435
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
2436
+ }
2437
+ };
2438
+
2439
+ const textLayer = document.createElement('div');
2440
+ textLayer.className = 'hj-progress-text hj-progress-copy';
2441
+
2442
+ const visualLayer = document.createElement('div');
2443
+ visualLayer.className = 'hj-progress-visual';
2444
+
2445
+ fillContent(textLayer, options.text);
2446
+ fillContent(visualLayer, options.colorContent);
2447
+
2448
+ const valueElement = merged.showValue === false ? null : document.createElement('div');
2449
+ if (valueElement) {
2450
+ valueElement.className = 'hj-progress-value';
2451
+ valueElement.setAttribute('aria-hidden', 'true');
2452
+ }
2453
+
2454
+ root.appendChild(canvas);
2455
+ root.appendChild(textLayer);
2456
+ root.appendChild(visualLayer);
2457
+ if (valueElement) root.appendChild(valueElement);
2458
+ container.appendChild(root);
2459
+
2460
+ const emitter = createEmitter();
2461
+ let paused = merged.respectReducedMotion && prefersReducedMotion$1();
2462
+ let disposed = false;
2463
+
2464
+ const applySize = () => {
2465
+ root.style.width = parseSize(merged.width);
2466
+ root.style.height = parseSize(merged.height);
2467
+ const vars = merged.cssVars || {};
2468
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2469
+ if (merged.textRatio !== undefined) {
2470
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
2471
+ }
2472
+ };
2131
2473
  applySize();
2132
2474
 
2133
- const controller = new ProgressCapsuleController({
2134
- root,
2135
- canvas,
2136
- valueElement,
2137
- preset,
2138
- emitter,
2139
- options: merged,
2140
- copy
2141
- });
2475
+ const controller = new ProgressCapsuleController({
2476
+ root,
2477
+ canvas,
2478
+ valueElement,
2479
+ preset,
2480
+ emitter,
2481
+ options: { ...merged, draggable: effectiveDraggable, keyboard: effectiveKeyboard },
2482
+ copy,
2483
+ dirty
2484
+ });
2142
2485
 
2143
2486
  let overlay = null;
2144
2487
  if (merged.renderer !== 'canvas2d') {
@@ -2149,7 +2492,13 @@ function createProgressCapsule(container, options = {}) {
2149
2492
  getProgress: () => controller.value
2150
2493
  });
2151
2494
  }
2152
- if (overlay) controller.webglActive = true;
2495
+ if (overlay) controller.webglActive = true;
2496
+ else if (merged.renderer !== 'canvas2d') {
2497
+ nextTick(() => {
2498
+ if (disposed) return;
2499
+ emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
2500
+ });
2501
+ }
2153
2502
 
2154
2503
  const visibility = createVisibilityGuard(root);
2155
2504
  let flowTime = 0;
@@ -2165,15 +2514,14 @@ function createProgressCapsule(container, options = {}) {
2165
2514
  () => paused
2166
2515
  );
2167
2516
 
2168
- emitter.emit('ready', { preset: { ...preset } });
2517
+ nextTick(() => {
2518
+ if (disposed) return;
2519
+ emitter.emit('ready', { preset: { ...preset } });
2520
+ });
2169
2521
 
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
- };
2522
+ const syncDom = () => {
2523
+ updateAria();
2524
+ };
2177
2525
 
2178
2526
  return {
2179
2527
  element: root,
@@ -2192,31 +2540,84 @@ function createProgressCapsule(container, options = {}) {
2192
2540
  controller.setRange(min, max);
2193
2541
  return this;
2194
2542
  },
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;
2543
+ setPreset(ref) {
2544
+ const next = getPreset('progress', ref);
2545
+ dirty.preset = true;
2546
+ Object.assign(preset, next);
2547
+ const nextColors = preset.colors.map(normalizeColor);
2548
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
2549
+ controller.preset = preset;
2550
+ controller.profile = next.edgeStyle === 'tide'
2551
+ ? FLOW_PROFILES.tide
2552
+ : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
2553
+ controller.flowTime = stringSeed(next.id) * 31;
2554
+ controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
2555
+ syncDom();
2556
+ if (overlay) {
2557
+ overlay.dispose();
2558
+ overlay = attachProgressFlowOverlay({ root, canvas, preset, getProgress: () => controller.value });
2559
+ }
2560
+ controller.webglActive = Boolean(overlay);
2561
+ controller.resizeCanvas();
2562
+ emitter.emit('presetchange', { preset: { ...preset } });
2563
+ return this;
2564
+ },
2565
+ setEdgeStyle(edgeStyle) {
2566
+ const value = String(edgeStyle || '').toLowerCase();
2567
+ if (value !== 'flow' && value !== 'tide') return this;
2568
+ dirty.edgeStyle = true;
2569
+ preset.edgeStyle = value;
2570
+ controller.profile = value === 'tide'
2571
+ ? FLOW_PROFILES.tide
2572
+ : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
2573
+ if (overlay) {
2574
+ overlay.dispose();
2575
+ overlay = attachProgressFlowOverlay({ root, canvas, preset, getProgress: () => controller.value });
2576
+ }
2577
+ controller.webglActive = Boolean(overlay);
2578
+ controller.resizeCanvas();
2579
+ return this;
2580
+ },
2581
+ setText(content) {
2582
+ fillContent(textLayer, content);
2583
+ return this;
2584
+ },
2585
+ setColorContent(content) {
2586
+ fillContent(visualLayer, content);
2587
+ return this;
2588
+ },
2589
+ setTextRatio(ratio) {
2590
+ const value = Number(ratio);
2591
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
2592
+ dirty.textRatio = true;
2593
+ merged.textRatio = value;
2594
+ root.style.setProperty('--hj-text-width', `${value}%`);
2595
+ return this;
2596
+ },
2597
+ setCssVars(vars) {
2598
+ if (!vars || typeof vars !== 'object') return this;
2599
+ dirty.cssVars = true;
2600
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
2601
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2602
+ return this;
2603
+ },
2604
+ setLabel(label) {
2605
+ customLabel = typeof label === 'string' && label ? label : null;
2606
+ updateAria();
2607
+ return this;
2608
+ },
2609
+ setValueSuffix(suffix) {
2610
+ controller.setValueSuffix(suffix);
2611
+ return this;
2612
+ },
2613
+ setColors(colors) {
2614
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
2615
+ if (next.length !== 4 || next.some((color) => !color)) return this;
2616
+ dirty.colors = true;
2617
+ controller.setColors(next);
2618
+ if (overlay) overlay.setColors(next);
2619
+ controller.resizeCanvas();
2620
+ return this;
2220
2621
  },
2221
2622
  setSize(width, height) {
2222
2623
  if (width !== undefined) merged.width = width;
@@ -2243,14 +2644,240 @@ function createProgressCapsule(container, options = {}) {
2243
2644
  controller.resizeCanvas();
2244
2645
  return this;
2245
2646
  },
2246
- dispose() {
2247
- unsubscribe();
2647
+ dispose() {
2648
+ disposed = true;
2649
+ unsubscribe();
2248
2650
  visibility.dispose();
2249
2651
  if (overlay) overlay.dispose();
2250
- controller.dispose();
2251
- root.remove();
2252
- }
2652
+ controller.dispose();
2653
+ root.remove();
2654
+ },
2655
+ textRatio: merged.textRatio,
2656
+ cssVars: merged.cssVars,
2657
+ dirty
2658
+ };
2659
+ }
2660
+
2661
+ function prefersReducedMotion() {
2662
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
2663
+ }
2664
+
2665
+ /**
2666
+ * Mount the cosmic (nebula) material as a background layer inside `host`.
2667
+ *
2668
+ * The layer is absolutely positioned and defaults to `z-index: -1`, so it
2669
+ * paints behind the host's in-flow content: text/buttons/images inside the
2670
+ * host sit on top with zero extra CSS. Host gets `position: relative;
2671
+ * isolation: isolate` automatically so the layer can never escape its box
2672
+ * (or break stacking outside it).
2673
+ *
2674
+ * Options: preset (id/code/name or object), colors, seed, speed, quality,
2675
+ * renderer, mouseColor, respectReducedMotion, opacity (0-1), fallbackColor
2676
+ * (true=preset base color, a hex string, or false to disable).
2677
+ */
2678
+ function createColorBackground(host, options = {}) {
2679
+ if (!host || typeof host.appendChild !== 'function') {
2680
+ throw new Error('createColorBackground: host element is required');
2681
+ }
2682
+
2683
+ const preset = { ...getPreset('capsule', options.preset ?? '初光') };
2684
+ const merged = normalizeOptions(
2685
+ {
2686
+ quality: DEFAULTS.capsule.quality,
2687
+ renderer: DEFAULTS.capsule.renderer,
2688
+ respectReducedMotion: DEFAULTS.capsule.respectReducedMotion,
2689
+ mouseColor: true,
2690
+ opacity: 1,
2691
+ fallbackColor: true
2692
+ },
2693
+ preset,
2694
+ options
2695
+ );
2696
+ const normalizedColors = merged.colors.map(normalizeColor);
2697
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2698
+ preset.seed = merged.seed;
2699
+ preset.speed = merged.speed;
2700
+
2701
+ const computed = getComputedStyle(host);
2702
+ if (computed.position === 'static' || computed.position === '') {
2703
+ host.style.position = 'relative';
2704
+ }
2705
+ host.style.isolation = 'isolate';
2706
+
2707
+ const layer = document.createElement('div');
2708
+ layer.className = 'dlc-color-layer';
2709
+
2710
+ const syncLayerVars = () => {
2711
+ layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
2712
+ const fallback =
2713
+ merged.fallbackColor === false || merged.fallbackColor == null
2714
+ ? 'transparent'
2715
+ : typeof merged.fallbackColor === 'string'
2716
+ ? merged.fallbackColor
2717
+ : preset.colors[0];
2718
+ layer.style.setProperty('--dlc-color-bg', fallback);
2253
2719
  };
2720
+ syncLayerVars();
2721
+
2722
+ const canvas = document.createElement('canvas');
2723
+ canvas.className = 'dlc-color-canvas';
2724
+ canvas.setAttribute('aria-hidden', 'true');
2725
+ layer.appendChild(canvas);
2726
+ host.appendChild(layer);
2727
+
2728
+ const emitter = createEmitter();
2729
+ let paused = merged.respectReducedMotion && prefersReducedMotion();
2730
+ let disposed = false;
2731
+ const dirty = {
2732
+ preset: false,
2733
+ seed: false,
2734
+ speed: false,
2735
+ colors: false,
2736
+ opacity: false,
2737
+ fallbackColor: false,
2738
+ mouseColor: false
2739
+ };
2740
+
2741
+ let renderer;
2742
+ const useWebgl = merged.renderer !== 'canvas2d';
2743
+ if (useWebgl) {
2744
+ try {
2745
+ renderer = new CosmicRenderer(canvas, merged, {
2746
+ dprCap: dprCapFor(merged.quality),
2747
+ mouseColor: merged.mouseColor !== false,
2748
+ eventTarget: host
2749
+ });
2750
+ } catch (error) {
2751
+ renderer = new FallbackRenderer(canvas, merged);
2752
+ nextTick(() => {
2753
+ if (disposed) return;
2754
+ emitter.emit('error', {
2755
+ message: String(error && error.message ? error.message : error)
2756
+ });
2757
+ });
2758
+ }
2759
+ } else {
2760
+ renderer = new FallbackRenderer(canvas, merged);
2761
+ }
2762
+
2763
+ const resizeObserver =
2764
+ typeof ResizeObserver !== 'undefined'
2765
+ ? new ResizeObserver(() => renderer.resize())
2766
+ : null;
2767
+ if (resizeObserver) resizeObserver.observe(host);
2768
+ else window.addEventListener('resize', renderer.resize);
2769
+
2770
+ const visibility = createVisibilityGuard(layer);
2771
+
2772
+ let animationTime = 0;
2773
+ const unsubscribe = subscribeScheduler(
2774
+ (delta) => {
2775
+ animationTime += delta;
2776
+ if (visibility.isVisible()) renderer.draw(animationTime);
2777
+ },
2778
+ () => paused
2779
+ );
2780
+
2781
+ nextTick(() => {
2782
+ if (disposed) return;
2783
+ emitter.emit('ready', { preset: { ...preset } });
2784
+ });
2785
+
2786
+ return {
2787
+ element: host,
2788
+ layer,
2789
+ canvas,
2790
+ preset,
2791
+ on: emitter.on,
2792
+ off: emitter.off,
2793
+ setPreset(ref) {
2794
+ const next = getPreset('capsule', ref);
2795
+ dirty.preset = true;
2796
+ Object.assign(preset, next);
2797
+ const nextColors = preset.colors.map(normalizeColor);
2798
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
2799
+ renderer.setPreset({ ...preset });
2800
+ renderer.resize();
2801
+ syncLayerVars();
2802
+ emitter.emit('presetchange', { preset: { ...preset } });
2803
+ return this;
2804
+ },
2805
+ setColors(colors) {
2806
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
2807
+ if (next.length !== 4 || next.some((color) => !color)) return this;
2808
+ dirty.colors = true;
2809
+ preset.colors = next;
2810
+ renderer.setPreset({ ...preset, colors: next });
2811
+ syncLayerVars();
2812
+ return this;
2813
+ },
2814
+ setSeed(seed) {
2815
+ const value = Number(seed);
2816
+ if (!Number.isFinite(value)) return this;
2817
+ dirty.seed = true;
2818
+ preset.seed = value;
2819
+ renderer.setPreset({ ...preset });
2820
+ return this;
2821
+ },
2822
+ setSpeed(speed) {
2823
+ const value = Number(speed);
2824
+ if (!Number.isFinite(value)) return this;
2825
+ dirty.speed = true;
2826
+ preset.speed = value;
2827
+ renderer.setPreset({ ...preset });
2828
+ return this;
2829
+ },
2830
+ setFallbackColor(value) {
2831
+ dirty.fallbackColor = true;
2832
+ merged.fallbackColor = value;
2833
+ syncLayerVars();
2834
+ return this;
2835
+ },
2836
+ setMouseColor(value) {
2837
+ dirty.mouseColor = true;
2838
+ merged.mouseColor = value !== false;
2839
+ if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
2840
+ return this;
2841
+ },
2842
+ setQuality(quality) {
2843
+ merged.quality = quality;
2844
+ if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
2845
+ return this;
2846
+ },
2847
+ setOpacity(value) {
2848
+ const opacity = Number(value);
2849
+ if (!Number.isFinite(opacity)) return this;
2850
+ dirty.opacity = true;
2851
+ merged.opacity = Math.min(1, Math.max(0, opacity));
2852
+ layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
2853
+ return this;
2854
+ },
2855
+ randomize() {
2856
+ this.setSeed(Math.random() * 100);
2857
+ return this;
2858
+ },
2859
+ pause() {
2860
+ paused = true;
2861
+ return this;
2862
+ },
2863
+ resume() {
2864
+ paused = false;
2865
+ return this;
2866
+ },
2867
+ dispose() {
2868
+ disposed = true;
2869
+ unsubscribe();
2870
+ visibility.dispose();
2871
+ if (resizeObserver) resizeObserver.disconnect();
2872
+ else window.removeEventListener('resize', renderer.resize);
2873
+ renderer.dispose();
2874
+ layer.remove();
2875
+ },
2876
+ opacity: merged.opacity,
2877
+ fallbackColor: merged.fallbackColor,
2878
+ mouseColor: merged.mouseColor,
2879
+ dirty
2880
+ };
2254
2881
  }
2255
2882
 
2256
2883
  /**
@@ -2259,11 +2886,12 @@ function createProgressCapsule(container, options = {}) {
2259
2886
  * `h` (see vue2.js / vue3.js). Lifecycle options cover both.
2260
2887
  */
2261
2888
 
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'];
2889
+ const CAPSULE_EVENTS = [
2890
+ 'ready', 'error', 'click', 'pointerdown', 'pointerup', 'dblclick',
2891
+ 'pointerenter', 'pointerleave', 'presetchange'
2892
+ ];
2893
+ const PROGRESS_EVENTS = ['ready', 'error', 'change', 'dragstart', 'dragend', 'presetchange'];
2894
+ const COLOR_EVENTS = ['ready', 'error', 'presetchange'];
2267
2895
 
2268
2896
  function hookEvents(controller, events, emit) {
2269
2897
  for (const event of events) {
@@ -2271,42 +2899,92 @@ function hookEvents(controller, events, emit) {
2271
2899
  }
2272
2900
  }
2273
2901
 
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;
2902
+ function makeWrapper({ name, props, events, create, render, methods, watch }) {
2903
+ const internalMethods = {
2904
+ mountController(extraState = {}) {
2905
+ this.controller = create(this.$el, { ...this.$props, ...extraState });
2906
+ hookEvents(this.controller, events, (event, payload) => this.$emit(event, payload));
2907
+ this.moveSlots();
2908
+ this.applyAttrs();
2909
+ },
2910
+ recreate() {
2911
+ const extraState = this.runtimeState();
2912
+ if (this.controller) this.controller.dispose();
2913
+ this.mountController(extraState);
2914
+ },
2915
+ scheduleRecreate() {
2916
+ if (this._recreateQueued) return;
2917
+ this._recreateQueued = true;
2288
2918
  this.$nextTick(() => {
2289
2919
  this._recreateQueued = false;
2290
- this.recreate();
2291
- });
2292
- }
2293
- };
2294
-
2295
- return {
2296
- name,
2297
- props,
2298
- emits: events,
2299
- render,
2920
+ this.recreate();
2921
+ });
2922
+ },
2923
+ moveSlots() {
2924
+ if (!this.controller || !this.controller.element || !this.$el) return;
2925
+ const root = this.controller.element;
2926
+ const textLayer = root.querySelector('.hj-capsule-text, .hj-progress-text');
2927
+ const visualLayer = root.querySelector('.hj-capsule-visual, .hj-progress-visual');
2928
+ const move = (wrapper, layer) => {
2929
+ if (!wrapper || !layer) return;
2930
+ for (const node of Array.from(wrapper.childNodes)) layer.appendChild(node);
2931
+ };
2932
+ const refs = this.$refs || {};
2933
+ move(refs.slotText, textLayer);
2934
+ move(refs.slotColor, visualLayer);
2935
+ },
2936
+ applyAttrs() {
2937
+ if (!this.controller || !this.controller.element || !this.$attrs) return;
2938
+ const root = this.controller.element;
2939
+ for (const key of Object.keys(this.$attrs)) {
2940
+ if (key === 'class' || key === 'style') continue;
2941
+ const value = this.$attrs[key];
2942
+ if (value == null || value === false) root.removeAttribute(key);
2943
+ else root.setAttribute(key, String(value));
2944
+ }
2945
+ },
2946
+ runtimeState() {
2947
+ const controller = this.controller;
2948
+ if (!controller || !controller.dirty) return {};
2949
+ const dirty = controller.dirty;
2950
+ const state = {};
2951
+ if (dirty.preset && controller.preset) state.preset = controller.preset.name;
2952
+ if (dirty.seed && controller.preset && controller.preset.seed !== undefined) state.seed = controller.preset.seed;
2953
+ if (dirty.speed && controller.preset && controller.preset.speed !== undefined) state.speed = controller.preset.speed;
2954
+ if (dirty.colors && controller.preset && Array.isArray(controller.preset.colors)) {
2955
+ state.colors = [...controller.preset.colors];
2956
+ }
2957
+ if (dirty.textRatio && controller.textRatio !== undefined) state.textRatio = controller.textRatio;
2958
+ if (dirty.cssVars && controller.cssVars) state.cssVars = { ...controller.cssVars };
2959
+ if (dirty.value && typeof controller.getValue === 'function') state.value = controller.getValue();
2960
+ if (dirty.edgeStyle && controller.preset) state.edgeStyle = controller.preset.edgeStyle;
2961
+ if (dirty.opacity) state.opacity = controller.opacity;
2962
+ if (dirty.fallbackColor) state.fallbackColor = controller.fallbackColor;
2963
+ if (dirty.mouseColor) state.mouseColor = controller.mouseColor;
2964
+ return state;
2965
+ }
2966
+ };
2967
+
2968
+ return {
2969
+ name,
2970
+ props,
2971
+ emits: events,
2972
+ render,
2973
+ inheritAttrs: false,
2300
2974
  mounted() {
2301
2975
  this.mountController();
2302
2976
  },
2303
2977
  beforeUnmount() {
2304
2978
  if (this.controller) this.controller.dispose();
2305
2979
  },
2306
- beforeDestroy() {
2307
- if (this.controller) this.controller.dispose();
2308
- },
2309
- watch: {
2980
+ beforeDestroy() {
2981
+ if (this.controller) this.controller.dispose();
2982
+ },
2983
+ updated() {
2984
+ this.moveSlots();
2985
+ this.applyAttrs();
2986
+ },
2987
+ watch: {
2310
2988
  preset(value) { if (value && this.controller) this.controller.setPreset(value); },
2311
2989
  colors(value) { if (value && this.controller) this.controller.setColors(value); },
2312
2990
  width(value) { if (value !== undefined && this.controller) this.controller.setSize(value); },
@@ -2320,33 +2998,39 @@ function makeWrapper({ name, props, events, create, render, methods, watch }) {
2320
2998
 
2321
2999
  function makeCapsuleWrapper(createCapsule, render) {
2322
3000
  return makeWrapper({
2323
- name: 'CosmicCapsule',
2324
- props: [
2325
- 'preset', 'width', 'height', 'colors', 'seed', 'speed', 'interactive',
2326
- 'mouseColor', 'renderer', 'quality', 'respectReducedMotion', 'copy', 'cssVars'
2327
- ],
3001
+ name: 'CosmicCapsule',
3002
+ props: [
3003
+ 'preset', 'width', 'height', 'colors', 'seed', 'speed',
3004
+ 'textRatio', 'label', 'mouseColor', 'renderer', 'quality', 'respectReducedMotion', 'cssVars'
3005
+ ],
2328
3006
  events: CAPSULE_EVENTS,
2329
3007
  create: createCapsule,
2330
3008
  render,
2331
3009
  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
- },
3010
+ seed(value) {
3011
+ const seed = Number(value);
3012
+ if (this.controller && Number.isFinite(seed)) {
3013
+ this.controller.setSeed(seed);
3014
+ }
3015
+ },
3016
+ speed(value) {
3017
+ const speed = Number(value);
3018
+ if (this.controller && Number.isFinite(speed)) {
3019
+ this.controller.setSpeed(speed);
3020
+ }
3021
+ },
3022
+ textRatio(value) {
3023
+ if (value !== undefined && this.controller) this.controller.setTextRatio(Number(value));
3024
+ },
3025
+ cssVars(value) {
3026
+ if (value && this.controller) this.controller.setCssVars(value);
3027
+ },
3028
+ label(value) {
3029
+ if (this.controller) this.controller.setLabel(value);
3030
+ },
3031
+ mouseColor() {
3032
+ this.scheduleRecreate();
3033
+ },
2350
3034
  renderer() {
2351
3035
  this.scheduleRecreate();
2352
3036
  }
@@ -2355,23 +3039,28 @@ function makeCapsuleWrapper(createCapsule, render) {
2355
3039
  randomize() { return this.controller && this.controller.randomize(); },
2356
3040
  pause() { return this.controller && this.controller.pause(); },
2357
3041
  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; }
3042
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
3043
+ setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
3044
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
3045
+ setText(content) { return this.controller && this.controller.setText(content); },
3046
+ setColorContent(content) { return this.controller && this.controller.setColorContent(content); },
3047
+ setTextRatio(ratio) { return this.controller && this.controller.setTextRatio(ratio); },
3048
+ setCssVars(vars) { return this.controller && this.controller.setCssVars(vars); },
3049
+ setLabel(label) { return this.controller && this.controller.setLabel(label); },
3050
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
3051
+ getController() { return this.controller; }
2363
3052
  }
2364
3053
  });
2365
3054
  }
2366
3055
 
2367
- function makeProgressWrapper(createProgressCapsule, render) {
3056
+ function makeProgressWrapper(createProgressCapsule, render) {
2368
3057
  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
- ],
3058
+ name: 'ProgressCapsule',
3059
+ props: [
3060
+ 'preset', 'width', 'height', 'value', 'min', 'max',
3061
+ 'draggable', 'keyboard', 'disabled', 'edgeStyle', 'colors', 'textRatio', 'label', 'showValue', 'valueSuffix',
3062
+ 'quality', 'renderer', 'respectReducedMotion', 'cssVars'
3063
+ ],
2375
3064
  events: PROGRESS_EVENTS,
2376
3065
  create: createProgressCapsule,
2377
3066
  render,
@@ -2385,12 +3074,30 @@ function makeProgressWrapper(createProgressCapsule, render) {
2385
3074
  max(value) {
2386
3075
  if (value !== undefined && this.controller) this.controller.setRange(this.controller.min, value);
2387
3076
  },
2388
- edgeStyle(value) {
2389
- if (value && this.controller) this.controller.setPreset({ ...this.controller.preset, edgeStyle: value });
2390
- },
2391
- draggable() {
2392
- this.scheduleRecreate();
2393
- },
3077
+ edgeStyle(value) {
3078
+ if (value && this.controller) this.controller.setEdgeStyle(value);
3079
+ },
3080
+ textRatio(value) {
3081
+ if (value !== undefined && this.controller) this.controller.setTextRatio(Number(value));
3082
+ },
3083
+ cssVars(value) {
3084
+ if (value && this.controller) this.controller.setCssVars(value);
3085
+ },
3086
+ label(value) {
3087
+ if (this.controller) this.controller.setLabel(value);
3088
+ },
3089
+ valueSuffix(value) {
3090
+ if (value !== undefined && this.controller) this.controller.setValueSuffix(value);
3091
+ },
3092
+ showValue() {
3093
+ this.scheduleRecreate();
3094
+ },
3095
+ disabled() {
3096
+ this.scheduleRecreate();
3097
+ },
3098
+ draggable() {
3099
+ this.scheduleRecreate();
3100
+ },
2394
3101
  keyboard() {
2395
3102
  this.scheduleRecreate();
2396
3103
  },
@@ -2405,24 +3112,96 @@ function makeProgressWrapper(createProgressCapsule, render) {
2405
3112
  randomize() { return this.controller && this.controller.randomize(); },
2406
3113
  pause() { return this.controller && this.controller.pause(); },
2407
3114
  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); },
3115
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
3116
+ setSize(width, height) { return this.controller && this.controller.setSize(width, height); },
3117
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
3118
+ setText(content) { return this.controller && this.controller.setText(content); },
3119
+ setColorContent(content) { return this.controller && this.controller.setColorContent(content); },
3120
+ setTextRatio(ratio) { return this.controller && this.controller.setTextRatio(ratio); },
3121
+ setCssVars(vars) { return this.controller && this.controller.setCssVars(vars); },
3122
+ setLabel(label) { return this.controller && this.controller.setLabel(label); },
3123
+ setValueSuffix(suffix) { return this.controller && this.controller.setValueSuffix(suffix); },
3124
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
2412
3125
  getController() { return this.controller; }
2413
3126
  }
2414
- });
3127
+ });
2415
3128
  }
2416
3129
 
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
3130
+ function makeColorWrapper(createColorBackground, render) {
3131
+ return makeWrapper({
3132
+ name: 'ColorBackground',
3133
+ props: [
3134
+ 'preset', 'colors', 'seed', 'speed', 'quality', 'renderer',
3135
+ 'mouseColor', 'respectReducedMotion', 'opacity', 'fallbackColor'
3136
+ ],
3137
+ events: COLOR_EVENTS,
3138
+ create: createColorBackground,
3139
+ render,
3140
+ watch: {
3141
+ seed(value) {
3142
+ const seed = Number(value);
3143
+ if (this.controller && Number.isFinite(seed)) this.controller.setSeed(seed);
3144
+ },
3145
+ speed(value) {
3146
+ const speed = Number(value);
3147
+ if (this.controller && Number.isFinite(speed)) this.controller.setSpeed(speed);
3148
+ },
3149
+ opacity(value) {
3150
+ if (value !== undefined && this.controller) this.controller.setOpacity(Number(value));
3151
+ },
3152
+ fallbackColor(value) {
3153
+ if (this.controller) this.controller.setFallbackColor(value);
3154
+ },
3155
+ mouseColor(value) {
3156
+ if (this.controller) this.controller.setMouseColor(value);
3157
+ },
3158
+ renderer() {
3159
+ this.scheduleRecreate();
3160
+ },
3161
+ respectReducedMotion() {
3162
+ this.scheduleRecreate();
3163
+ }
3164
+ },
3165
+ methods: {
3166
+ setPreset(preset) { return this.controller && this.controller.setPreset(preset); },
3167
+ setColors(colors) { return this.controller && this.controller.setColors(colors); },
3168
+ setSeed(seed) { return this.controller && this.controller.setSeed(seed); },
3169
+ setSpeed(speed) { return this.controller && this.controller.setSpeed(speed); },
3170
+ setQuality(quality) { return this.controller && this.controller.setQuality(quality); },
3171
+ setOpacity(value) { return this.controller && this.controller.setOpacity(value); },
3172
+ setFallbackColor(value) { return this.controller && this.controller.setFallbackColor(value); },
3173
+ setMouseColor(value) { return this.controller && this.controller.setMouseColor(value); },
3174
+ randomize() { return this.controller && this.controller.randomize(); },
3175
+ pause() { return this.controller && this.controller.pause(); },
3176
+ resume() { return this.controller && this.controller.resume(); },
3177
+ getController() { return this.controller; }
3178
+ }
3179
+ });
3180
+ }
3181
+
3182
+ // Vue3 does NOT pass `h` to the render option; it must be imported. Slot
3183
+ // content renders into two marker wrappers, then moveSlots moves it into the
3184
+ // component's slot containers.
3185
+ const render = function render() {
3186
+ return h('div', { class: 'hj-vue-host' }, [
3187
+ h('div', { class: 'hj-vue-slot-text', ref: 'slotText' }, this.$slots.default ? this.$slots.default() : []),
3188
+ h('div', { class: 'hj-vue-slot-color', ref: 'slotColor' }, this.$slots.color ? this.$slots.color() : [])
3189
+ ]);
3190
+ };
3191
+
3192
+ // Color 的宿主内容就是组件内容:直接放在 host 里,浮在背景层上方。
3193
+ const renderColor = function renderColor() {
3194
+ return h('div', { class: 'hj-vue-host' }, this.$slots.default ? this.$slots.default() : []);
3195
+ };
3196
+
3197
+ const CosmicCapsule = makeCapsuleWrapper(createCapsule, render);
3198
+ const ProgressCapsule = makeProgressWrapper(createProgressCapsule, render);
3199
+ const ColorBackground = makeColorWrapper(createColorBackground, renderColor);
3200
+
3201
+ var vue3 = {
3202
+ CosmicCapsule,
3203
+ ProgressCapsule,
3204
+ ColorBackground
2426
3205
  };
2427
3206
 
2428
- export { CosmicCapsule, ProgressCapsule, vue3 as default };
3207
+ export { ColorBackground, CosmicCapsule, ProgressCapsule, vue3 as default };