@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/index.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,114 +164,291 @@ const PROGRESS_PRESETS = [
164
164
  }
165
165
  ];
166
166
 
167
- const PRESETS = CAPSULE_PRESETS;
168
- const ALL_PRESETS = [...CAPSULE_PRESETS, ...PROGRESS_PRESETS];
169
-
170
- function validatePreset(preset) {
171
- return Boolean(
172
- preset &&
173
- typeof preset.id === 'string' &&
174
- typeof preset.code === 'string' &&
175
- typeof preset.name === 'string' &&
176
- Number.isFinite(preset.seed) &&
177
- Number.isFinite(preset.speed) &&
178
- Array.isArray(preset.colors) &&
179
- preset.colors.length === 4
180
- );
181
- }
182
-
183
- function validateProgressPreset(preset) {
184
- return Boolean(
185
- preset &&
186
- typeof preset.id === 'string' &&
187
- typeof preset.code === 'string' &&
188
- typeof preset.name === 'string' &&
189
- preset.group === 'progress' &&
190
- Number.isFinite(preset.initialProgress) &&
191
- ['flow', 'tide'].indexOf(preset.edgeStyle || 'flow') !== -1 &&
192
- Array.isArray(preset.colors) &&
193
- preset.colors.length === 4
194
- );
195
- }
196
-
197
- function getPreset(kind, ref) {
198
- const list = kind === 'capsule' ? CAPSULE_PRESETS : kind === 'progress' ? PROGRESS_PRESETS : null;
199
- if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
200
- if (ref && typeof ref === 'object') {
201
- const valid = kind === 'capsule' ? validatePreset(ref) : validateProgressPreset(ref);
202
- if (!valid) throw new Error(`Invalid ${kind} preset object`);
203
- return ref;
204
- }
205
- const key = String(ref).trim().toLowerCase();
206
- const found = list.find(
207
- (preset) =>
208
- preset.id.toLowerCase() === key ||
209
- preset.code.toLowerCase() === key ||
210
- preset.name.toLowerCase() === key
211
- );
212
- if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
213
- return found;
167
+ const PRESETS = CAPSULE_PRESETS;
168
+ const ALL_PRESETS = [...CAPSULE_PRESETS, ...PROGRESS_PRESETS];
169
+
170
+ /**
171
+ * Resolve a preset by its literary name (e.g. '沧溟').
172
+ * Matching is case-insensitive and ignores surrounding whitespace,
173
+ * so '沧溟' and ' 沧溟 ' both work.
174
+ */
175
+ function getPreset(kind, ref) {
176
+ const list = kind === 'capsule' ? CAPSULE_PRESETS : kind === 'progress' ? PROGRESS_PRESETS : null;
177
+ if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
178
+ const key = String(ref).trim().toLowerCase();
179
+ const found = list.find(
180
+ (preset) => preset.name.toLowerCase() === key
181
+ );
182
+ if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
183
+ return found;
214
184
  }
215
185
 
216
- const DEFAULTS = {
217
- capsule: {
218
- width: '100%',
219
- height: 160,
220
- quality: 'auto',
221
- renderer: 'auto',
222
- respectReducedMotion: true,
223
- interactive: true,
224
- mouseColor: true,
225
- showCopy: true
226
- },
227
- progress: {
228
- width: 454,
229
- height: 104,
230
- min: 0,
186
+ const DEFAULTS = {
187
+ capsule: {
188
+ width: '100%',
189
+ height: 160,
190
+ quality: 'auto',
191
+ renderer: 'auto',
192
+ respectReducedMotion: true,
193
+ mouseColor: true,
194
+ textRatio: 39
195
+ },
196
+ progress: {
197
+ width: 454,
198
+ height: 104,
199
+ min: 0,
231
200
  max: 100,
232
- draggable: true,
233
- keyboard: true,
234
- edgeStyle: 'flow',
235
- quality: 'auto',
236
- renderer: 'auto',
237
- showCopy: true,
238
- respectReducedMotion: true
239
- }
201
+ draggable: true,
202
+ keyboard: true,
203
+ disabled: false,
204
+ valueSuffix: '%',
205
+ edgeStyle: 'flow',
206
+ quality: 'auto',
207
+ renderer: 'auto',
208
+ textRatio: 54,
209
+ showValue: true,
210
+ respectReducedMotion: true
211
+ }
240
212
  };
241
213
 
242
- /**
243
- * All user-facing copy lives here so wording/brand changes never require a
244
- * global search. Templates use {brand} {code} {name} placeholders.
245
- */
246
- const COPY = {
247
- brandName: '画境观屿',
248
- dragLabel: 'DRAG',
249
- valueSuffix: '%',
214
+ /**
215
+ * Internal accessibility labels and small UI text. Since the copy API was
216
+ * removed (slots own the text area), these are no longer user-overridable.
217
+ * Templates use {brand} {code} {name} placeholders.
218
+ */
219
+ const COPY = {
220
+ brandName: '画境观屿',
221
+ valueSuffix: '%',
250
222
  progressAria: '{brand} {code} 加载进度',
251
223
  capsuleAria: '打开 {name} 沉浸预览'
252
224
  };
253
225
 
254
- function hexToRgb01$1(hex) {
255
- const normalized = hex.replace('#', '');
256
- const value = Number.parseInt(normalized, 16);
257
- return [
258
- ((value >> 16) & 255) / 255,
259
- ((value >> 8) & 255) / 255,
260
- (value & 255) / 255
261
- ];
262
- }
263
-
264
- function hexToRgba(hex, alpha = 1) {
265
- const normalized = hex.replace('#', '');
266
- const value = Number.parseInt(normalized, 16);
267
- const red = (value >> 16) & 255;
268
- const green = (value >> 8) & 255;
269
- const blue = value & 255;
270
- return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
271
- }
272
-
273
- function validateColors(colors) {
274
- return Array.isArray(colors) && colors.length === 4 && colors.every((c) => typeof c === 'string' && /^#[0-9a-f]{6}$/i.test(c));
226
+ /**
227
+ * Color helpers. `normalizeColor` converts any supported CSS color
228
+ * (hex / named / rgb() / rgba()) into a 6-digit hex string, so renderers can
229
+ * keep working with #rrggbb only. rgba() alpha is intentionally dropped:
230
+ * the WebGL shader has no per-color alpha channel, and the component is
231
+ * opaque by design use component-level `opacity` for transparency.
232
+ */
233
+
234
+ const NAMED_COLORS = {
235
+ aliceblue: '#f0f8ff',
236
+ antiquewhite: '#faebd7',
237
+ aqua: '#00ffff',
238
+ aquamarine: '#7fffd4',
239
+ azure: '#f0ffff',
240
+ beige: '#f5f5dc',
241
+ bisque: '#ffe4c4',
242
+ black: '#000000',
243
+ blanchedalmond: '#ffebcd',
244
+ blue: '#0000ff',
245
+ blueviolet: '#8a2be2',
246
+ brown: '#a52a2a',
247
+ burlywood: '#deb887',
248
+ cadetblue: '#5f9ea0',
249
+ chartreuse: '#7fff00',
250
+ chocolate: '#d2691e',
251
+ coral: '#ff7f50',
252
+ cornflowerblue: '#6495ed',
253
+ cornsilk: '#fff8dc',
254
+ crimson: '#dc143c',
255
+ cyan: '#00ffff',
256
+ darkblue: '#00008b',
257
+ darkcyan: '#008b8b',
258
+ darkgoldenrod: '#b8860b',
259
+ darkgray: '#a9a9a9',
260
+ darkgreen: '#006400',
261
+ darkgrey: '#a9a9a9',
262
+ darkkhaki: '#bdb76b',
263
+ darkmagenta: '#8b008b',
264
+ darkolivegreen: '#556b2f',
265
+ darkorange: '#ff8c00',
266
+ darkorchid: '#9932cc',
267
+ darkred: '#8b0000',
268
+ darksalmon: '#e9967a',
269
+ darkseagreen: '#8fbc8f',
270
+ darkslateblue: '#483d8b',
271
+ darkslategray: '#2f4f4f',
272
+ darkslategrey: '#2f4f4f',
273
+ darkturquoise: '#00ced1',
274
+ darkviolet: '#9400d3',
275
+ deeppink: '#ff1493',
276
+ deepskyblue: '#00bfff',
277
+ dimgray: '#696969',
278
+ dimgrey: '#696969',
279
+ dodgerblue: '#1e90ff',
280
+ firebrick: '#b22222',
281
+ floralwhite: '#fffaf0',
282
+ forestgreen: '#228b22',
283
+ fuchsia: '#ff00ff',
284
+ gainsboro: '#dcdcdc',
285
+ ghostwhite: '#f8f8ff',
286
+ gold: '#ffd700',
287
+ goldenrod: '#daa520',
288
+ gray: '#808080',
289
+ green: '#008000',
290
+ greenyellow: '#adff2f',
291
+ grey: '#808080',
292
+ honeydew: '#f0fff0',
293
+ hotpink: '#ff69b4',
294
+ indianred: '#cd5c5c',
295
+ indigo: '#4b0082',
296
+ ivory: '#fffff0',
297
+ khaki: '#f0e68c',
298
+ lavender: '#e6e6fa',
299
+ lavenderblush: '#fff0f5',
300
+ lawngreen: '#7cfc00',
301
+ lemonchiffon: '#fffacd',
302
+ lightblue: '#add8e6',
303
+ lightcoral: '#f08080',
304
+ lightcyan: '#e0ffff',
305
+ lightgoldenrodyellow: '#fafad2',
306
+ lightgray: '#d3d3d3',
307
+ lightgreen: '#90ee90',
308
+ lightgrey: '#d3d3d3',
309
+ lightpink: '#ffb6c1',
310
+ lightsalmon: '#ffa07a',
311
+ lightseagreen: '#20b2aa',
312
+ lightskyblue: '#87cefa',
313
+ lightslategray: '#778899',
314
+ lightslategrey: '#778899',
315
+ lightsteelblue: '#b0c4de',
316
+ lightyellow: '#ffffe0',
317
+ lime: '#00ff00',
318
+ limegreen: '#32cd32',
319
+ linen: '#faf0e6',
320
+ magenta: '#ff00ff',
321
+ maroon: '#800000',
322
+ mediumaquamarine: '#66cdaa',
323
+ mediumblue: '#0000cd',
324
+ mediumorchid: '#ba55d3',
325
+ mediumpurple: '#9370db',
326
+ mediumseagreen: '#3cb371',
327
+ mediumslateblue: '#7b68ee',
328
+ mediumspringgreen: '#00fa9a',
329
+ mediumturquoise: '#48d1cc',
330
+ mediumvioletred: '#c71585',
331
+ midnightblue: '#191970',
332
+ mintcream: '#f5fffa',
333
+ mistyrose: '#ffe4e1',
334
+ moccasin: '#ffe4b5',
335
+ navajowhite: '#ffdead',
336
+ navy: '#000080',
337
+ oldlace: '#fdf5e6',
338
+ olive: '#808000',
339
+ olivedrab: '#6b8e23',
340
+ orange: '#ffa500',
341
+ orangered: '#ff4500',
342
+ orchid: '#da70d6',
343
+ palegoldenrod: '#eee8aa',
344
+ palegreen: '#98fb98',
345
+ paleturquoise: '#afeeee',
346
+ palevioletred: '#db7093',
347
+ papayawhip: '#ffefd5',
348
+ peachpuff: '#ffdab9',
349
+ peru: '#cd853f',
350
+ pink: '#ffc0cb',
351
+ plum: '#dda0dd',
352
+ powderblue: '#b0e0e6',
353
+ purple: '#800080',
354
+ rebeccapurple: '#663399',
355
+ red: '#ff0000',
356
+ rosybrown: '#bc8f8f',
357
+ royalblue: '#4169e1',
358
+ saddlebrown: '#8b4513',
359
+ salmon: '#fa8072',
360
+ sandybrown: '#f4a460',
361
+ seagreen: '#2e8b57',
362
+ seashell: '#fff5ee',
363
+ sienna: '#a0522d',
364
+ silver: '#c0c0c0',
365
+ skyblue: '#87ceeb',
366
+ slateblue: '#6a5acd',
367
+ slategray: '#708090',
368
+ slategrey: '#708090',
369
+ snow: '#fffafa',
370
+ springgreen: '#00ff7f',
371
+ steelblue: '#4682b4',
372
+ tan: '#d2b48c',
373
+ teal: '#008080',
374
+ thistle: '#d8bfd8',
375
+ tomato: '#ff6347',
376
+ turquoise: '#40e0d0',
377
+ violet: '#ee82ee',
378
+ wheat: '#f5deb3',
379
+ white: '#ffffff',
380
+ whitesmoke: '#f5f5f5',
381
+ yellow: '#ffff00',
382
+ yellowgreen: '#9acd32'
383
+ };
384
+
385
+ function clampChannel(value) {
386
+ return Math.min(255, Math.max(0, Math.round(value)));
387
+ }
388
+
389
+ function normalizeRgbArgs(args) {
390
+ const parts = args.split(/[,\s/]+/).filter(Boolean);
391
+ if (parts.length < 3) return null;
392
+ const to255 = (value) => {
393
+ if (typeof value !== 'string' || !value) return null;
394
+ if (value.endsWith('%')) return clampChannel((parseFloat(value) / 100) * 255);
395
+ const number = Number(value);
396
+ return Number.isFinite(number) ? clampChannel(number) : null;
397
+ };
398
+ const red = to255(parts[0]);
399
+ const green = to255(parts[1]);
400
+ const blue = to255(parts[2]);
401
+ if (red === null || green === null || blue === null) return null;
402
+ return `#${[red, green, blue].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
403
+ }
404
+
405
+ /**
406
+ * Convert any supported CSS color into `#rrggbb`. Returns null when the
407
+ * value cannot be parsed. Supports: #rgb / #rrggbb (case-insensitive),
408
+ * CSS named colors, rgb() / rgba() with comma or modern space syntax,
409
+ * including percentages. Alpha in rgba() is ignored.
410
+ */
411
+ function normalizeColor(value) {
412
+ if (typeof value !== 'string') return null;
413
+ const input = value.trim();
414
+ if (!input) return null;
415
+ const lower = input.toLowerCase();
416
+ if (NAMED_COLORS[lower]) return NAMED_COLORS[lower];
417
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(input)) {
418
+ const hex = input.slice(1).toLowerCase();
419
+ if (hex.length === 3) return `#${hex.split('').map((c) => c + c).join('')}`;
420
+ return `#${hex}`;
421
+ }
422
+ const match = input.match(/^rgba?\((.*)\)$/i);
423
+ if (match) return normalizeRgbArgs(match[1]);
424
+ return null;
425
+ }
426
+
427
+ function hexToRgb01$1(color) {
428
+ const hex = normalizeColor(color);
429
+ if (!hex) return [0, 0, 0];
430
+ const value = Number.parseInt(hex.slice(1), 16);
431
+ return [
432
+ ((value >> 16) & 255) / 255,
433
+ ((value >> 8) & 255) / 255,
434
+ (value & 255) / 255
435
+ ];
436
+ }
437
+
438
+ function hexToRgba(color, alpha = 1) {
439
+ const hex = normalizeColor(color);
440
+ if (!hex) return 'rgba(0, 0, 0, 0)';
441
+ const value = Number.parseInt(hex.slice(1), 16);
442
+ const red = (value >> 16) & 255;
443
+ const green = (value >> 8) & 255;
444
+ const blue = value & 255;
445
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
446
+ }
447
+
448
+ function validateColors(colors) {
449
+ return Array.isArray(colors)
450
+ && colors.length === 4
451
+ && colors.every((color) => normalizeColor(color) !== null);
275
452
  }
276
453
 
277
454
  const VERTEX_SHADER$1 = `#version 300 es
@@ -499,12 +676,31 @@ class CosmicRenderer {
499
676
  this.timeOffset = preset.seed * 0.73;
500
677
  }
501
678
 
502
- setDprCap(cap) {
503
- this.options.dprCap = cap;
504
- this.resize();
505
- }
506
-
507
- randomize() {
679
+ setDprCap(cap) {
680
+ this.options.dprCap = cap;
681
+ this.resize();
682
+ }
683
+
684
+ setMouseColor(enabled) {
685
+ this.options.mouseColor = enabled !== false;
686
+ if (this.options.mouseColor) {
687
+ if (this.onPointerMove) return this;
688
+ this.#bindEvents();
689
+ return this;
690
+ }
691
+ if (this.onPointerMove) {
692
+ const target = this.eventTarget;
693
+ target.removeEventListener('pointermove', this.onPointerMove);
694
+ target.removeEventListener('pointerdown', this.onPointerMove);
695
+ target.removeEventListener('pointerleave', this.onPointerLeave);
696
+ this.onPointerMove = null;
697
+ this.onPointerLeave = null;
698
+ }
699
+ this.motionTarget = 0;
700
+ return this;
701
+ }
702
+
703
+ randomize() {
508
704
  this.preset.seed = Math.random() * 100;
509
705
  this.timeOffset = Math.random() * 40;
510
706
  }
@@ -584,10 +780,12 @@ class FallbackRenderer {
584
780
  }
585
781
  }
586
782
 
587
- drawNebula(time) {
588
- const ctx = this.context;
589
- const { width, height } = this.canvas;
590
- const gradient = ctx.createLinearGradient(0, 0, width, height);
783
+ drawNebula(time) {
784
+ const ctx = this.context;
785
+ const { width, height } = this.canvas;
786
+ const t = time * (this.preset.speed || 1);
787
+ const phase = (this.preset.seed || 0) * 0.7;
788
+ const gradient = ctx.createLinearGradient(0, 0, width, height);
591
789
  gradient.addColorStop(0, this.preset.colors[0]);
592
790
  gradient.addColorStop(0.38, this.preset.colors[1]);
593
791
  gradient.addColorStop(0.72, this.preset.colors[2]);
@@ -595,10 +793,10 @@ class FallbackRenderer {
595
793
  ctx.fillStyle = gradient;
596
794
  ctx.fillRect(0, 0, width, height);
597
795
 
598
- ctx.globalCompositeOperation = 'screen';
599
- for (let index = 0; index < 6; index += 1) {
600
- const x = (0.5 + 0.45 * Math.sin(time * 0.32 + index * 1.7)) * width;
601
- const y = (0.5 + 0.4 * Math.cos(time * 0.25 + index)) * height;
796
+ ctx.globalCompositeOperation = 'screen';
797
+ for (let index = 0; index < 6; index += 1) {
798
+ const x = (0.5 + 0.45 * Math.sin(t * 0.32 + index * 1.7 + phase)) * width;
799
+ const y = (0.5 + 0.4 * Math.cos(t * 0.25 + index + phase)) * height;
602
800
  const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
603
801
  const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
604
802
  glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
@@ -619,8 +817,11 @@ class FallbackRenderer {
619
817
  this.preset = preset;
620
818
  }
621
819
 
622
- randomize() {}
623
- dispose() {}
820
+ randomize() {
821
+ if (this.preset) this.preset.seed = Math.random() * 100;
822
+ }
823
+ setMouseColor() {}
824
+ dispose() {}
624
825
  }
625
826
 
626
827
  /**
@@ -729,78 +930,105 @@ function createVisibilityGuard(element) {
729
930
  };
730
931
  }
731
932
 
933
+ /**
934
+ * Run a callback asynchronously, right after the current task and before the
935
+ * next paint. Used for mount-time events (ready / error) so listeners that
936
+ * are attached after create() still receive them. Falls back for legacy
937
+ * browsers that lack queueMicrotask / Promise.
938
+ */
939
+ function nextTick(fn) {
940
+ if (typeof queueMicrotask === 'function') {
941
+ queueMicrotask(fn);
942
+ } else if (typeof Promise !== 'undefined' && typeof Promise.resolve === 'function') {
943
+ Promise.resolve().then(fn);
944
+ } else {
945
+ setTimeout(fn, 0);
946
+ }
947
+ }
948
+
732
949
  function prefersReducedMotion$2() {
733
950
  return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
734
951
  }
735
952
 
736
953
  /**
737
- * Mount a cosmic (nebula) capsule into `container`.
738
- *
739
- * Options: preset (id/code/name or object), width, height (number=px or
740
- * string with px/%/vw/vh/em/rem), colors, seed, speed, quality, interactive,
741
- * respectReducedMotion, copy, cssVars.
742
- */
743
- function createCapsule(container, options = {}) {
954
+ * Mount a cosmic (nebula) capsule into `container`.
955
+ *
956
+ * Options: preset (literary name), width, height (number=px or string with
957
+ * px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
958
+ * width in percent), text (HTML string or DOM nodes for the text slot),
959
+ * colorContent (HTML string or DOM nodes for the color slot), quality,
960
+ * renderer, respectReducedMotion, mouseColor, cssVars.
961
+ */
962
+ function createCapsule(container, options = {}) {
744
963
  if (!container || typeof container.appendChild !== 'function') {
745
964
  throw new Error('createCapsule: container element is required');
746
965
  }
747
966
 
748
- const preset = { ...getPreset('capsule', options.preset ?? 'NC-01') };
749
- const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
750
- const copy = { ...COPY, ...(merged.copy || {}) };
751
-
752
- const root = document.createElement('div');
753
- root.className = 'hj-capsule-root hj-capsule-cosmic';
754
- root.dataset.group = preset.group;
755
- root.dataset.mode = 'nebula';
756
- root.dataset.theme = preset.theme || 'light';
757
- root.setAttribute('aria-label', copy.capsuleAria.replace('{name}', preset.name));
758
-
759
- const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
760
-
761
- const copyText = (key, fallback) => {
762
- const value = copy[key];
763
- return value === undefined ? fallback : value;
764
- };
765
-
766
- function buildCopyHtml() {
767
- const codeText = copyText('code', preset.code);
768
- const nameText = copyText('name', preset.name);
769
- const subtitleText = copyText('subtitle', preset.subtitle);
770
- const stateText = copyText('state', 'LIVE COSMIC STUDY');
771
- let html = '';
772
- if (codeText) html += `<span class="hj-capsule-code">${codeText}</span>`;
773
- if (nameText) html += `<span class="hj-capsule-name">${nameText}</span>`;
774
- if (subtitleText) html += `<span class="hj-capsule-brand">${subtitleText}</span>`;
775
- if (stateText) html += `<span class="hj-capsule-state">${stateText}</span>`;
776
- return html;
777
- }
778
-
779
- const copyLayer = document.createElement('div');
780
- copyLayer.className = 'hj-capsule-copy';
781
- const renderCopy = () => {
782
- if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
783
- };
784
- renderCopy();
785
-
786
- const isHexColor = (value) => typeof value === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value);
787
- if (copy.background) {
788
- root.style.setProperty('--hj-copy-bg', isHexColor(copy.background)
789
- ? `linear-gradient(90deg, ${copy.background} 0%, ${copy.background} 74%, ${hexToRgba(copy.background, 0.97)} 84%, ${hexToRgba(copy.background, 0)} 100%)`
790
- : copy.background);
791
- }
792
- if (copy.color) root.style.setProperty('--hj-copy-color', copy.color);
793
-
794
- const canvas = document.createElement('canvas');
795
- canvas.className = 'hj-capsule-canvas';
796
- canvas.setAttribute('aria-hidden', 'true');
797
-
798
- if (copyEnabled) root.appendChild(copyLayer);
799
- root.appendChild(canvas);
800
- container.appendChild(root);
801
-
802
- const emitter = createEmitter();
803
- let paused = merged.respectReducedMotion && prefersReducedMotion$2();
967
+ const preset = { ...getPreset('capsule', options.preset ?? '初光') };
968
+ const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
969
+ const normalizedColors = merged.colors.map(normalizeColor);
970
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
971
+ preset.seed = merged.seed;
972
+ preset.speed = merged.speed;
973
+ let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
974
+
975
+ const root = document.createElement('div');
976
+ root.className = 'hj-capsule-root hj-capsule-cosmic';
977
+ root.dataset.group = preset.group;
978
+ root.dataset.mode = 'nebula';
979
+ root.dataset.theme = preset.theme || 'light';
980
+ root.setAttribute('role', 'img');
981
+ const updateAria = () => {
982
+ root.setAttribute('aria-label', customLabel || COPY.capsuleAria.replace('{name}', preset.name));
983
+ };
984
+ updateAria();
985
+
986
+ // Slot containers are always present but transparent by default: without
987
+ // content they are invisible, so "no slot" behaves like the old
988
+ // showCopy=false. They only provide geometry — no typography, padding or
989
+ // background is imposed on user content (see docs: 插槽 CSS 约定).
990
+ const fillContent = (layer, content) => {
991
+ layer.innerHTML = '';
992
+ if (content == null) return;
993
+ if (typeof content === 'string') {
994
+ layer.innerHTML = content;
995
+ return;
996
+ }
997
+ const nodes = Array.isArray(content) ? content : [content];
998
+ for (const node of nodes) {
999
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
1000
+ }
1001
+ };
1002
+
1003
+ const textLayer = document.createElement('div');
1004
+ textLayer.className = 'hj-capsule-text hj-capsule-copy';
1005
+
1006
+ const visualLayer = document.createElement('div');
1007
+ visualLayer.className = 'hj-capsule-visual';
1008
+
1009
+ fillContent(textLayer, options.text);
1010
+ fillContent(visualLayer, options.colorContent);
1011
+
1012
+ const canvas = document.createElement('canvas');
1013
+ canvas.className = 'hj-capsule-canvas';
1014
+ canvas.setAttribute('aria-hidden', 'true');
1015
+
1016
+ root.appendChild(textLayer);
1017
+ root.appendChild(visualLayer);
1018
+ root.appendChild(canvas);
1019
+ container.appendChild(root);
1020
+
1021
+ const emitter = createEmitter();
1022
+ let paused = merged.respectReducedMotion && prefersReducedMotion$2();
1023
+ let disposed = false;
1024
+ const dirty = {
1025
+ preset: false,
1026
+ seed: false,
1027
+ speed: false,
1028
+ colors: false,
1029
+ textRatio: false,
1030
+ cssVars: false
1031
+ };
804
1032
 
805
1033
  let renderer;
806
1034
  const useWebgl = merged.renderer !== 'canvas2d';
@@ -809,19 +1037,25 @@ function createCapsule(container, options = {}) {
809
1037
  renderer = new CosmicRenderer(canvas, merged, { dprCap: dprCapFor(merged.quality) });
810
1038
  } catch (error) {
811
1039
  renderer = new FallbackRenderer(canvas, merged);
812
- emitter.emit('error', { message: String(error && error.message ? error.message : error) });
1040
+ nextTick(() => {
1041
+ if (disposed) return;
1042
+ emitter.emit('error', { message: String(error && error.message ? error.message : error) });
1043
+ });
813
1044
  }
814
1045
  } else {
815
1046
  renderer = new FallbackRenderer(canvas, merged);
816
1047
  }
817
1048
 
818
- const applySize = () => {
819
- root.style.width = parseSize(merged.width);
820
- root.style.height = parseSize(merged.height);
821
- const vars = merged.cssVars || {};
822
- for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
823
- renderer.resize();
824
- };
1049
+ const applySize = () => {
1050
+ root.style.width = parseSize(merged.width);
1051
+ root.style.height = parseSize(merged.height);
1052
+ const vars = merged.cssVars || {};
1053
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1054
+ if (merged.textRatio !== undefined) {
1055
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
1056
+ }
1057
+ renderer.resize();
1058
+ };
825
1059
  applySize();
826
1060
 
827
1061
  const resizeObserver = typeof ResizeObserver !== 'undefined'
@@ -832,10 +1066,8 @@ function createCapsule(container, options = {}) {
832
1066
 
833
1067
  const visibility = createVisibilityGuard(root);
834
1068
 
835
- if (merged.interactive !== false) {
836
- root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
837
- root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
838
- }
1069
+ root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
1070
+ root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
839
1071
  root.addEventListener('click', (event) => {
840
1072
  emitter.emit('click', { event, preset: { ...preset } });
841
1073
  });
@@ -852,45 +1084,97 @@ function createCapsule(container, options = {}) {
852
1084
  () => paused
853
1085
  );
854
1086
 
855
- emitter.emit('ready', { preset: { ...preset } });
1087
+ nextTick(() => {
1088
+ if (disposed) return;
1089
+ emitter.emit('ready', { preset: { ...preset } });
1090
+ });
856
1091
 
857
- const syncCopy = () => {
858
- root.dataset.mode = 'nebula';
859
- root.dataset.theme = preset.theme || 'light';
860
- renderCopy();
861
- };
862
-
863
- return {
1092
+ const syncTheme = () => {
1093
+ root.dataset.mode = 'nebula';
1094
+ root.dataset.theme = preset.theme || 'light';
1095
+ updateAria();
1096
+ };
1097
+
1098
+ return {
864
1099
  element: root,
865
1100
  canvas,
866
1101
  preset,
867
1102
  on: emitter.on,
868
1103
  off: emitter.off,
869
- setPreset(ref) {
870
- const next = getPreset('capsule', ref);
871
- Object.assign(preset, next);
872
- renderer.setPreset(next);
873
- syncCopy();
874
- renderer.resize();
875
- emitter.emit('presetchange', { preset: { ...next } });
876
- return this;
1104
+ setPreset(ref) {
1105
+ const next = getPreset('capsule', ref);
1106
+ dirty.preset = true;
1107
+ Object.assign(preset, next);
1108
+ const nextColors = preset.colors.map(normalizeColor);
1109
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
1110
+ renderer.setPreset({ ...preset });
1111
+ syncTheme();
1112
+ renderer.resize();
1113
+ emitter.emit('presetchange', { preset: { ...next } });
1114
+ return this;
877
1115
  },
878
- setColors(colors) {
879
- if (!Array.isArray(colors) || colors.length !== 4) return this;
880
- preset.colors = colors;
881
- renderer.setPreset({ ...preset, colors });
882
- return this;
883
- },
884
- setSize(width, height) {
1116
+ setColors(colors) {
1117
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
1118
+ if (next.length !== 4 || next.some((color) => !color)) return this;
1119
+ dirty.colors = true;
1120
+ preset.colors = next;
1121
+ renderer.setPreset({ ...preset, colors: next });
1122
+ return this;
1123
+ },
1124
+ setSeed(seed) {
1125
+ const value = Number(seed);
1126
+ if (!Number.isFinite(value)) return this;
1127
+ dirty.seed = true;
1128
+ preset.seed = value;
1129
+ renderer.setPreset({ ...preset });
1130
+ return this;
1131
+ },
1132
+ setSpeed(speed) {
1133
+ const value = Number(speed);
1134
+ if (!Number.isFinite(value)) return this;
1135
+ dirty.speed = true;
1136
+ preset.speed = value;
1137
+ renderer.setPreset({ ...preset });
1138
+ return this;
1139
+ },
1140
+ setText(content) {
1141
+ fillContent(textLayer, content);
1142
+ return this;
1143
+ },
1144
+ setColorContent(content) {
1145
+ fillContent(visualLayer, content);
1146
+ return this;
1147
+ },
1148
+ setTextRatio(ratio) {
1149
+ const value = Number(ratio);
1150
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
1151
+ dirty.textRatio = true;
1152
+ merged.textRatio = value;
1153
+ root.style.setProperty('--hj-text-width', `${value}%`);
1154
+ return this;
1155
+ },
1156
+ setCssVars(vars) {
1157
+ if (!vars || typeof vars !== 'object') return this;
1158
+ dirty.cssVars = true;
1159
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
1160
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1161
+ return this;
1162
+ },
1163
+ setLabel(label) {
1164
+ customLabel = typeof label === 'string' && label ? label : null;
1165
+ updateAria();
1166
+ return this;
1167
+ },
1168
+ setSize(width, height) {
885
1169
  if (width !== undefined) merged.width = width;
886
1170
  if (height !== undefined) merged.height = height;
887
1171
  applySize();
888
1172
  return this;
889
1173
  },
890
- randomize() {
891
- renderer.randomize();
892
- return this;
893
- },
1174
+ randomize() {
1175
+ this.setSeed(Math.random() * 100);
1176
+ return this;
1177
+ },
894
1178
  pause() {
895
1179
  paused = true;
896
1180
  return this;
@@ -904,15 +1188,19 @@ function createCapsule(container, options = {}) {
904
1188
  if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
905
1189
  return this;
906
1190
  },
907
- dispose() {
908
- unsubscribe();
1191
+ dispose() {
1192
+ disposed = true;
1193
+ unsubscribe();
909
1194
  visibility.dispose();
910
1195
  if (resizeObserver) resizeObserver.disconnect();
911
1196
  else window.removeEventListener('resize', renderer.resize);
912
- renderer.dispose();
913
- root.remove();
914
- }
915
- };
1197
+ renderer.dispose();
1198
+ root.remove();
1199
+ },
1200
+ textRatio: merged.textRatio,
1201
+ cssVars: merged.cssVars,
1202
+ dirty
1203
+ };
916
1204
  }
917
1205
 
918
1206
  const PROGRESS_MOTION_WIDTH = 240;
@@ -1600,14 +1888,16 @@ const FLOW_PROFILES = {
1600
1888
  };
1601
1889
 
1602
1890
  class ProgressCapsuleController {
1603
- constructor({ root, canvas, valueElement, preset, emitter, options, copy }) {
1891
+ constructor({ root, canvas, valueElement, preset, emitter, options, copy, dirty }) {
1604
1892
  this.root = root;
1605
1893
  this.canvas = canvas;
1606
1894
  this.valueElement = valueElement;
1607
1895
  this.preset = preset;
1608
1896
  this.emitter = emitter;
1609
- this.options = options;
1610
- this.copy = copy;
1897
+ this.options = options;
1898
+ this.copy = copy;
1899
+ this.dirty = dirty;
1900
+ this.valueSuffix = options.valueSuffix ?? copy.valueSuffix;
1611
1901
  this.profile = preset.edgeStyle === 'tide'
1612
1902
  ? FLOW_PROFILES.tide
1613
1903
  : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
@@ -1648,11 +1938,22 @@ class ProgressCapsuleController {
1648
1938
  this.value = clamp(nextValue, this.min, this.max);
1649
1939
  const rounded = Math.round(this.value);
1650
1940
  this.root.style.setProperty('--progress', this.value.toFixed(2));
1651
- this.root.setAttribute('aria-valuenow', String(rounded));
1652
- this.root.setAttribute('aria-valuetext', `${rounded}${this.copy.valueSuffix}`);
1653
- this.valueElement.textContent = `${rounded}${this.copy.valueSuffix}`;
1654
- if (!this.suppressEvents) this.emitter.emit('change', { value: this.value, source });
1655
- }
1941
+ this.root.setAttribute('aria-valuenow', String(rounded));
1942
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1943
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1944
+ if (!this.suppressEvents) {
1945
+ if (this.dirty) this.dirty.value = true;
1946
+ this.emitter.emit('change', { value: this.value, source });
1947
+ }
1948
+ }
1949
+
1950
+ setValueSuffix(suffix) {
1951
+ this.valueSuffix = String(suffix == null ? '' : suffix);
1952
+ const rounded = Math.round(this.value);
1953
+ if (this.valueElement) this.valueElement.textContent = `${rounded}${this.valueSuffix}`;
1954
+ this.root.setAttribute('aria-valuetext', `${rounded}${this.valueSuffix}`);
1955
+ return this;
1956
+ }
1656
1957
 
1657
1958
  resizeCanvas() {
1658
1959
  const bounds = this.root.getBoundingClientRect();
@@ -1825,13 +2126,14 @@ class ProgressCapsuleController {
1825
2126
  if (clamped !== this.value) this.setProgress(clamped, 'prop');
1826
2127
  }
1827
2128
 
1828
- drawReferenceFlow() {
2129
+ drawReferenceFlow() {
1829
2130
  const ctx = this.ctx;
1830
2131
  const width = this.width;
1831
2132
  const height = this.height;
1832
2133
  if (!ctx || width <= 0 || height <= 0) return;
1833
2134
 
1834
- const shoreline = width * (this.value / 100);
2135
+ const range = Math.max(this.max - this.min, 1);
2136
+ const shoreline = width * Math.min(Math.max((this.value - this.min) / range, 0), 1);
1835
2137
  const time = this.flowTime;
1836
2138
  const [dark, accentA, accentB, glow] = this.preset.colors;
1837
2139
 
@@ -1949,32 +2251,49 @@ class ProgressCapsuleController {
1949
2251
  }
1950
2252
  }
1951
2253
 
1952
- updateFromPointer(event) {
1953
- const bounds = this.root.getBoundingClientRect();
1954
- const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
1955
- this.setProgress(ratio * 100, 'drag');
1956
- }
1957
-
1958
- beginDrag(event) {
1959
- if (event.button !== undefined && event.button !== 0) return;
1960
- event.preventDefault();
1961
- this.dragging = true;
1962
- this.root.classList.add('is-dragging');
1963
- try { this.root.setPointerCapture?.(event.pointerId); } catch {}
1964
- this.emitter.emit('dragstart', { value: this.value });
1965
- this.updateFromPointer(event);
1966
- }
1967
-
1968
- moveDrag(event) {
1969
- if (!this.dragging) return;
1970
- event.preventDefault();
1971
- this.updateFromPointer(event);
1972
- }
1973
-
1974
- endDrag(event) {
1975
- if (!this.dragging) return;
1976
- this.dragging = false;
1977
- this.root.classList.remove('is-dragging');
2254
+ updateFromPointer(event) {
2255
+ const bounds = this.root.getBoundingClientRect();
2256
+ const ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
2257
+ this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
2258
+ }
2259
+
2260
+ beginDrag(event) {
2261
+ if (event.button !== undefined && event.button !== 0) return;
2262
+ event.preventDefault();
2263
+ this.dragging = true;
2264
+ this.pendingPointer = null;
2265
+ this._dragRaf = 0;
2266
+ this.root.classList.add('is-dragging');
2267
+ try { this.root.setPointerCapture?.(event.pointerId); } catch {}
2268
+ this.emitter.emit('dragstart', { value: this.value });
2269
+ this.updateFromPointer(event);
2270
+ }
2271
+
2272
+ moveDrag(event) {
2273
+ if (!this.dragging) return;
2274
+ event.preventDefault();
2275
+ // rAF 合并:一帧最多一次 setProgress/change,视觉仍每帧更新。
2276
+ this.pendingPointer = event;
2277
+ if (this._dragRaf) return;
2278
+ this._dragRaf = requestAnimationFrame(() => {
2279
+ this._dragRaf = 0;
2280
+ const pending = this.pendingPointer;
2281
+ this.pendingPointer = null;
2282
+ if (pending) this.updateFromPointer(pending);
2283
+ });
2284
+ }
2285
+
2286
+ endDrag(event) {
2287
+ if (!this.dragging) return;
2288
+ this.dragging = false;
2289
+ if (this._dragRaf) {
2290
+ cancelAnimationFrame(this._dragRaf);
2291
+ this._dragRaf = 0;
2292
+ }
2293
+ const pending = this.pendingPointer;
2294
+ this.pendingPointer = null;
2295
+ if (pending) this.updateFromPointer(pending);
2296
+ this.root.classList.remove('is-dragging');
1978
2297
  try {
1979
2298
  if (event?.pointerId !== undefined && this.root.hasPointerCapture?.(event.pointerId)) {
1980
2299
  this.root.releasePointerCapture(event.pointerId);
@@ -1996,15 +2315,16 @@ class ProgressCapsuleController {
1996
2315
  }
1997
2316
 
1998
2317
  if (this.options.keyboard !== false) {
1999
- this.handlers.keydown = (event) => {
2000
- const actions = {
2001
- ArrowLeft: -2,
2002
- ArrowDown: -2,
2003
- ArrowRight: 2,
2004
- ArrowUp: 2,
2005
- PageDown: -10,
2006
- PageUp: 10
2007
- };
2318
+ this.handlers.keydown = (event) => {
2319
+ const step = (this.max - this.min) * 0.02;
2320
+ const actions = {
2321
+ ArrowLeft: -step,
2322
+ ArrowDown: -step,
2323
+ ArrowRight: step,
2324
+ ArrowUp: step,
2325
+ PageDown: -step * 5,
2326
+ PageUp: step * 5
2327
+ };
2008
2328
  if (event.key === 'Home') {
2009
2329
  event.preventDefault();
2010
2330
  this.setProgress(this.min, 'keyboard');
@@ -2042,8 +2362,9 @@ class ProgressCapsuleController {
2042
2362
  this.preset = { ...this.preset, colors };
2043
2363
  }
2044
2364
 
2045
- dispose() {
2046
- if (this.resizeObserver) this.resizeObserver.disconnect();
2365
+ dispose() {
2366
+ if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
2367
+ if (this.resizeObserver) this.resizeObserver.disconnect();
2047
2368
  else window.removeEventListener('resize', this.resizeCanvas);
2048
2369
  for (const name of Object.keys(this.handlers)) {
2049
2370
  const handler = this.handlers[name];
@@ -2054,103 +2375,127 @@ class ProgressCapsuleController {
2054
2375
  }
2055
2376
 
2056
2377
  /**
2057
- * Mount a fluid progress capsule into `container`.
2058
- *
2059
- * Options: preset (id/code/name or object), width, height, value, min, max,
2060
- * draggable, keyboard, colors, edgeStyle, quality,
2061
- * respectReducedMotion, copy, cssVars.
2062
- */
2063
- function createProgressCapsule(container, options = {}) {
2378
+ * Mount a fluid progress capsule into `container`.
2379
+ *
2380
+ * Options: preset (id/code/name or object), width, height, value, min, max,
2381
+ * draggable, keyboard, colors, edgeStyle, textRatio (0-100, text region
2382
+ * width in percent), text (HTML string or DOM nodes for the text slot),
2383
+ * colorContent (HTML string or DOM nodes for the color slot),
2384
+ * showValue (show/hide the right-side percentage), quality,
2385
+ * respectReducedMotion, cssVars.
2386
+ */
2387
+ function createProgressCapsule(container, options = {}) {
2064
2388
  if (!container || typeof container.appendChild !== 'function') {
2065
2389
  throw new Error('createProgressCapsule: container element is required');
2066
2390
  }
2067
2391
 
2068
- const preset = { ...getPreset('progress', options.preset ?? 'NC-10') };
2069
- const merged = normalizeOptions(DEFAULTS.progress, preset, options);
2070
- const copy = { ...COPY, ...(merged.copy || {}) };
2071
-
2072
- const root = document.createElement('div');
2073
- root.className = 'hj-capsule-root hj-progress-root';
2074
- root.setAttribute('role', 'slider');
2075
- root.setAttribute('tabindex', '0');
2076
- root.setAttribute('data-draggable', String(merged.draggable !== false));
2392
+ const preset = { ...getPreset('progress', options.preset ?? '星火') };
2393
+ const merged = normalizeOptions(DEFAULTS.progress, preset, options);
2394
+ const normalizedColors = merged.colors.map(normalizeColor);
2395
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2396
+ preset.edgeStyle = merged.edgeStyle;
2397
+ const copy = COPY;
2398
+ const customLabel = typeof options.label === 'string' && options.label ? options.label : null;
2399
+ // Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
2400
+ // 这里统一按 true 处理。
2401
+ const disabled = merged.disabled === true || merged.disabled === '' || merged.disabled === 'true';
2402
+ const effectiveDraggable = disabled ? false : merged.draggable !== false;
2403
+ const effectiveKeyboard = disabled ? false : merged.keyboard !== false;
2404
+ const dirty = {
2405
+ preset: false,
2406
+ colors: false,
2407
+ textRatio: false,
2408
+ cssVars: false,
2409
+ edgeStyle: false,
2410
+ value: false
2411
+ };
2412
+
2413
+ const root = document.createElement('div');
2414
+ root.className = 'hj-capsule-root hj-progress-root';
2415
+ root.setAttribute('role', 'slider');
2416
+ root.setAttribute('tabindex', '0');
2417
+ root.setAttribute('data-draggable', String(effectiveDraggable));
2418
+ if (disabled) {
2419
+ root.setAttribute('aria-disabled', 'true');
2420
+ root.classList.add('is-disabled');
2421
+ }
2077
2422
  root.setAttribute('aria-valuemin', String(merged.min ?? 0));
2078
2423
  root.setAttribute('aria-valuemax', String(merged.max ?? 100));
2079
2424
  root.setAttribute('aria-valuenow', String(preset.initialProgress));
2080
- root.setAttribute(
2081
- 'aria-label',
2082
- interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2083
- );
2084
- if (merged.keyboard === false) root.setAttribute('tabindex', '-1');
2425
+ const updateAria = () => {
2426
+ root.setAttribute(
2427
+ 'aria-label',
2428
+ customLabel || interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2429
+ );
2430
+ };
2431
+ updateAria();
2432
+ if (!effectiveKeyboard) root.setAttribute('tabindex', '-1');
2085
2433
 
2086
2434
  const canvas = document.createElement('canvas');
2087
2435
  canvas.className = 'hj-progress-canvas';
2088
2436
  canvas.setAttribute('aria-hidden', 'true');
2089
2437
 
2090
- const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
2091
-
2092
- const copyText = (key, fallback) => {
2093
- const value = copy[key];
2094
- return value === undefined ? fallback : value;
2095
- };
2096
-
2097
- function buildCopyHtml() {
2098
- let html = '';
2099
- const brandText = copy.brandName || '';
2100
- const subtitleText = copyText('subtitle', preset.subtitle);
2101
- if (brandText) html += `<span class="hj-progress-name">${brandText}</span>`;
2102
- if (subtitleText) html += `<span class="hj-progress-subtitle">${subtitleText}</span>`;
2103
- return html;
2104
- }
2105
-
2106
- const copyLayer = document.createElement('div');
2107
- copyLayer.className = 'hj-progress-copy';
2108
- const renderCopy = () => {
2109
- if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
2110
- };
2111
- renderCopy();
2112
-
2113
- if (copy.background) {
2114
- root.style.setProperty('--hj-progress-copy-bg', copy.background);
2115
- root.style.setProperty('--hj-progress-copy-pad', '6px 12px');
2116
- }
2117
- if (copy.color) root.style.setProperty('--hj-progress-copy-color', copy.color);
2118
-
2119
- const valueElement = document.createElement('div');
2120
- valueElement.className = 'hj-progress-value';
2121
- valueElement.setAttribute('aria-hidden', 'true');
2122
-
2123
- const dragLabel = document.createElement('span');
2124
- dragLabel.className = 'hj-progress-drag-label';
2125
- dragLabel.setAttribute('aria-hidden', 'true');
2126
- dragLabel.textContent = copy.dragLabel;
2127
-
2128
- root.appendChild(canvas);
2129
- if (copyEnabled) root.appendChild(copyLayer);
2130
- root.appendChild(valueElement);
2131
- root.appendChild(dragLabel);
2132
- container.appendChild(root);
2133
-
2134
- const emitter = createEmitter();
2135
- let paused = merged.respectReducedMotion && prefersReducedMotion$1();
2136
-
2137
- const applySize = () => {
2138
- root.style.width = parseSize(merged.width);
2139
- root.style.height = parseSize(merged.height);
2140
- const vars = merged.cssVars || {};
2141
- for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2142
- };
2438
+ // Slot containers are always present but transparent by default; they only
2439
+ // provide geometry, never typography/padding/background (docs: 插槽 CSS 约定).
2440
+ const fillContent = (layer, content) => {
2441
+ layer.innerHTML = '';
2442
+ if (content == null) return;
2443
+ if (typeof content === 'string') {
2444
+ layer.innerHTML = content;
2445
+ return;
2446
+ }
2447
+ const nodes = Array.isArray(content) ? content : [content];
2448
+ for (const node of nodes) {
2449
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
2450
+ }
2451
+ };
2452
+
2453
+ const textLayer = document.createElement('div');
2454
+ textLayer.className = 'hj-progress-text hj-progress-copy';
2455
+
2456
+ const visualLayer = document.createElement('div');
2457
+ visualLayer.className = 'hj-progress-visual';
2458
+
2459
+ fillContent(textLayer, options.text);
2460
+ fillContent(visualLayer, options.colorContent);
2461
+
2462
+ const valueElement = merged.showValue === false ? null : document.createElement('div');
2463
+ if (valueElement) {
2464
+ valueElement.className = 'hj-progress-value';
2465
+ valueElement.setAttribute('aria-hidden', 'true');
2466
+ }
2467
+
2468
+ root.appendChild(canvas);
2469
+ root.appendChild(textLayer);
2470
+ root.appendChild(visualLayer);
2471
+ if (valueElement) root.appendChild(valueElement);
2472
+ container.appendChild(root);
2473
+
2474
+ const emitter = createEmitter();
2475
+ let paused = merged.respectReducedMotion && prefersReducedMotion$1();
2476
+ let disposed = false;
2477
+
2478
+ const applySize = () => {
2479
+ root.style.width = parseSize(merged.width);
2480
+ root.style.height = parseSize(merged.height);
2481
+ const vars = merged.cssVars || {};
2482
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2483
+ if (merged.textRatio !== undefined) {
2484
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
2485
+ }
2486
+ };
2143
2487
  applySize();
2144
2488
 
2145
- const controller = new ProgressCapsuleController({
2146
- root,
2147
- canvas,
2148
- valueElement,
2149
- preset,
2150
- emitter,
2151
- options: merged,
2152
- copy
2153
- });
2489
+ const controller = new ProgressCapsuleController({
2490
+ root,
2491
+ canvas,
2492
+ valueElement,
2493
+ preset,
2494
+ emitter,
2495
+ options: { ...merged, draggable: effectiveDraggable, keyboard: effectiveKeyboard },
2496
+ copy,
2497
+ dirty
2498
+ });
2154
2499
 
2155
2500
  let overlay = null;
2156
2501
  if (merged.renderer !== 'canvas2d') {
@@ -2161,7 +2506,13 @@ function createProgressCapsule(container, options = {}) {
2161
2506
  getProgress: () => controller.value
2162
2507
  });
2163
2508
  }
2164
- if (overlay) controller.webglActive = true;
2509
+ if (overlay) controller.webglActive = true;
2510
+ else if (merged.renderer !== 'canvas2d') {
2511
+ nextTick(() => {
2512
+ if (disposed) return;
2513
+ emitter.emit('error', { message: 'WebGL2 unavailable, using Canvas2D fallback' });
2514
+ });
2515
+ }
2165
2516
 
2166
2517
  const visibility = createVisibilityGuard(root);
2167
2518
  let flowTime = 0;
@@ -2177,15 +2528,14 @@ function createProgressCapsule(container, options = {}) {
2177
2528
  () => paused
2178
2529
  );
2179
2530
 
2180
- emitter.emit('ready', { preset: { ...preset } });
2531
+ nextTick(() => {
2532
+ if (disposed) return;
2533
+ emitter.emit('ready', { preset: { ...preset } });
2534
+ });
2181
2535
 
2182
- const syncDom = () => {
2183
- root.setAttribute(
2184
- 'aria-label',
2185
- interpolate(copy.progressAria, { brand: copy.brandName, code: preset.code, name: preset.name })
2186
- );
2187
- renderCopy();
2188
- };
2536
+ const syncDom = () => {
2537
+ updateAria();
2538
+ };
2189
2539
 
2190
2540
  return {
2191
2541
  element: root,
@@ -2204,31 +2554,84 @@ function createProgressCapsule(container, options = {}) {
2204
2554
  controller.setRange(min, max);
2205
2555
  return this;
2206
2556
  },
2207
- setPreset(ref) {
2208
- const next = getPreset('progress', ref);
2209
- Object.assign(preset, next);
2210
- controller.preset = next;
2211
- controller.profile = next.edgeStyle === 'tide'
2212
- ? FLOW_PROFILES.tide
2213
- : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
2214
- controller.flowTime = stringSeed(next.id) * 31;
2215
- controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
2216
- syncDom();
2217
- if (overlay) {
2218
- overlay.dispose();
2219
- overlay = attachProgressFlowOverlay({ root, canvas, preset: next, getProgress: () => controller.value });
2220
- }
2221
- controller.webglActive = Boolean(overlay);
2222
- controller.resizeCanvas();
2223
- emitter.emit('presetchange', { preset: { ...next } });
2224
- return this;
2225
- },
2226
- setColors(colors) {
2227
- if (!Array.isArray(colors) || colors.length !== 4) return this;
2228
- controller.setColors(colors);
2229
- if (overlay) overlay.setColors(colors);
2230
- controller.resizeCanvas();
2231
- return this;
2557
+ setPreset(ref) {
2558
+ const next = getPreset('progress', ref);
2559
+ dirty.preset = true;
2560
+ Object.assign(preset, next);
2561
+ const nextColors = preset.colors.map(normalizeColor);
2562
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
2563
+ controller.preset = preset;
2564
+ controller.profile = next.edgeStyle === 'tide'
2565
+ ? FLOW_PROFILES.tide
2566
+ : (FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training']);
2567
+ controller.flowTime = stringSeed(next.id) * 31;
2568
+ controller.seed = stringSeed(`${next.id}-reference`) * Math.PI * 2;
2569
+ syncDom();
2570
+ if (overlay) {
2571
+ overlay.dispose();
2572
+ overlay = attachProgressFlowOverlay({ root, canvas, preset, getProgress: () => controller.value });
2573
+ }
2574
+ controller.webglActive = Boolean(overlay);
2575
+ controller.resizeCanvas();
2576
+ emitter.emit('presetchange', { preset: { ...preset } });
2577
+ return this;
2578
+ },
2579
+ setEdgeStyle(edgeStyle) {
2580
+ const value = String(edgeStyle || '').toLowerCase();
2581
+ if (value !== 'flow' && value !== 'tide') return this;
2582
+ dirty.edgeStyle = true;
2583
+ preset.edgeStyle = value;
2584
+ controller.profile = value === 'tide'
2585
+ ? FLOW_PROFILES.tide
2586
+ : (FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training']);
2587
+ if (overlay) {
2588
+ overlay.dispose();
2589
+ overlay = attachProgressFlowOverlay({ root, canvas, preset, getProgress: () => controller.value });
2590
+ }
2591
+ controller.webglActive = Boolean(overlay);
2592
+ controller.resizeCanvas();
2593
+ return this;
2594
+ },
2595
+ setText(content) {
2596
+ fillContent(textLayer, content);
2597
+ return this;
2598
+ },
2599
+ setColorContent(content) {
2600
+ fillContent(visualLayer, content);
2601
+ return this;
2602
+ },
2603
+ setTextRatio(ratio) {
2604
+ const value = Number(ratio);
2605
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
2606
+ dirty.textRatio = true;
2607
+ merged.textRatio = value;
2608
+ root.style.setProperty('--hj-text-width', `${value}%`);
2609
+ return this;
2610
+ },
2611
+ setCssVars(vars) {
2612
+ if (!vars || typeof vars !== 'object') return this;
2613
+ dirty.cssVars = true;
2614
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
2615
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
2616
+ return this;
2617
+ },
2618
+ setLabel(label) {
2619
+ customLabel = typeof label === 'string' && label ? label : null;
2620
+ updateAria();
2621
+ return this;
2622
+ },
2623
+ setValueSuffix(suffix) {
2624
+ controller.setValueSuffix(suffix);
2625
+ return this;
2626
+ },
2627
+ setColors(colors) {
2628
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
2629
+ if (next.length !== 4 || next.some((color) => !color)) return this;
2630
+ dirty.colors = true;
2631
+ controller.setColors(next);
2632
+ if (overlay) overlay.setColors(next);
2633
+ controller.resizeCanvas();
2634
+ return this;
2232
2635
  },
2233
2636
  setSize(width, height) {
2234
2637
  if (width !== undefined) merged.width = width;
@@ -2255,14 +2658,18 @@ function createProgressCapsule(container, options = {}) {
2255
2658
  controller.resizeCanvas();
2256
2659
  return this;
2257
2660
  },
2258
- dispose() {
2259
- unsubscribe();
2661
+ dispose() {
2662
+ disposed = true;
2663
+ unsubscribe();
2260
2664
  visibility.dispose();
2261
2665
  if (overlay) overlay.dispose();
2262
- controller.dispose();
2263
- root.remove();
2264
- }
2265
- };
2666
+ controller.dispose();
2667
+ root.remove();
2668
+ },
2669
+ textRatio: merged.textRatio,
2670
+ cssVars: merged.cssVars,
2671
+ dirty
2672
+ };
2266
2673
  }
2267
2674
 
2268
2675
  function prefersReducedMotion() {
@@ -2287,8 +2694,8 @@ function createColorBackground(host, options = {}) {
2287
2694
  throw new Error('createColorBackground: host element is required');
2288
2695
  }
2289
2696
 
2290
- const preset = { ...getPreset('capsule', options.preset ?? 'NC-01') };
2291
- const merged = normalizeOptions(
2697
+ const preset = { ...getPreset('capsule', options.preset ?? '初光') };
2698
+ const merged = normalizeOptions(
2292
2699
  {
2293
2700
  quality: DEFAULTS.capsule.quality,
2294
2701
  renderer: DEFAULTS.capsule.renderer,
@@ -2298,8 +2705,12 @@ function createColorBackground(host, options = {}) {
2298
2705
  fallbackColor: true
2299
2706
  },
2300
2707
  preset,
2301
- options
2302
- );
2708
+ options
2709
+ );
2710
+ const normalizedColors = merged.colors.map(normalizeColor);
2711
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2712
+ preset.seed = merged.seed;
2713
+ preset.speed = merged.speed;
2303
2714
 
2304
2715
  const computed = getComputedStyle(host);
2305
2716
  if (computed.position === 'static' || computed.position === '') {
@@ -2328,8 +2739,18 @@ function createColorBackground(host, options = {}) {
2328
2739
  layer.appendChild(canvas);
2329
2740
  host.appendChild(layer);
2330
2741
 
2331
- const emitter = createEmitter();
2332
- let paused = merged.respectReducedMotion && prefersReducedMotion();
2742
+ const emitter = createEmitter();
2743
+ let paused = merged.respectReducedMotion && prefersReducedMotion();
2744
+ let disposed = false;
2745
+ const dirty = {
2746
+ preset: false,
2747
+ seed: false,
2748
+ speed: false,
2749
+ colors: false,
2750
+ opacity: false,
2751
+ fallbackColor: false,
2752
+ mouseColor: false
2753
+ };
2333
2754
 
2334
2755
  let renderer;
2335
2756
  const useWebgl = merged.renderer !== 'canvas2d';
@@ -2342,9 +2763,12 @@ function createColorBackground(host, options = {}) {
2342
2763
  });
2343
2764
  } catch (error) {
2344
2765
  renderer = new FallbackRenderer(canvas, merged);
2345
- emitter.emit('error', {
2346
- message: String(error && error.message ? error.message : error)
2347
- });
2766
+ nextTick(() => {
2767
+ if (disposed) return;
2768
+ emitter.emit('error', {
2769
+ message: String(error && error.message ? error.message : error)
2770
+ });
2771
+ });
2348
2772
  }
2349
2773
  } else {
2350
2774
  renderer = new FallbackRenderer(canvas, merged);
@@ -2368,7 +2792,10 @@ function createColorBackground(host, options = {}) {
2368
2792
  () => paused
2369
2793
  );
2370
2794
 
2371
- emitter.emit('ready', { preset: { ...preset } });
2795
+ nextTick(() => {
2796
+ if (disposed) return;
2797
+ emitter.emit('ready', { preset: { ...preset } });
2798
+ });
2372
2799
 
2373
2800
  return {
2374
2801
  element: host,
@@ -2377,38 +2804,72 @@ function createColorBackground(host, options = {}) {
2377
2804
  preset,
2378
2805
  on: emitter.on,
2379
2806
  off: emitter.off,
2380
- setPreset(ref) {
2381
- const next = getPreset('capsule', ref);
2382
- Object.assign(preset, next);
2383
- renderer.setPreset(next);
2384
- renderer.resize();
2385
- syncLayerVars();
2386
- emitter.emit('presetchange', { preset: { ...next } });
2387
- return this;
2388
- },
2389
- setColors(colors) {
2390
- if (!Array.isArray(colors) || colors.length !== 4) return this;
2391
- preset.colors = colors;
2392
- renderer.setPreset({ ...preset, colors });
2393
- syncLayerVars();
2394
- return this;
2395
- },
2807
+ setPreset(ref) {
2808
+ const next = getPreset('capsule', ref);
2809
+ dirty.preset = true;
2810
+ Object.assign(preset, next);
2811
+ const nextColors = preset.colors.map(normalizeColor);
2812
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
2813
+ renderer.setPreset({ ...preset });
2814
+ renderer.resize();
2815
+ syncLayerVars();
2816
+ emitter.emit('presetchange', { preset: { ...preset } });
2817
+ return this;
2818
+ },
2819
+ setColors(colors) {
2820
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
2821
+ if (next.length !== 4 || next.some((color) => !color)) return this;
2822
+ dirty.colors = true;
2823
+ preset.colors = next;
2824
+ renderer.setPreset({ ...preset, colors: next });
2825
+ syncLayerVars();
2826
+ return this;
2827
+ },
2828
+ setSeed(seed) {
2829
+ const value = Number(seed);
2830
+ if (!Number.isFinite(value)) return this;
2831
+ dirty.seed = true;
2832
+ preset.seed = value;
2833
+ renderer.setPreset({ ...preset });
2834
+ return this;
2835
+ },
2836
+ setSpeed(speed) {
2837
+ const value = Number(speed);
2838
+ if (!Number.isFinite(value)) return this;
2839
+ dirty.speed = true;
2840
+ preset.speed = value;
2841
+ renderer.setPreset({ ...preset });
2842
+ return this;
2843
+ },
2844
+ setFallbackColor(value) {
2845
+ dirty.fallbackColor = true;
2846
+ merged.fallbackColor = value;
2847
+ syncLayerVars();
2848
+ return this;
2849
+ },
2850
+ setMouseColor(value) {
2851
+ dirty.mouseColor = true;
2852
+ merged.mouseColor = value !== false;
2853
+ if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
2854
+ return this;
2855
+ },
2396
2856
  setQuality(quality) {
2397
2857
  merged.quality = quality;
2398
2858
  if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
2399
2859
  return this;
2400
2860
  },
2401
- setOpacity(value) {
2402
- const opacity = Number(value);
2403
- if (!Number.isFinite(opacity)) return this;
2404
- merged.opacity = Math.min(1, Math.max(0, opacity));
2861
+ setOpacity(value) {
2862
+ const opacity = Number(value);
2863
+ if (!Number.isFinite(opacity)) return this;
2864
+ dirty.opacity = true;
2865
+ merged.opacity = Math.min(1, Math.max(0, opacity));
2405
2866
  layer.style.setProperty('--dlc-color-opacity', String(merged.opacity));
2406
2867
  return this;
2407
2868
  },
2408
- randomize() {
2409
- renderer.randomize();
2410
- return this;
2411
- },
2869
+ randomize() {
2870
+ this.setSeed(Math.random() * 100);
2871
+ return this;
2872
+ },
2412
2873
  pause() {
2413
2874
  paused = true;
2414
2875
  return this;
@@ -2417,15 +2878,20 @@ function createColorBackground(host, options = {}) {
2417
2878
  paused = false;
2418
2879
  return this;
2419
2880
  },
2420
- dispose() {
2421
- unsubscribe();
2881
+ dispose() {
2882
+ disposed = true;
2883
+ unsubscribe();
2422
2884
  visibility.dispose();
2423
2885
  if (resizeObserver) resizeObserver.disconnect();
2424
2886
  else window.removeEventListener('resize', renderer.resize);
2425
- renderer.dispose();
2426
- layer.remove();
2427
- }
2428
- };
2887
+ renderer.dispose();
2888
+ layer.remove();
2889
+ },
2890
+ opacity: merged.opacity,
2891
+ fallbackColor: merged.fallbackColor,
2892
+ mouseColor: merged.mouseColor,
2893
+ dirty
2894
+ };
2429
2895
  }
2430
2896
 
2431
- export { ALL_PRESETS, COPY, CosmicRenderer, DEFAULTS, FallbackRenderer, PRESETS, PROGRESS_PRESETS, ProgressCapsuleController, ProgressFlowRenderer, QUALITY_TIERS, createCapsule, createColorBackground, createProgressCapsule, dprCapFor, getPreset, hexToRgb01$1 as hexToRgb01, hexToRgba, parseSize, pauseAll, resumeAll, validateColors, validatePreset, validateProgressPreset };
2897
+ export { ALL_PRESETS, COPY, CosmicRenderer, DEFAULTS, FallbackRenderer, PRESETS, PROGRESS_PRESETS, ProgressCapsuleController, ProgressFlowRenderer, QUALITY_TIERS, createCapsule, createColorBackground, createProgressCapsule, dprCapFor, getPreset, hexToRgb01$1 as hexToRgb01, hexToRgba, normalizeColor, parseSize, pauseAll, resumeAll, validateColors };