@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/CHANGELOG.md +33 -3
- package/README.md +277 -248
- package/dist/capsule.cjs +421 -103
- package/dist/capsule.mjs +481 -197
- package/dist/color.cjs +342 -37
- package/dist/color.mjs +390 -105
- package/dist/index.cjs +745 -203
- package/dist/index.mjs +923 -457
- package/dist/index.umd.js +745 -203
- package/dist/index.umd.min.js +1 -1
- package/dist/progress.cjs +487 -116
- package/dist/progress.mjs +568 -245
- package/dist/vue2.cjs +1092 -208
- package/dist/vue2.mjs +1280 -501
- package/dist/vue3.cjs +1092 -208
- package/dist/vue3.mjs +1280 -501
- package/package.json +1 -1
- package/styles/base.css +12 -18
- package/styles/capsule.css +34 -81
- package/styles/progress.css +37 -57
- package/types/index.d.ts +97 -81
- package/types/vue3.d.ts +59 -20
package/dist/capsule.mjs
CHANGED
|
@@ -112,88 +112,261 @@ const PALETTES = {
|
|
|
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: '
|
|
117
|
-
{ id: 'ocean', code: 'NC-02', name: '
|
|
118
|
-
{ id: 'klein', code: 'NC-03', name: '
|
|
119
|
-
{ id: 'ultraviolet', code: 'NC-04', name: '
|
|
120
|
-
{ id: 'chrome', code: 'NC-05', name: '
|
|
121
|
-
{ id: 'plus', code: 'NC-06', name: '
|
|
115
|
+
const CAPSULE_PRESETS = [
|
|
116
|
+
{ id: 'original', code: 'NC-01', name: '初光', group: 'warm', seed: 1.7, speed: 0.5, colors: [...PALETTES.original] },
|
|
117
|
+
{ id: 'ocean', code: 'NC-02', name: '沧溟', group: 'cold', seed: 8.2, speed: 0.48, colors: [...PALETTES.ocean] },
|
|
118
|
+
{ id: 'klein', code: 'NC-03', name: '玄夜', group: 'cold', seed: 14.1, speed: 0.49, colors: [...PALETTES.klein] },
|
|
119
|
+
{ id: 'ultraviolet', code: 'NC-04', name: '紫霄', group: 'cold', seed: 23.4, speed: 0.47, colors: [...PALETTES.ultraviolet] },
|
|
120
|
+
{ id: 'chrome', code: 'NC-05', name: '银汉', group: 'cold', seed: 37.8, speed: 0.42, colors: [...PALETTES.chrome] },
|
|
121
|
+
{ id: 'plus', code: 'NC-06', name: '熔金', group: 'warm', seed: 51.3, speed: 0.5, colors: [...PALETTES.plus] }
|
|
122
122
|
];
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const list = CAPSULE_PRESETS ;
|
|
139
|
-
if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
|
|
140
|
-
if (ref && typeof ref === 'object') {
|
|
141
|
-
const valid = validatePreset(ref) ;
|
|
142
|
-
if (!valid) throw new Error(`Invalid ${kind} preset object`);
|
|
143
|
-
return ref;
|
|
144
|
-
}
|
|
145
|
-
const key = String(ref).trim().toLowerCase();
|
|
146
|
-
const found = list.find(
|
|
147
|
-
(preset) =>
|
|
148
|
-
preset.id.toLowerCase() === key ||
|
|
149
|
-
preset.code.toLowerCase() === key ||
|
|
150
|
-
preset.name.toLowerCase() === key
|
|
151
|
-
);
|
|
152
|
-
if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
|
|
153
|
-
return found;
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a preset by its literary name (e.g. '沧溟').
|
|
126
|
+
* Matching is case-insensitive and ignores surrounding whitespace,
|
|
127
|
+
* so '沧溟' and ' 沧溟 ' both work.
|
|
128
|
+
*/
|
|
129
|
+
function getPreset(kind, ref) {
|
|
130
|
+
const list = CAPSULE_PRESETS ;
|
|
131
|
+
if (!list) throw new Error(`Unknown preset kind: ${kind} (use "capsule" or "progress")`);
|
|
132
|
+
const key = String(ref).trim().toLowerCase();
|
|
133
|
+
const found = list.find(
|
|
134
|
+
(preset) => preset.name.toLowerCase() === key
|
|
135
|
+
);
|
|
136
|
+
if (!found) throw new Error(`Unknown ${kind} preset: ${ref}`);
|
|
137
|
+
return found;
|
|
154
138
|
}
|
|
155
139
|
|
|
156
|
-
const DEFAULTS = {
|
|
157
|
-
capsule: {
|
|
158
|
-
width: '100%',
|
|
159
|
-
height: 160,
|
|
160
|
-
quality: 'auto',
|
|
161
|
-
renderer: 'auto',
|
|
162
|
-
respectReducedMotion: true,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
showCopy: true
|
|
140
|
+
const DEFAULTS = {
|
|
141
|
+
capsule: {
|
|
142
|
+
width: '100%',
|
|
143
|
+
height: 160,
|
|
144
|
+
quality: 'auto',
|
|
145
|
+
renderer: 'auto',
|
|
146
|
+
respectReducedMotion: true,
|
|
147
|
+
mouseColor: true,
|
|
148
|
+
textRatio: 39
|
|
166
149
|
}};
|
|
167
150
|
|
|
168
|
-
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
dragLabel: 'DRAG',
|
|
175
|
-
valueSuffix: '%',
|
|
176
|
-
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 = {
|
|
177
157
|
capsuleAria: '打开 {name} 沉浸预览'
|
|
178
158
|
};
|
|
179
159
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
];
|
|
197
370
|
}
|
|
198
371
|
|
|
199
372
|
const VERTEX_SHADER = `#version 300 es
|
|
@@ -421,12 +594,31 @@ class CosmicRenderer {
|
|
|
421
594
|
this.timeOffset = preset.seed * 0.73;
|
|
422
595
|
}
|
|
423
596
|
|
|
424
|
-
setDprCap(cap) {
|
|
425
|
-
this.options.dprCap = cap;
|
|
426
|
-
this.resize();
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
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() {
|
|
430
622
|
this.preset.seed = Math.random() * 100;
|
|
431
623
|
this.timeOffset = Math.random() * 40;
|
|
432
624
|
}
|
|
@@ -506,10 +698,12 @@ class FallbackRenderer {
|
|
|
506
698
|
}
|
|
507
699
|
}
|
|
508
700
|
|
|
509
|
-
drawNebula(time) {
|
|
510
|
-
const ctx = this.context;
|
|
511
|
-
const { width, height } = this.canvas;
|
|
512
|
-
const
|
|
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);
|
|
513
707
|
gradient.addColorStop(0, this.preset.colors[0]);
|
|
514
708
|
gradient.addColorStop(0.38, this.preset.colors[1]);
|
|
515
709
|
gradient.addColorStop(0.72, this.preset.colors[2]);
|
|
@@ -517,10 +711,10 @@ class FallbackRenderer {
|
|
|
517
711
|
ctx.fillStyle = gradient;
|
|
518
712
|
ctx.fillRect(0, 0, width, height);
|
|
519
713
|
|
|
520
|
-
ctx.globalCompositeOperation = 'screen';
|
|
521
|
-
for (let index = 0; index < 6; index += 1) {
|
|
522
|
-
const x = (0.5 + 0.45 * Math.sin(
|
|
523
|
-
const y = (0.5 + 0.4 * Math.cos(
|
|
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;
|
|
524
718
|
const radius = Math.max(width, height) * (0.08 + (index % 3) * 0.04);
|
|
525
719
|
const glow = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
526
720
|
glow.addColorStop(0, rgb(this.preset.colors[(index + 1) % 4], 0.24));
|
|
@@ -541,8 +735,11 @@ class FallbackRenderer {
|
|
|
541
735
|
this.preset = preset;
|
|
542
736
|
}
|
|
543
737
|
|
|
544
|
-
randomize() {
|
|
545
|
-
|
|
738
|
+
randomize() {
|
|
739
|
+
if (this.preset) this.preset.seed = Math.random() * 100;
|
|
740
|
+
}
|
|
741
|
+
setMouseColor() {}
|
|
742
|
+
dispose() {}
|
|
546
743
|
}
|
|
547
744
|
|
|
548
745
|
/**
|
|
@@ -642,78 +839,105 @@ function createVisibilityGuard(element) {
|
|
|
642
839
|
};
|
|
643
840
|
}
|
|
644
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
|
+
|
|
645
858
|
function prefersReducedMotion() {
|
|
646
859
|
return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
647
860
|
}
|
|
648
861
|
|
|
649
862
|
/**
|
|
650
|
-
* Mount a cosmic (nebula) capsule into `container`.
|
|
651
|
-
*
|
|
652
|
-
* Options: preset (
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
|
|
656
|
-
|
|
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 = {}) {
|
|
657
872
|
if (!container || typeof container.appendChild !== 'function') {
|
|
658
873
|
throw new Error('createCapsule: container element is required');
|
|
659
874
|
}
|
|
660
875
|
|
|
661
|
-
const preset = { ...getPreset('capsule', options.preset ?? '
|
|
662
|
-
const merged = normalizeOptions(DEFAULTS.capsule, preset, options);
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
root
|
|
670
|
-
root.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
canvas.
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
root.appendChild(canvas);
|
|
713
|
-
container.appendChild(root);
|
|
714
|
-
|
|
715
|
-
const emitter = createEmitter();
|
|
716
|
-
let paused = merged.respectReducedMotion && prefersReducedMotion();
|
|
876
|
+
const preset = { ...getPreset('capsule', options.preset ?? '初光') };
|
|
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
|
+
};
|
|
717
941
|
|
|
718
942
|
let renderer;
|
|
719
943
|
const useWebgl = merged.renderer !== 'canvas2d';
|
|
@@ -722,19 +946,25 @@ function createCapsule(container, options = {}) {
|
|
|
722
946
|
renderer = new CosmicRenderer(canvas, merged, { dprCap: dprCapFor(merged.quality) });
|
|
723
947
|
} catch (error) {
|
|
724
948
|
renderer = new FallbackRenderer(canvas, merged);
|
|
725
|
-
|
|
949
|
+
nextTick(() => {
|
|
950
|
+
if (disposed) return;
|
|
951
|
+
emitter.emit('error', { message: String(error && error.message ? error.message : error) });
|
|
952
|
+
});
|
|
726
953
|
}
|
|
727
954
|
} else {
|
|
728
955
|
renderer = new FallbackRenderer(canvas, merged);
|
|
729
956
|
}
|
|
730
957
|
|
|
731
|
-
const applySize = () => {
|
|
732
|
-
root.style.width = parseSize(merged.width);
|
|
733
|
-
root.style.height = parseSize(merged.height);
|
|
734
|
-
const vars = merged.cssVars || {};
|
|
735
|
-
for (const name of Object.keys(vars)) root.style.setProperty(name, vars[name]);
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
};
|
|
738
968
|
applySize();
|
|
739
969
|
|
|
740
970
|
const resizeObserver = typeof ResizeObserver !== 'undefined'
|
|
@@ -745,10 +975,8 @@ function createCapsule(container, options = {}) {
|
|
|
745
975
|
|
|
746
976
|
const visibility = createVisibilityGuard(root);
|
|
747
977
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
|
|
751
|
-
}
|
|
978
|
+
root.addEventListener('pointerenter', () => emitter.emit('pointerenter', { preset: { ...preset } }));
|
|
979
|
+
root.addEventListener('pointerleave', () => emitter.emit('pointerleave', { preset: { ...preset } }));
|
|
752
980
|
root.addEventListener('click', (event) => {
|
|
753
981
|
emitter.emit('click', { event, preset: { ...preset } });
|
|
754
982
|
});
|
|
@@ -765,45 +993,97 @@ function createCapsule(container, options = {}) {
|
|
|
765
993
|
() => paused
|
|
766
994
|
);
|
|
767
995
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
root.dataset.theme = preset.theme || 'light';
|
|
773
|
-
renderCopy();
|
|
774
|
-
};
|
|
996
|
+
nextTick(() => {
|
|
997
|
+
if (disposed) return;
|
|
998
|
+
emitter.emit('ready', { preset: { ...preset } });
|
|
999
|
+
});
|
|
775
1000
|
|
|
776
|
-
|
|
1001
|
+
const syncTheme = () => {
|
|
1002
|
+
root.dataset.mode = 'nebula';
|
|
1003
|
+
root.dataset.theme = preset.theme || 'light';
|
|
1004
|
+
updateAria();
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
777
1008
|
element: root,
|
|
778
1009
|
canvas,
|
|
779
1010
|
preset,
|
|
780
1011
|
on: emitter.on,
|
|
781
1012
|
off: emitter.off,
|
|
782
|
-
setPreset(ref) {
|
|
783
|
-
const next = getPreset('capsule', ref);
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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;
|
|
790
1024
|
},
|
|
791
|
-
setColors(colors) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1025
|
+
setColors(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 });
|
|
1031
|
+
return this;
|
|
1032
|
+
},
|
|
1033
|
+
setSeed(seed) {
|
|
1034
|
+
const value = Number(seed);
|
|
1035
|
+
if (!Number.isFinite(value)) return this;
|
|
1036
|
+
dirty.seed = true;
|
|
1037
|
+
preset.seed = value;
|
|
1038
|
+
renderer.setPreset({ ...preset });
|
|
1039
|
+
return this;
|
|
1040
|
+
},
|
|
1041
|
+
setSpeed(speed) {
|
|
1042
|
+
const value = Number(speed);
|
|
1043
|
+
if (!Number.isFinite(value)) return this;
|
|
1044
|
+
dirty.speed = true;
|
|
1045
|
+
preset.speed = value;
|
|
1046
|
+
renderer.setPreset({ ...preset });
|
|
1047
|
+
return this;
|
|
1048
|
+
},
|
|
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) {
|
|
798
1078
|
if (width !== undefined) merged.width = width;
|
|
799
1079
|
if (height !== undefined) merged.height = height;
|
|
800
1080
|
applySize();
|
|
801
1081
|
return this;
|
|
802
1082
|
},
|
|
803
|
-
randomize() {
|
|
804
|
-
|
|
805
|
-
return this;
|
|
806
|
-
},
|
|
1083
|
+
randomize() {
|
|
1084
|
+
this.setSeed(Math.random() * 100);
|
|
1085
|
+
return this;
|
|
1086
|
+
},
|
|
807
1087
|
pause() {
|
|
808
1088
|
paused = true;
|
|
809
1089
|
return this;
|
|
@@ -817,15 +1097,19 @@ function createCapsule(container, options = {}) {
|
|
|
817
1097
|
if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
|
|
818
1098
|
return this;
|
|
819
1099
|
},
|
|
820
|
-
dispose() {
|
|
821
|
-
|
|
1100
|
+
dispose() {
|
|
1101
|
+
disposed = true;
|
|
1102
|
+
unsubscribe();
|
|
822
1103
|
visibility.dispose();
|
|
823
1104
|
if (resizeObserver) resizeObserver.disconnect();
|
|
824
1105
|
else window.removeEventListener('resize', renderer.resize);
|
|
825
|
-
renderer.dispose();
|
|
826
|
-
root.remove();
|
|
827
|
-
}
|
|
828
|
-
|
|
1106
|
+
renderer.dispose();
|
|
1107
|
+
root.remove();
|
|
1108
|
+
},
|
|
1109
|
+
textRatio: merged.textRatio,
|
|
1110
|
+
cssVars: merged.cssVars,
|
|
1111
|
+
dirty
|
|
1112
|
+
};
|
|
829
1113
|
}
|
|
830
1114
|
|
|
831
1115
|
export { createCapsule };
|