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