@5even7/dlc-ui 0.2.0 → 0.2.2

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/capsule.mjs CHANGED
@@ -137,46 +137,236 @@ function getPreset(kind, ref) {
137
137
  return found;
138
138
  }
139
139
 
140
- const DEFAULTS = {
141
- capsule: {
142
- width: '100%',
143
- height: 160,
140
+ const DEFAULTS = {
141
+ capsule: {
142
+ width: '100%',
143
+ height: 160,
144
144
  quality: 'auto',
145
145
  renderer: 'auto',
146
146
  respectReducedMotion: true,
147
147
  mouseColor: true,
148
- showCopy: true
148
+ textRatio: 39
149
149
  }};
150
150
 
151
- /**
152
- * All user-facing copy lives here so wording/brand changes never require a
153
- * global search. Templates use {brand} {code} {name} placeholders.
154
- */
155
- const COPY = {
156
- brandName: '画境观屿',
157
- dragLabel: 'DRAG',
158
- valueSuffix: '%',
159
- progressAria: '{brand} {code} 加载进度',
151
+ /**
152
+ * Internal accessibility labels and small UI text. Since the copy API was
153
+ * removed (slots own the text area), these are no longer user-overridable.
154
+ * Templates use {brand} {code} {name} placeholders.
155
+ */
156
+ const COPY = {
160
157
  capsuleAria: '打开 {name} 沉浸预览'
161
158
  };
162
159
 
163
- function hexToRgb01(hex) {
164
- const normalized = hex.replace('#', '');
165
- const value = Number.parseInt(normalized, 16);
166
- return [
167
- ((value >> 16) & 255) / 255,
168
- ((value >> 8) & 255) / 255,
169
- (value & 255) / 255
170
- ];
171
- }
172
-
173
- function hexToRgba(hex, alpha = 1) {
174
- const normalized = hex.replace('#', '');
175
- const value = Number.parseInt(normalized, 16);
176
- const red = (value >> 16) & 255;
177
- const green = (value >> 8) & 255;
178
- const blue = value & 255;
179
- return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
160
+ /**
161
+ * Color helpers. `normalizeColor` converts any supported CSS color
162
+ * (hex / named / rgb() / rgba()) into a 6-digit hex string, so renderers can
163
+ * keep working with #rrggbb only. rgba() alpha is intentionally dropped:
164
+ * the WebGL shader has no per-color alpha channel, and the component is
165
+ * opaque by design use component-level `opacity` for transparency.
166
+ */
167
+
168
+ const NAMED_COLORS = {
169
+ aliceblue: '#f0f8ff',
170
+ antiquewhite: '#faebd7',
171
+ aqua: '#00ffff',
172
+ aquamarine: '#7fffd4',
173
+ azure: '#f0ffff',
174
+ beige: '#f5f5dc',
175
+ bisque: '#ffe4c4',
176
+ black: '#000000',
177
+ blanchedalmond: '#ffebcd',
178
+ blue: '#0000ff',
179
+ blueviolet: '#8a2be2',
180
+ brown: '#a52a2a',
181
+ burlywood: '#deb887',
182
+ cadetblue: '#5f9ea0',
183
+ chartreuse: '#7fff00',
184
+ chocolate: '#d2691e',
185
+ coral: '#ff7f50',
186
+ cornflowerblue: '#6495ed',
187
+ cornsilk: '#fff8dc',
188
+ crimson: '#dc143c',
189
+ cyan: '#00ffff',
190
+ darkblue: '#00008b',
191
+ darkcyan: '#008b8b',
192
+ darkgoldenrod: '#b8860b',
193
+ darkgray: '#a9a9a9',
194
+ darkgreen: '#006400',
195
+ darkgrey: '#a9a9a9',
196
+ darkkhaki: '#bdb76b',
197
+ darkmagenta: '#8b008b',
198
+ darkolivegreen: '#556b2f',
199
+ darkorange: '#ff8c00',
200
+ darkorchid: '#9932cc',
201
+ darkred: '#8b0000',
202
+ darksalmon: '#e9967a',
203
+ darkseagreen: '#8fbc8f',
204
+ darkslateblue: '#483d8b',
205
+ darkslategray: '#2f4f4f',
206
+ darkslategrey: '#2f4f4f',
207
+ darkturquoise: '#00ced1',
208
+ darkviolet: '#9400d3',
209
+ deeppink: '#ff1493',
210
+ deepskyblue: '#00bfff',
211
+ dimgray: '#696969',
212
+ dimgrey: '#696969',
213
+ dodgerblue: '#1e90ff',
214
+ firebrick: '#b22222',
215
+ floralwhite: '#fffaf0',
216
+ forestgreen: '#228b22',
217
+ fuchsia: '#ff00ff',
218
+ gainsboro: '#dcdcdc',
219
+ ghostwhite: '#f8f8ff',
220
+ gold: '#ffd700',
221
+ goldenrod: '#daa520',
222
+ gray: '#808080',
223
+ green: '#008000',
224
+ greenyellow: '#adff2f',
225
+ grey: '#808080',
226
+ honeydew: '#f0fff0',
227
+ hotpink: '#ff69b4',
228
+ indianred: '#cd5c5c',
229
+ indigo: '#4b0082',
230
+ ivory: '#fffff0',
231
+ khaki: '#f0e68c',
232
+ lavender: '#e6e6fa',
233
+ lavenderblush: '#fff0f5',
234
+ lawngreen: '#7cfc00',
235
+ lemonchiffon: '#fffacd',
236
+ lightblue: '#add8e6',
237
+ lightcoral: '#f08080',
238
+ lightcyan: '#e0ffff',
239
+ lightgoldenrodyellow: '#fafad2',
240
+ lightgray: '#d3d3d3',
241
+ lightgreen: '#90ee90',
242
+ lightgrey: '#d3d3d3',
243
+ lightpink: '#ffb6c1',
244
+ lightsalmon: '#ffa07a',
245
+ lightseagreen: '#20b2aa',
246
+ lightskyblue: '#87cefa',
247
+ lightslategray: '#778899',
248
+ lightslategrey: '#778899',
249
+ lightsteelblue: '#b0c4de',
250
+ lightyellow: '#ffffe0',
251
+ lime: '#00ff00',
252
+ limegreen: '#32cd32',
253
+ linen: '#faf0e6',
254
+ magenta: '#ff00ff',
255
+ maroon: '#800000',
256
+ mediumaquamarine: '#66cdaa',
257
+ mediumblue: '#0000cd',
258
+ mediumorchid: '#ba55d3',
259
+ mediumpurple: '#9370db',
260
+ mediumseagreen: '#3cb371',
261
+ mediumslateblue: '#7b68ee',
262
+ mediumspringgreen: '#00fa9a',
263
+ mediumturquoise: '#48d1cc',
264
+ mediumvioletred: '#c71585',
265
+ midnightblue: '#191970',
266
+ mintcream: '#f5fffa',
267
+ mistyrose: '#ffe4e1',
268
+ moccasin: '#ffe4b5',
269
+ navajowhite: '#ffdead',
270
+ navy: '#000080',
271
+ oldlace: '#fdf5e6',
272
+ olive: '#808000',
273
+ olivedrab: '#6b8e23',
274
+ orange: '#ffa500',
275
+ orangered: '#ff4500',
276
+ orchid: '#da70d6',
277
+ palegoldenrod: '#eee8aa',
278
+ palegreen: '#98fb98',
279
+ paleturquoise: '#afeeee',
280
+ palevioletred: '#db7093',
281
+ papayawhip: '#ffefd5',
282
+ peachpuff: '#ffdab9',
283
+ peru: '#cd853f',
284
+ pink: '#ffc0cb',
285
+ plum: '#dda0dd',
286
+ powderblue: '#b0e0e6',
287
+ purple: '#800080',
288
+ rebeccapurple: '#663399',
289
+ red: '#ff0000',
290
+ rosybrown: '#bc8f8f',
291
+ royalblue: '#4169e1',
292
+ saddlebrown: '#8b4513',
293
+ salmon: '#fa8072',
294
+ sandybrown: '#f4a460',
295
+ seagreen: '#2e8b57',
296
+ seashell: '#fff5ee',
297
+ sienna: '#a0522d',
298
+ silver: '#c0c0c0',
299
+ skyblue: '#87ceeb',
300
+ slateblue: '#6a5acd',
301
+ slategray: '#708090',
302
+ slategrey: '#708090',
303
+ snow: '#fffafa',
304
+ springgreen: '#00ff7f',
305
+ steelblue: '#4682b4',
306
+ tan: '#d2b48c',
307
+ teal: '#008080',
308
+ thistle: '#d8bfd8',
309
+ tomato: '#ff6347',
310
+ turquoise: '#40e0d0',
311
+ violet: '#ee82ee',
312
+ wheat: '#f5deb3',
313
+ white: '#ffffff',
314
+ whitesmoke: '#f5f5f5',
315
+ yellow: '#ffff00',
316
+ yellowgreen: '#9acd32'
317
+ };
318
+
319
+ function clampChannel(value) {
320
+ return Math.min(255, Math.max(0, Math.round(value)));
321
+ }
322
+
323
+ function normalizeRgbArgs(args) {
324
+ const parts = args.split(/[,\s/]+/).filter(Boolean);
325
+ if (parts.length < 3) return null;
326
+ const to255 = (value) => {
327
+ if (typeof value !== 'string' || !value) return null;
328
+ if (value.endsWith('%')) return clampChannel((parseFloat(value) / 100) * 255);
329
+ const number = Number(value);
330
+ return Number.isFinite(number) ? clampChannel(number) : null;
331
+ };
332
+ const red = to255(parts[0]);
333
+ const green = to255(parts[1]);
334
+ const blue = to255(parts[2]);
335
+ if (red === null || green === null || blue === null) return null;
336
+ return `#${[red, green, blue].map((n) => n.toString(16).padStart(2, '0')).join('')}`;
337
+ }
338
+
339
+ /**
340
+ * Convert any supported CSS color into `#rrggbb`. Returns null when the
341
+ * value cannot be parsed. Supports: #rgb / #rrggbb (case-insensitive),
342
+ * CSS named colors, rgb() / rgba() with comma or modern space syntax,
343
+ * including percentages. Alpha in rgba() is ignored.
344
+ */
345
+ function normalizeColor(value) {
346
+ if (typeof value !== 'string') return null;
347
+ const input = value.trim();
348
+ if (!input) return null;
349
+ const lower = input.toLowerCase();
350
+ if (NAMED_COLORS[lower]) return NAMED_COLORS[lower];
351
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(input)) {
352
+ const hex = input.slice(1).toLowerCase();
353
+ if (hex.length === 3) return `#${hex.split('').map((c) => c + c).join('')}`;
354
+ return `#${hex}`;
355
+ }
356
+ const match = input.match(/^rgba?\((.*)\)$/i);
357
+ if (match) return normalizeRgbArgs(match[1]);
358
+ return null;
359
+ }
360
+
361
+ function hexToRgb01(color) {
362
+ const hex = normalizeColor(color);
363
+ if (!hex) return [0, 0, 0];
364
+ const value = Number.parseInt(hex.slice(1), 16);
365
+ return [
366
+ ((value >> 16) & 255) / 255,
367
+ ((value >> 8) & 255) / 255,
368
+ (value & 255) / 255
369
+ ];
180
370
  }
181
371
 
182
372
  const VERTEX_SHADER = `#version 300 es
@@ -404,12 +594,31 @@ class CosmicRenderer {
404
594
  this.timeOffset = preset.seed * 0.73;
405
595
  }
406
596
 
407
- setDprCap(cap) {
408
- this.options.dprCap = cap;
409
- this.resize();
410
- }
411
-
412
- randomize() {
597
+ setDprCap(cap) {
598
+ this.options.dprCap = cap;
599
+ this.resize();
600
+ }
601
+
602
+ setMouseColor(enabled) {
603
+ this.options.mouseColor = enabled !== false;
604
+ if (this.options.mouseColor) {
605
+ if (this.onPointerMove) return this;
606
+ this.#bindEvents();
607
+ return this;
608
+ }
609
+ if (this.onPointerMove) {
610
+ const target = this.eventTarget;
611
+ target.removeEventListener('pointermove', this.onPointerMove);
612
+ target.removeEventListener('pointerdown', this.onPointerMove);
613
+ target.removeEventListener('pointerleave', this.onPointerLeave);
614
+ this.onPointerMove = null;
615
+ this.onPointerLeave = null;
616
+ }
617
+ this.motionTarget = 0;
618
+ return this;
619
+ }
620
+
621
+ randomize() {
413
622
  this.preset.seed = Math.random() * 100;
414
623
  this.timeOffset = Math.random() * 40;
415
624
  }
@@ -489,10 +698,12 @@ class FallbackRenderer {
489
698
  }
490
699
  }
491
700
 
492
- drawNebula(time) {
493
- const ctx = this.context;
494
- const { width, height } = this.canvas;
495
- const gradient = ctx.createLinearGradient(0, 0, width, height);
701
+ drawNebula(time) {
702
+ const ctx = this.context;
703
+ const { width, height } = this.canvas;
704
+ const t = time * (this.preset.speed || 1);
705
+ const phase = (this.preset.seed || 0) * 0.7;
706
+ const gradient = ctx.createLinearGradient(0, 0, width, height);
496
707
  gradient.addColorStop(0, this.preset.colors[0]);
497
708
  gradient.addColorStop(0.38, this.preset.colors[1]);
498
709
  gradient.addColorStop(0.72, this.preset.colors[2]);
@@ -500,10 +711,10 @@ class FallbackRenderer {
500
711
  ctx.fillStyle = gradient;
501
712
  ctx.fillRect(0, 0, width, height);
502
713
 
503
- ctx.globalCompositeOperation = 'screen';
504
- for (let index = 0; index < 6; index += 1) {
505
- const x = (0.5 + 0.45 * Math.sin(time * 0.32 + index * 1.7)) * width;
506
- const y = (0.5 + 0.4 * Math.cos(time * 0.25 + index)) * height;
714
+ ctx.globalCompositeOperation = 'screen';
715
+ for (let index = 0; index < 6; index += 1) {
716
+ const x = (0.5 + 0.45 * Math.sin(t * 0.32 + index * 1.7 + phase)) * width;
717
+ const y = (0.5 + 0.4 * Math.cos(t * 0.25 + index + phase)) * height;
507
718
  const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
508
719
  const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
509
720
  glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
@@ -524,8 +735,11 @@ class FallbackRenderer {
524
735
  this.preset = preset;
525
736
  }
526
737
 
527
- randomize() {}
528
- dispose() {}
738
+ randomize() {
739
+ if (this.preset) this.preset.seed = Math.random() * 100;
740
+ }
741
+ setMouseColor() {}
742
+ dispose() {}
529
743
  }
530
744
 
531
745
  /**
@@ -625,78 +839,105 @@ function createVisibilityGuard(element) {
625
839
  };
626
840
  }
627
841
 
842
+ /**
843
+ * Run a callback asynchronously, right after the current task and before the
844
+ * next paint. Used for mount-time events (ready / error) so listeners that
845
+ * are attached after create() still receive them. Falls back for legacy
846
+ * browsers that lack queueMicrotask / Promise.
847
+ */
848
+ function nextTick(fn) {
849
+ if (typeof queueMicrotask === 'function') {
850
+ queueMicrotask(fn);
851
+ } else if (typeof Promise !== 'undefined' && typeof Promise.resolve === 'function') {
852
+ Promise.resolve().then(fn);
853
+ } else {
854
+ setTimeout(fn, 0);
855
+ }
856
+ }
857
+
628
858
  function prefersReducedMotion() {
629
859
  return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
630
860
  }
631
861
 
632
862
  /**
633
- * Mount a cosmic (nebula) capsule into `container`.
634
- *
635
- * Options: preset (id/code/name or object), width, height (number=px or
636
- * string with px/%/vw/vh/em/rem), colors, seed, speed, quality,
637
- * respectReducedMotion, copy, cssVars.
638
- */
639
- function createCapsule(container, options = {}) {
863
+ * Mount a cosmic (nebula) capsule into `container`.
864
+ *
865
+ * Options: preset (literary name), width, height (number=px or string with
866
+ * px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
867
+ * width in percent), text (HTML string or DOM nodes for the text slot),
868
+ * colorContent (HTML string or DOM nodes for the color slot), quality,
869
+ * renderer, respectReducedMotion, mouseColor, cssVars.
870
+ */
871
+ function createCapsule(container, options = {}) {
640
872
  if (!container || typeof container.appendChild !== 'function') {
641
873
  throw new Error('createCapsule: container element is required');
642
874
  }
643
875
 
644
876
  const preset = { ...getPreset('capsule', options.preset ?? '初光') };
645
- const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
646
- const copy = { ...COPY, ...(merged.copy || {}) };
647
-
648
- const root = document.createElement('div');
649
- root.className = 'hj-capsule-root hj-capsule-cosmic';
650
- root.dataset.group = preset.group;
651
- root.dataset.mode = 'nebula';
652
- root.dataset.theme = preset.theme || 'light';
653
- root.setAttribute('aria-label', copy.capsuleAria.replace('{name}', preset.name));
654
-
655
- const copyEnabled = copy.enabled !== false && merged.showCopy !== false;
656
-
657
- const copyText = (key, fallback) => {
658
- const value = copy[key];
659
- return value === undefined ? fallback : value;
660
- };
661
-
662
- function buildCopyHtml() {
663
- const codeText = copyText('code', preset.code);
664
- const nameText = copyText('name', preset.name);
665
- const subtitleText = copyText('subtitle', preset.subtitle);
666
- const stateText = copyText('state', 'LIVE COSMIC STUDY');
667
- let html = '';
668
- if (codeText) html += `<span class="hj-capsule-code">${codeText}</span>`;
669
- if (nameText) html += `<span class="hj-capsule-name">${nameText}</span>`;
670
- if (subtitleText) html += `<span class="hj-capsule-brand">${subtitleText}</span>`;
671
- if (stateText) html += `<span class="hj-capsule-state">${stateText}</span>`;
672
- return html;
673
- }
674
-
675
- const copyLayer = document.createElement('div');
676
- copyLayer.className = 'hj-capsule-copy';
677
- const renderCopy = () => {
678
- if (copyEnabled) copyLayer.innerHTML = buildCopyHtml();
679
- };
680
- renderCopy();
681
-
682
- const isHexColor = (value) => typeof value === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value);
683
- if (copy.background) {
684
- root.style.setProperty('--hj-copy-bg', isHexColor(copy.background)
685
- ? `linear-gradient(90deg, ${copy.background} 0%, ${copy.background} 74%, ${hexToRgba(copy.background, 0.97)} 84%, ${hexToRgba(copy.background, 0)} 100%)`
686
- : copy.background);
687
- }
688
- if (copy.color) root.style.setProperty('--hj-copy-color', copy.color);
689
-
690
- const canvas = document.createElement('canvas');
691
- canvas.className = 'hj-capsule-canvas';
692
- canvas.setAttribute('aria-hidden', 'true');
693
-
694
- if (copyEnabled) root.appendChild(copyLayer);
695
- root.appendChild(canvas);
696
- container.appendChild(root);
697
-
698
- const emitter = createEmitter();
699
- let paused = merged.respectReducedMotion && prefersReducedMotion();
877
+ const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
878
+ const normalizedColors = merged.colors.map(normalizeColor);
879
+ if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
880
+ preset.seed = merged.seed;
881
+ preset.speed = merged.speed;
882
+ let customLabel = typeof options.label === 'string' && options.label ? options.label : null;
883
+
884
+ const root = document.createElement('div');
885
+ root.className = 'hj-capsule-root hj-capsule-cosmic';
886
+ root.dataset.group = preset.group;
887
+ root.dataset.mode = 'nebula';
888
+ root.dataset.theme = preset.theme || 'light';
889
+ root.setAttribute('role', 'img');
890
+ const updateAria = () => {
891
+ root.setAttribute('aria-label', customLabel || COPY.capsuleAria.replace('{name}', preset.name));
892
+ };
893
+ updateAria();
894
+
895
+ // Slot containers are always present but transparent by default: without
896
+ // content they are invisible, so "no slot" behaves like the old
897
+ // showCopy=false. They only provide geometry — no typography, padding or
898
+ // background is imposed on user content (see docs: 插槽 CSS 约定).
899
+ const fillContent = (layer, content) => {
900
+ layer.innerHTML = '';
901
+ if (content == null) return;
902
+ if (typeof content === 'string') {
903
+ layer.innerHTML = content;
904
+ return;
905
+ }
906
+ const nodes = Array.isArray(content) ? content : [content];
907
+ for (const node of nodes) {
908
+ if (node && typeof node.nodeType === 'number') layer.appendChild(node);
909
+ }
910
+ };
911
+
912
+ const textLayer = document.createElement('div');
913
+ textLayer.className = 'hj-capsule-text hj-capsule-copy';
914
+
915
+ const visualLayer = document.createElement('div');
916
+ visualLayer.className = 'hj-capsule-visual';
917
+
918
+ fillContent(textLayer, options.text);
919
+ fillContent(visualLayer, options.colorContent);
920
+
921
+ const canvas = document.createElement('canvas');
922
+ canvas.className = 'hj-capsule-canvas';
923
+ canvas.setAttribute('aria-hidden', 'true');
924
+
925
+ root.appendChild(textLayer);
926
+ root.appendChild(visualLayer);
927
+ root.appendChild(canvas);
928
+ container.appendChild(root);
929
+
930
+ const emitter = createEmitter();
931
+ let paused = merged.respectReducedMotion && prefersReducedMotion();
932
+ let disposed = false;
933
+ const dirty = {
934
+ preset: false,
935
+ seed: false,
936
+ speed: false,
937
+ colors: false,
938
+ textRatio: false,
939
+ cssVars: false
940
+ };
700
941
 
701
942
  let renderer;
702
943
  const useWebgl = merged.renderer !== 'canvas2d';
@@ -705,19 +946,25 @@ function createCapsule(container, options = {}) {
705
946
  renderer = new CosmicRenderer(canvas, merged, { dprCap: dprCapFor(merged.quality) });
706
947
  } catch (error) {
707
948
  renderer = new FallbackRenderer(canvas, merged);
708
- emitter.emit('error', { message: String(error && error.message ? error.message : error) });
949
+ nextTick(() => {
950
+ if (disposed) return;
951
+ emitter.emit('error', { message: String(error && error.message ? error.message : error) });
952
+ });
709
953
  }
710
954
  } else {
711
955
  renderer = new FallbackRenderer(canvas, merged);
712
956
  }
713
957
 
714
- const applySize = () => {
715
- root.style.width = parseSize(merged.width);
716
- root.style.height = parseSize(merged.height);
717
- const vars = merged.cssVars || {};
718
- for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
719
- renderer.resize();
720
- };
958
+ const applySize = () => {
959
+ root.style.width = parseSize(merged.width);
960
+ root.style.height = parseSize(merged.height);
961
+ const vars = merged.cssVars || {};
962
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
963
+ if (merged.textRatio !== undefined) {
964
+ root.style.setProperty('--hj-text-width', `${merged.textRatio}%`);
965
+ }
966
+ renderer.resize();
967
+ };
721
968
  applySize();
722
969
 
723
970
  const resizeObserver = typeof ResizeObserver !== 'undefined'
@@ -746,38 +993,47 @@ function createCapsule(container, options = {}) {
746
993
  () => paused
747
994
  );
748
995
 
749
- emitter.emit('ready', { preset: { ...preset } });
750
-
751
- const syncCopy = () => {
752
- root.dataset.mode = 'nebula';
753
- root.dataset.theme = preset.theme || 'light';
754
- renderCopy();
755
- };
996
+ nextTick(() => {
997
+ if (disposed) return;
998
+ emitter.emit('ready', { preset: { ...preset } });
999
+ });
756
1000
 
757
- return {
1001
+ const syncTheme = () => {
1002
+ root.dataset.mode = 'nebula';
1003
+ root.dataset.theme = preset.theme || 'light';
1004
+ updateAria();
1005
+ };
1006
+
1007
+ return {
758
1008
  element: root,
759
1009
  canvas,
760
1010
  preset,
761
1011
  on: emitter.on,
762
1012
  off: emitter.off,
763
- setPreset(ref) {
764
- const next = getPreset('capsule', ref);
765
- Object.assign(preset, next);
766
- renderer.setPreset(next);
767
- syncCopy();
768
- renderer.resize();
769
- emitter.emit('presetchange', { preset: { ...next } });
770
- return this;
1013
+ setPreset(ref) {
1014
+ const next = getPreset('capsule', ref);
1015
+ dirty.preset = true;
1016
+ Object.assign(preset, next);
1017
+ const nextColors = preset.colors.map(normalizeColor);
1018
+ if (nextColors.every(Boolean)) preset.colors = nextColors;
1019
+ renderer.setPreset({ ...preset });
1020
+ syncTheme();
1021
+ renderer.resize();
1022
+ emitter.emit('presetchange', { preset: { ...next } });
1023
+ return this;
771
1024
  },
772
1025
  setColors(colors) {
773
- if (!Array.isArray(colors) || colors.length !== 4) return this;
774
- preset.colors = colors;
775
- renderer.setPreset({ ...preset, colors });
1026
+ const next = Array.isArray(colors) ? colors.map(normalizeColor) : [];
1027
+ if (next.length !== 4 || next.some((color) => !color)) return this;
1028
+ dirty.colors = true;
1029
+ preset.colors = next;
1030
+ renderer.setPreset({ ...preset, colors: next });
776
1031
  return this;
777
1032
  },
778
1033
  setSeed(seed) {
779
1034
  const value = Number(seed);
780
1035
  if (!Number.isFinite(value)) return this;
1036
+ dirty.seed = true;
781
1037
  preset.seed = value;
782
1038
  renderer.setPreset({ ...preset });
783
1039
  return this;
@@ -785,20 +1041,49 @@ function createCapsule(container, options = {}) {
785
1041
  setSpeed(speed) {
786
1042
  const value = Number(speed);
787
1043
  if (!Number.isFinite(value)) return this;
1044
+ dirty.speed = true;
788
1045
  preset.speed = value;
789
1046
  renderer.setPreset({ ...preset });
790
1047
  return this;
791
1048
  },
792
- setSize(width, height) {
1049
+ setText(content) {
1050
+ fillContent(textLayer, content);
1051
+ return this;
1052
+ },
1053
+ setColorContent(content) {
1054
+ fillContent(visualLayer, content);
1055
+ return this;
1056
+ },
1057
+ setTextRatio(ratio) {
1058
+ const value = Number(ratio);
1059
+ if (!Number.isFinite(value) || value < 0 || value > 100) return this;
1060
+ dirty.textRatio = true;
1061
+ merged.textRatio = value;
1062
+ root.style.setProperty('--hj-text-width', `${value}%`);
1063
+ return this;
1064
+ },
1065
+ setCssVars(vars) {
1066
+ if (!vars || typeof vars !== 'object') return this;
1067
+ dirty.cssVars = true;
1068
+ merged.cssVars = { ...(merged.cssVars || {}), ...vars };
1069
+ for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
1070
+ return this;
1071
+ },
1072
+ setLabel(label) {
1073
+ customLabel = typeof label === 'string' && label ? label : null;
1074
+ updateAria();
1075
+ return this;
1076
+ },
1077
+ setSize(width, height) {
793
1078
  if (width !== undefined) merged.width = width;
794
1079
  if (height !== undefined) merged.height = height;
795
1080
  applySize();
796
1081
  return this;
797
1082
  },
798
- randomize() {
799
- renderer.randomize();
800
- return this;
801
- },
1083
+ randomize() {
1084
+ this.setSeed(Math.random() * 100);
1085
+ return this;
1086
+ },
802
1087
  pause() {
803
1088
  paused = true;
804
1089
  return this;
@@ -812,15 +1097,19 @@ function createCapsule(container, options = {}) {
812
1097
  if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
813
1098
  return this;
814
1099
  },
815
- dispose() {
816
- unsubscribe();
1100
+ dispose() {
1101
+ disposed = true;
1102
+ unsubscribe();
817
1103
  visibility.dispose();
818
1104
  if (resizeObserver) resizeObserver.disconnect();
819
1105
  else window.removeEventListener('resize', renderer.resize);
820
- renderer.dispose();
821
- root.remove();
822
- }
823
- };
1106
+ renderer.dispose();
1107
+ root.remove();
1108
+ },
1109
+ textRatio: merged.textRatio,
1110
+ cssVars: merged.cssVars,
1111
+ dirty
1112
+ };
824
1113
  }
825
1114
 
826
1115
  export { createCapsule };