@5even7/dlc-ui 0.2.11 → 0.2.13

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.umd.js CHANGED
@@ -189,9 +189,9 @@
189
189
  }
190
190
  }
191
191
 
192
- /**
193
- * Tiny event emitter. Accepts any event name so the API stays open for
194
- * future events (hover, click, custom) without breaking changes.
192
+ /**
193
+ * Tiny event emitter. Accepts any event name so the API stays open for
194
+ * future events (hover, click, custom) without breaking changes.
195
195
  */
196
196
  function createEmitter() {
197
197
  // Plain object storage (no Map/Set) so the legacy build has no API
@@ -241,9 +241,9 @@
241
241
 
242
242
  var UNIT_PATTERN = /^[\d.]+(?:px|%|vw|vh|vmin|vmax|em|rem)$/;
243
243
 
244
- /**
245
- * Normalize a size option to a CSS length string.
246
- * Accepts numbers (treated as px) or strings with px/%, vw/vh/vmin/vmax, em/rem.
244
+ /**
245
+ * Normalize a size option to a CSS length string.
246
+ * Accepts numbers (treated as px) or strings with px/%, vw/vh/vmin/vmax, em/rem.
247
247
  */
248
248
  function parseSize(value) {
249
249
  if (typeof value === 'number') {
@@ -282,11 +282,17 @@
282
282
  var tier = QUALITY_TIERS[quality] || QUALITY_TIERS.medium;
283
283
  return tier.dpr;
284
284
  }
285
+ function effectiveDprCap(quality) {
286
+ var renderScale = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
287
+ var scale = Number(renderScale);
288
+ var normalizedScale = Number.isFinite(scale) ? Math.min(1, Math.max(0.25, scale)) : 1;
289
+ return Math.max(0.5, dprCapFor(quality) * normalizedScale);
290
+ }
285
291
 
286
- /**
287
- * Merge defaults < preset < user. Unknown user keys are preserved so the
288
- * component API can grow (new props, cssVars, callbacks) without a breaking
289
- * change.
292
+ /**
293
+ * Merge defaults < preset < user. Unknown user keys are preserved so the
294
+ * component API can grow (new props, cssVars, callbacks) without a breaking
295
+ * change.
290
296
  */
291
297
  function normalizeOptions(defaults, preset, user) {
292
298
  // undefined means "not provided" (e.g. Vue $props with unset props):
@@ -304,9 +310,9 @@
304
310
  return _objectSpread2(_objectSpread2(_objectSpread2({}, defaults), presetOptions), userOptions);
305
311
  }
306
312
 
307
- /**
308
- * 公共色板:NC-01~NC-06 的四色组(底色 / 主色 / 辅色 / 高光色)。
309
- * Capsule 预置与 dlc-color 等组件共用,新增色板只改这里。
313
+ /**
314
+ * 公共色板:NC-01~NC-06 的四色组(底色 / 主色 / 辅色 / 高光色)。
315
+ * Capsule 预置与 dlc-color 等组件共用,新增色板只改这里。
310
316
  */
311
317
  var PALETTES$1 = {
312
318
  original: ['#FFF3EA', '#F5B27A', '#F67BC6', '#A978E8'],
@@ -317,9 +323,9 @@
317
323
  plus: ['#FFF0E6', '#F6C26B', '#F98A64', '#E86D74']
318
324
  };
319
325
 
320
- /**
321
- * Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
322
- * 颜色引用公共色板,seed/speed 决定形态与流速。
326
+ /**
327
+ * Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
328
+ * 颜色引用公共色板,seed/speed 决定形态与流速。
323
329
  */
324
330
  var CAPSULE_PRESETS = [{
325
331
  id: 'original',
@@ -407,6 +413,15 @@
407
413
  initialProgress: 30,
408
414
  edgeStyle: 'tide',
409
415
  colors: ['#0A2239', '#2E9BFF', '#7FE3FF', '#EAF9FF']
416
+ }, {
417
+ id: 'botanic-lab',
418
+ code: 'NC-14',
419
+ name: '森息',
420
+ subtitle: 'MOSS CULTURE / ROOTING',
421
+ group: 'progress',
422
+ initialProgress: 46,
423
+ edgeStyle: 'flow',
424
+ colors: ['#102A23', '#287A58', '#9BD65B', '#F2FFC4']
410
425
  }];
411
426
 
412
427
  var PRESETS = CAPSULE_PRESETS;
@@ -438,6 +453,11 @@
438
453
  height: 160,
439
454
  quality: 'auto',
440
455
  renderer: 'auto',
456
+ renderScale: 1,
457
+ powerPreference: 'high-performance',
458
+ fps: 60,
459
+ paused: false,
460
+ static: false,
441
461
  respectReducedMotion: true,
442
462
  mouseColor: true,
443
463
  textRatio: 39
@@ -447,13 +467,22 @@
447
467
  height: 104,
448
468
  min: 0,
449
469
  max: 100,
470
+ step: 'any',
450
471
  draggable: true,
472
+ keyboard: true,
451
473
  disabled: false,
452
474
  readonly: false,
475
+ direction: 'ltr',
476
+ precision: 0,
453
477
  valueSuffix: '%',
454
478
  edgeStyle: 'flow',
455
479
  quality: 'auto',
456
480
  renderer: 'auto',
481
+ renderScale: 1,
482
+ powerPreference: 'high-performance',
483
+ fps: 60,
484
+ paused: false,
485
+ static: false,
457
486
  textRatio: 54,
458
487
  showValue: true,
459
488
  respectReducedMotion: true
@@ -743,7 +772,7 @@
743
772
  alpha: false,
744
773
  antialias: false,
745
774
  depth: false,
746
- powerPreference: 'high-performance',
775
+ powerPreference: this.options.powerPreference || 'high-performance',
747
776
  preserveDrawingBuffer: false
748
777
  });
749
778
  if (!this.gl) throw new Error('WebGL2 is not available');
@@ -754,16 +783,19 @@
754
783
  this.motion = 0;
755
784
  this.motionTarget = 0;
756
785
  this.timeOffset = preset.seed * 0.73;
786
+ this.colors = preset.colors.map(hexToRgb01$1);
757
787
  this.visible = true;
758
788
  this.disposed = false;
759
789
  _assertClassBrand(_CosmicRenderer_brand, this, _setupGeometry).call(this);
760
790
  _assertClassBrand(_CosmicRenderer_brand, this, _bindEvents).call(this);
791
+ _assertClassBrand(_CosmicRenderer_brand, this, _bindContextEvents).call(this);
761
792
  this.resize();
762
793
  }
763
794
  return _createClass(CosmicRenderer, [{
764
795
  key: "setPreset",
765
796
  value: function setPreset(preset) {
766
797
  this.preset = _objectSpread2({}, preset);
798
+ this.colors = preset.colors.map(hexToRgb01$1);
767
799
  this.timeOffset = preset.seed * 0.73;
768
800
  }
769
801
  }, {
@@ -808,15 +840,14 @@
808
840
  if (this.canvas.width !== width || this.canvas.height !== height) {
809
841
  this.canvas.width = width;
810
842
  this.canvas.height = height;
811
- this.gl.viewport(0, 0, width, height);
812
843
  }
844
+ this.gl.viewport(0, 0, width, height);
813
845
  }
814
846
  }, {
815
847
  key: "draw",
816
848
  value: function draw(elapsedSeconds) {
817
849
  var paused = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
818
850
  if (this.disposed || !this.visible) return;
819
- this.resize();
820
851
  var gl = this.gl;
821
852
  this.pointer[0] += (this.pointerTarget[0] - this.pointer[0]) * 0.08;
822
853
  this.pointer[1] += (this.pointerTarget[1] - this.pointer[1]) * 0.08;
@@ -825,16 +856,15 @@
825
856
  gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
826
857
  gl.enableVertexAttribArray(this.locations.position);
827
858
  gl.vertexAttribPointer(this.locations.position, 2, gl.FLOAT, false, 0, 0);
828
- var colors = this.preset.colors.map(hexToRgb01$1);
829
859
  gl.uniform2f(this.locations.resolution, this.canvas.width, this.canvas.height);
830
860
  gl.uniform1f(this.locations.time, this.timeOffset + (paused ? 0 : elapsedSeconds * this.preset.speed));
831
861
  gl.uniform1f(this.locations.seed, this.preset.seed);
832
862
  gl.uniform1f(this.locations.motion, this.motion);
833
863
  gl.uniform2f(this.locations.pointer, this.pointer[0], this.pointer[1]);
834
- gl.uniform3fv(this.locations.colorA, colors[0]);
835
- gl.uniform3fv(this.locations.colorB, colors[1]);
836
- gl.uniform3fv(this.locations.colorC, colors[2]);
837
- gl.uniform3fv(this.locations.colorD, colors[3]);
864
+ gl.uniform3fv(this.locations.colorA, this.colors[0]);
865
+ gl.uniform3fv(this.locations.colorB, this.colors[1]);
866
+ gl.uniform3fv(this.locations.colorC, this.colors[2]);
867
+ gl.uniform3fv(this.locations.colorD, this.colors[3]);
838
868
  gl.drawArrays(gl.TRIANGLES, 0, 3);
839
869
  }
840
870
  }, {
@@ -845,6 +875,8 @@
845
875
  target.removeEventListener('pointermove', this.onPointerMove);
846
876
  target.removeEventListener('pointerdown', this.onPointerMove);
847
877
  target.removeEventListener('pointerleave', this.onPointerLeave);
878
+ this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
879
+ this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
848
880
  this.gl.deleteBuffer(this.buffer);
849
881
  this.gl.deleteProgram(this.program);
850
882
  var lose = this.gl.getExtension('WEBGL_lose_context');
@@ -901,6 +933,26 @@
901
933
  passive: true
902
934
  });
903
935
  }
936
+ function _bindContextEvents() {
937
+ var _this3 = this;
938
+ this.onContextLost = function (event) {
939
+ event.preventDefault();
940
+ if (_this3.disposed) return;
941
+ _this3.visible = false;
942
+ if (typeof _this3.options.onContextLost === 'function') _this3.options.onContextLost(event);
943
+ };
944
+ this.onContextRestored = function () {
945
+ if (_this3.disposed) return;
946
+ _this3.program = createProgram$1(_this3.gl);
947
+ _this3.locations = _assertClassBrand(_CosmicRenderer_brand, _this3, _getLocations).call(_this3);
948
+ _assertClassBrand(_CosmicRenderer_brand, _this3, _setupGeometry).call(_this3);
949
+ _this3.visible = true;
950
+ _this3.resize();
951
+ if (typeof _this3.options.onContextRestored === 'function') _this3.options.onContextRestored();
952
+ };
953
+ this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
954
+ this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
955
+ }
904
956
 
905
957
  function rgb(color) {
906
958
  var alpha = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
@@ -915,18 +967,20 @@
915
967
  }
916
968
  var FallbackRenderer = /*#__PURE__*/function () {
917
969
  function FallbackRenderer(canvas, preset) {
970
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
918
971
  _classCallCheck(this, FallbackRenderer);
919
972
  this.canvas = canvas;
920
973
  this.preset = preset;
921
974
  this.context = canvas.getContext('2d');
922
975
  this.visible = true;
923
976
  this.timePhase = 0;
977
+ this.dprCap = options.dprCap || 1.5;
924
978
  }
925
979
  return _createClass(FallbackRenderer, [{
926
980
  key: "resize",
927
981
  value: function resize() {
928
982
  var rect = this.canvas.getBoundingClientRect();
929
- var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
983
+ var dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
930
984
  var width = Math.max(2, Math.round(rect.width * dpr));
931
985
  var height = Math.max(2, Math.round(rect.height * dpr));
932
986
  if (this.canvas.width !== width || this.canvas.height !== height) {
@@ -967,7 +1021,6 @@
967
1021
  key: "draw",
968
1022
  value: function draw(time) {
969
1023
  if (!this.visible) return;
970
- this.resize();
971
1024
  this.drawNebula(time);
972
1025
  }
973
1026
  }, {
@@ -975,6 +1028,12 @@
975
1028
  value: function setPreset(preset) {
976
1029
  this.preset = preset;
977
1030
  }
1031
+ }, {
1032
+ key: "setDprCap",
1033
+ value: function setDprCap(cap) {
1034
+ this.dprCap = cap;
1035
+ this.resize();
1036
+ }
978
1037
  }, {
979
1038
  key: "randomize",
980
1039
  value: function randomize() {
@@ -992,10 +1051,10 @@
992
1051
  }]);
993
1052
  }();
994
1053
 
995
- /**
996
- * Document-level shared rAF scheduler. Every component instance subscribes
997
- * its own frame callback; the whole page runs ONE animation loop (like the
998
- * original demo), which avoids jank from many competing rAF loops.
1054
+ /**
1055
+ * Document-level shared rAF scheduler. Every component instance subscribes
1056
+ * its own frame callback; the whole page runs ONE animation loop (like the
1057
+ * original demo), which avoids jank from many competing rAF loops.
999
1058
  */
1000
1059
  var subscribers = [];
1001
1060
  var running = false;
@@ -1004,11 +1063,7 @@
1004
1063
  var last = 0;
1005
1064
  function tick(now) {
1006
1065
  if (!running) return;
1007
- var delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
1008
- last = now;
1009
- // Schedule the next frame BEFORE running callbacks so one throwing
1010
- // subscriber can never kill the whole animation loop.
1011
- rafId = requestAnimationFrame(tick);
1066
+ var activeItems = [];
1012
1067
  if (!globallyPaused) {
1013
1068
  var _iterator = _createForOfIteratorHelper(subscribers.slice()),
1014
1069
  _step;
@@ -1016,10 +1071,8 @@
1016
1071
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
1017
1072
  var item = _step.value;
1018
1073
  try {
1019
- if (!item.isPaused()) item.onFrame(delta, now);
1020
- } catch (error) {
1021
- console.warn('[dlc-ui] frame error:', error);
1022
- }
1074
+ if (!item.isPaused()) activeItems.push(item);
1075
+ } catch (_unused) {}
1023
1076
  }
1024
1077
  } catch (err) {
1025
1078
  _iterator.e(err);
@@ -1027,6 +1080,25 @@
1027
1080
  _iterator.f();
1028
1081
  }
1029
1082
  }
1083
+ if (activeItems.length === 0) {
1084
+ running = false;
1085
+ rafId = 0;
1086
+ last = 0;
1087
+ return;
1088
+ }
1089
+ var delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
1090
+ last = now;
1091
+ // Schedule the next frame BEFORE running callbacks so one throwing
1092
+ // subscriber can never kill the whole animation loop.
1093
+ rafId = requestAnimationFrame(tick);
1094
+ for (var _i = 0, _activeItems = activeItems; _i < _activeItems.length; _i++) {
1095
+ var _item = _activeItems[_i];
1096
+ try {
1097
+ _item.onFrame(delta, now);
1098
+ } catch (error) {
1099
+ console.warn('[dlc-ui] frame error:', error);
1100
+ }
1101
+ }
1030
1102
  if (subscribers.length === 0) {
1031
1103
  cancelAnimationFrame(rafId);
1032
1104
  running = false;
@@ -1044,6 +1116,10 @@
1044
1116
  }
1045
1117
  function resumeAll() {
1046
1118
  globallyPaused = false;
1119
+ start();
1120
+ }
1121
+ function wakeScheduler() {
1122
+ start();
1047
1123
  }
1048
1124
  function subscribeScheduler(onFrame, isPaused) {
1049
1125
  var item = {
@@ -1063,11 +1139,12 @@
1063
1139
  };
1064
1140
  }
1065
1141
 
1066
- /**
1067
- * Gates drawing on "element intersects viewport AND the page tab is visible".
1068
- * Falls back to always-visible when IntersectionObserver is unavailable.
1142
+ /**
1143
+ * Gates drawing on "element intersects viewport AND the page tab is visible".
1144
+ * Falls back to always-visible when IntersectionObserver is unavailable.
1069
1145
  */
1070
1146
  function createVisibilityGuard(element) {
1147
+ var onChange = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1071
1148
  var intersecting = true;
1072
1149
  var pageVisible = typeof document === 'undefined' || !document.hidden;
1073
1150
  var disposed = false;
@@ -1077,6 +1154,7 @@
1077
1154
  intersecting = entries.some(function (entry) {
1078
1155
  return entry.isIntersecting;
1079
1156
  });
1157
+ if (typeof onChange === 'function') onChange();
1080
1158
  }, {
1081
1159
  rootMargin: '180px'
1082
1160
  });
@@ -1084,6 +1162,7 @@
1084
1162
  }
1085
1163
  var onVisibilityChange = function onVisibilityChange() {
1086
1164
  pageVisible = typeof document !== 'undefined' && !document.hidden;
1165
+ if (typeof onChange === 'function') onChange();
1087
1166
  };
1088
1167
  if (typeof document !== 'undefined') {
1089
1168
  document.addEventListener('visibilitychange', onVisibilityChange);
@@ -1119,18 +1198,69 @@
1119
1198
  }
1120
1199
  }
1121
1200
 
1122
- function prefersReducedMotion$2() {
1123
- return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
1201
+ function normalizeFps(value) {
1202
+ var fallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 60;
1203
+ var fps = Number(value);
1204
+ if (!Number.isFinite(fps)) return fallback;
1205
+ return Math.min(60, Math.max(1, fps));
1206
+ }
1207
+ function createFrameGate() {
1208
+ var initialFps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 60;
1209
+ var fps = normalizeFps(initialFps);
1210
+ var elapsed = 0;
1211
+ return {
1212
+ shouldDraw: function shouldDraw(delta) {
1213
+ elapsed += delta;
1214
+ var interval = 1 / fps;
1215
+ if (elapsed + 0.0001 < interval) return false;
1216
+ elapsed %= interval;
1217
+ return true;
1218
+ },
1219
+ setFps: function setFps(value) {
1220
+ fps = normalizeFps(value, fps);
1221
+ elapsed = 0;
1222
+ return fps;
1223
+ },
1224
+ getFps: function getFps() {
1225
+ return fps;
1226
+ }
1227
+ };
1228
+ }
1229
+ function createReducedMotionPreference(enabled, onChange) {
1230
+ var query = enabled && typeof matchMedia !== 'undefined' ? matchMedia('(prefers-reduced-motion: reduce)') : null;
1231
+ var notify = function notify() {
1232
+ if (typeof onChange === 'function') onChange(Boolean(query && query.matches));
1233
+ };
1234
+ if (query) {
1235
+ if (typeof query.addEventListener === 'function') query.addEventListener('change', notify);else if (typeof query.addListener === 'function') query.addListener(notify);
1236
+ }
1237
+ return {
1238
+ matches: function matches() {
1239
+ return Boolean(query && query.matches);
1240
+ },
1241
+ dispose: function dispose() {
1242
+ if (!query) return;
1243
+ if (typeof query.removeEventListener === 'function') query.removeEventListener('change', notify);else if (typeof query.removeListener === 'function') query.removeListener(notify);
1244
+ }
1245
+ };
1246
+ }
1247
+
1248
+ function toFiniteNumber(value) {
1249
+ if (value == null || typeof value === 'boolean') return null;
1250
+ if (typeof value === 'string' && value.trim() === '') return null;
1251
+ var number = Number(value);
1252
+ return Number.isFinite(number) ? number : null;
1124
1253
  }
1125
1254
 
1126
- /**
1255
+ /**
1127
1256
  * Mount a cosmic (nebula) capsule into `container`.
1128
1257
  *
1129
1258
  * Options: preset (literary name), width, height (number=px or string with
1130
1259
  * px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
1131
1260
  * width in percent), text (HTML string or DOM nodes for the text slot),
1132
1261
  * colorContent (HTML string or DOM nodes for the color slot), quality,
1133
- * renderer, respectReducedMotion, mouseColor, cssVars.
1262
+ * renderer, quality, renderScale, powerPreference, fps, paused/static,
1263
+ * respectReducedMotion, mouseColor, cssVars.
1134
1264
  */
1135
1265
  function createCapsule(container) {
1136
1266
  var _options$preset;
@@ -1140,10 +1270,19 @@
1140
1270
  }
1141
1271
  var preset = _objectSpread2({}, getPreset('capsule', (_options$preset = options.preset) !== null && _options$preset !== void 0 ? _options$preset : '初光'));
1142
1272
  var merged = normalizeOptions(DEFAULTS.capsule, preset, options);
1143
- var normalizedColors = merged.colors.map(normalizeColor);
1144
- if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
1145
- preset.seed = merged.seed;
1146
- preset.speed = merged.speed;
1273
+ var normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
1274
+ if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
1275
+ var initialSeed = toFiniteNumber(merged.seed);
1276
+ var initialSpeed = toFiniteNumber(merged.speed);
1277
+ if (initialSeed !== null) preset.seed = initialSeed;
1278
+ if (initialSpeed !== null) preset.speed = initialSpeed;
1279
+ merged.colors = _toConsumableArray(preset.colors);
1280
+ merged.seed = preset.seed;
1281
+ merged.speed = preset.speed;
1282
+ var optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
1283
+ var colorOverride = optionColors.length === 4 && optionColors.every(Boolean) ? _toConsumableArray(optionColors) : null;
1284
+ var seedOverride = options.seed !== undefined ? toFiniteNumber(options.seed) : null;
1285
+ var speedOverride = options.speed !== undefined ? toFiniteNumber(options.speed) : null;
1147
1286
  var customLabel = typeof options.label === 'string' && options.label ? options.label : null;
1148
1287
  var root = document.createElement('div');
1149
1288
  root.className = 'hj-capsule-root hj-capsule-cosmic';
@@ -1195,25 +1334,49 @@
1195
1334
  root.appendChild(canvas);
1196
1335
  container.appendChild(root);
1197
1336
  var emitter = createEmitter();
1198
- var paused = merged.respectReducedMotion && prefersReducedMotion$2();
1337
+ var manuallyPaused = merged.paused === true;
1338
+ var reducedPaused = false;
1339
+ var staticMode = merged.static === true;
1340
+ var contextLost = false;
1199
1341
  var disposed = false;
1342
+ var renderOnce = function renderOnce() {};
1200
1343
  var dirty = {
1201
1344
  preset: false,
1202
1345
  seed: false,
1203
1346
  speed: false,
1204
1347
  colors: false,
1205
1348
  textRatio: false,
1206
- cssVars: false
1349
+ cssVars: false,
1350
+ mouseColor: false,
1351
+ quality: false,
1352
+ renderScale: false,
1353
+ paused: false,
1354
+ static: false,
1355
+ fps: false
1207
1356
  };
1208
1357
  var renderer;
1209
1358
  var useWebgl = merged.renderer !== 'canvas2d';
1210
1359
  if (useWebgl) {
1211
1360
  try {
1212
1361
  renderer = new CosmicRenderer(canvas, merged, {
1213
- dprCap: dprCapFor(merged.quality)
1362
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
1363
+ mouseColor: merged.mouseColor !== false,
1364
+ powerPreference: merged.powerPreference,
1365
+ onContextLost: function onContextLost() {
1366
+ contextLost = true;
1367
+ emitter.emit('contextlost', {});
1368
+ },
1369
+ onContextRestored: function onContextRestored() {
1370
+ contextLost = false;
1371
+ emitter.emit('contextrestored', {});
1372
+ renderOnce();
1373
+ wakeScheduler();
1374
+ }
1214
1375
  });
1215
1376
  } catch (error) {
1216
- renderer = new FallbackRenderer(canvas, merged);
1377
+ renderer = new FallbackRenderer(canvas, merged, {
1378
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale)
1379
+ });
1217
1380
  nextTick(function () {
1218
1381
  if (disposed) return;
1219
1382
  emitter.emit('error', {
@@ -1222,8 +1385,15 @@
1222
1385
  });
1223
1386
  }
1224
1387
  } else {
1225
- renderer = new FallbackRenderer(canvas, merged);
1388
+ renderer = new FallbackRenderer(canvas, merged, {
1389
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale)
1390
+ });
1226
1391
  }
1392
+ var animationTime = 0;
1393
+ var frameGate = createFrameGate(merged.fps);
1394
+ renderOnce = function renderOnce() {
1395
+ return renderer.draw(animationTime);
1396
+ };
1227
1397
  var applySize = function applySize() {
1228
1398
  root.style.width = parseSize(merged.width);
1229
1399
  root.style.height = parseSize(merged.height);
@@ -1241,11 +1411,21 @@
1241
1411
  renderer.resize();
1242
1412
  };
1243
1413
  applySize();
1244
- var resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(function () {
1245
- return renderer.resize();
1246
- }) : null;
1247
- if (resizeObserver) resizeObserver.observe(root);else window.addEventListener('resize', renderer.resize);
1248
- var visibility = createVisibilityGuard(root);
1414
+ var resize = function resize() {
1415
+ renderer.resize();
1416
+ if (manuallyPaused || reducedPaused || staticMode) renderOnce();
1417
+ };
1418
+ var resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
1419
+ if (resizeObserver) resizeObserver.observe(root);else window.addEventListener('resize', resize);
1420
+ var visibility = createVisibilityGuard(root, wakeScheduler);
1421
+ var motionPreference = createReducedMotionPreference(merged.respectReducedMotion, function (matches) {
1422
+ reducedPaused = matches;
1423
+ if (matches) renderOnce();else wakeScheduler();
1424
+ });
1425
+ reducedPaused = motionPreference.matches();
1426
+ var isMotionPaused = function isMotionPaused() {
1427
+ return manuallyPaused || reducedPaused || staticMode || contextLost;
1428
+ };
1249
1429
  root.addEventListener('pointerenter', function () {
1250
1430
  return emitter.emit('pointerenter', {
1251
1431
  preset: _objectSpread2({}, preset)
@@ -1280,12 +1460,12 @@
1280
1460
  preset: _objectSpread2({}, preset)
1281
1461
  });
1282
1462
  });
1283
- var animationTime = 0;
1463
+ renderOnce();
1284
1464
  var unsubscribe = subscribeScheduler(function (delta) {
1285
1465
  animationTime += delta;
1286
- if (visibility.isVisible()) renderer.draw(animationTime);
1466
+ if (frameGate.shouldDraw(delta)) renderer.draw(animationTime);
1287
1467
  }, function () {
1288
- return paused;
1468
+ return isMotionPaused() || !visibility.isVisible();
1289
1469
  });
1290
1470
  nextTick(function () {
1291
1471
  if (disposed) return;
@@ -1308,11 +1488,15 @@
1308
1488
  var next = getPreset('capsule', ref);
1309
1489
  dirty.preset = true;
1310
1490
  Object.assign(preset, next);
1491
+ if (colorOverride) preset.colors = _toConsumableArray(colorOverride);
1492
+ if (seedOverride !== null) preset.seed = seedOverride;
1493
+ if (speedOverride !== null) preset.speed = speedOverride;
1311
1494
  var nextColors = preset.colors.map(normalizeColor);
1312
1495
  if (nextColors.every(Boolean)) preset.colors = nextColors;
1313
1496
  renderer.setPreset(_objectSpread2({}, preset));
1314
1497
  syncTheme();
1315
1498
  renderer.resize();
1499
+ if (isMotionPaused()) renderOnce();
1316
1500
  emitter.emit('presetchange', {
1317
1501
  preset: _objectSpread2({}, next)
1318
1502
  });
@@ -1324,26 +1508,32 @@
1324
1508
  return !color;
1325
1509
  })) return this;
1326
1510
  dirty.colors = true;
1511
+ colorOverride = _toConsumableArray(next);
1327
1512
  preset.colors = next;
1328
1513
  renderer.setPreset(_objectSpread2(_objectSpread2({}, preset), {}, {
1329
1514
  colors: next
1330
1515
  }));
1516
+ if (isMotionPaused()) renderOnce();
1331
1517
  return this;
1332
1518
  },
1333
1519
  setSeed: function setSeed(seed) {
1334
- var value = Number(seed);
1335
- if (!Number.isFinite(value)) return this;
1520
+ var value = toFiniteNumber(seed);
1521
+ if (value === null) return this;
1336
1522
  dirty.seed = true;
1523
+ seedOverride = value;
1337
1524
  preset.seed = value;
1338
1525
  renderer.setPreset(_objectSpread2({}, preset));
1526
+ if (isMotionPaused()) renderOnce();
1339
1527
  return this;
1340
1528
  },
1341
1529
  setSpeed: function setSpeed(speed) {
1342
- var value = Number(speed);
1343
- if (!Number.isFinite(value)) return this;
1530
+ var value = toFiniteNumber(speed);
1531
+ if (value === null) return this;
1344
1532
  dirty.speed = true;
1533
+ speedOverride = value;
1345
1534
  preset.speed = value;
1346
1535
  renderer.setPreset(_objectSpread2({}, preset));
1536
+ if (isMotionPaused()) renderOnce();
1347
1537
  return this;
1348
1538
  },
1349
1539
  setText: function setText(content) {
@@ -1379,6 +1569,7 @@
1379
1569
  return this;
1380
1570
  },
1381
1571
  setMouseColor: function setMouseColor(value) {
1572
+ dirty.mouseColor = true;
1382
1573
  merged.mouseColor = value !== false;
1383
1574
  if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
1384
1575
  return this;
@@ -1393,6 +1584,7 @@
1393
1584
  merged.height = height;
1394
1585
  }
1395
1586
  applySize();
1587
+ if (isMotionPaused()) renderOnce();
1396
1588
  return this;
1397
1589
  },
1398
1590
  randomize: function randomize() {
@@ -1400,23 +1592,59 @@
1400
1592
  return this;
1401
1593
  },
1402
1594
  pause: function pause() {
1403
- paused = true;
1595
+ dirty.paused = true;
1596
+ manuallyPaused = true;
1597
+ renderOnce();
1404
1598
  return this;
1405
1599
  },
1406
1600
  resume: function resume() {
1407
- paused = false;
1601
+ dirty.paused = true;
1602
+ manuallyPaused = false;
1603
+ wakeScheduler();
1604
+ return this;
1605
+ },
1606
+ setPaused: function setPaused(value) {
1607
+ return value ? this.pause() : this.resume();
1608
+ },
1609
+ setStatic: function setStatic(value) {
1610
+ dirty.static = true;
1611
+ staticMode = value === true;
1612
+ merged.static = staticMode;
1613
+ if (staticMode) renderOnce();else wakeScheduler();
1614
+ return this;
1615
+ },
1616
+ setFps: function setFps(fps) {
1617
+ dirty.fps = true;
1618
+ merged.fps = frameGate.setFps(fps);
1619
+ wakeScheduler();
1408
1620
  return this;
1409
1621
  },
1410
1622
  setQuality: function setQuality(quality) {
1623
+ dirty.quality = true;
1411
1624
  merged.quality = quality;
1412
- if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
1625
+ if (typeof renderer.setDprCap === 'function') {
1626
+ renderer.setDprCap(effectiveDprCap(quality, merged.renderScale));
1627
+ }
1628
+ if (isMotionPaused()) renderOnce();
1629
+ return this;
1630
+ },
1631
+ setRenderScale: function setRenderScale(renderScale) {
1632
+ var value = Number(renderScale);
1633
+ if (!Number.isFinite(value)) return this;
1634
+ dirty.renderScale = true;
1635
+ merged.renderScale = Math.min(1, Math.max(0.25, value));
1636
+ if (typeof renderer.setDprCap === 'function') {
1637
+ renderer.setDprCap(effectiveDprCap(merged.quality, merged.renderScale));
1638
+ }
1639
+ if (isMotionPaused()) renderOnce();
1413
1640
  return this;
1414
1641
  },
1415
1642
  dispose: function dispose() {
1416
1643
  disposed = true;
1417
1644
  unsubscribe();
1418
1645
  visibility.dispose();
1419
- if (resizeObserver) resizeObserver.disconnect();else window.removeEventListener('resize', renderer.resize);
1646
+ motionPreference.dispose();
1647
+ if (resizeObserver) resizeObserver.disconnect();else window.removeEventListener('resize', resize);
1420
1648
  renderer.dispose();
1421
1649
  root.remove();
1422
1650
  },
@@ -1426,6 +1654,24 @@
1426
1654
  get cssVars() {
1427
1655
  return merged.cssVars;
1428
1656
  },
1657
+ get mouseColor() {
1658
+ return merged.mouseColor;
1659
+ },
1660
+ get quality() {
1661
+ return merged.quality;
1662
+ },
1663
+ get renderScale() {
1664
+ return merged.renderScale;
1665
+ },
1666
+ get paused() {
1667
+ return manuallyPaused;
1668
+ },
1669
+ get static() {
1670
+ return staticMode;
1671
+ },
1672
+ get fps() {
1673
+ return frameGate.getFps();
1674
+ },
1429
1675
  dirty: dirty
1430
1676
  };
1431
1677
  }
@@ -1456,6 +1702,13 @@
1456
1702
  detail: 0.06,
1457
1703
  lobe: 0.23
1458
1704
  },
1705
+ 'botanic-lab': {
1706
+ seed: 3.46,
1707
+ broad: 0.54,
1708
+ middle: 0.18,
1709
+ detail: 0.05,
1710
+ lobe: 0.28
1711
+ },
1459
1712
  // ponytail: first-pass tide = asymmetric time warp on the same motion pipeline.
1460
1713
  // Refine (foam line / wash streaks) in the shader after visual QA.
1461
1714
  'tide': {
@@ -1566,64 +1819,95 @@
1566
1819
  var ProgressFlowRenderer = /*#__PURE__*/function () {
1567
1820
  function ProgressFlowRenderer(canvas, preset) {
1568
1821
  var _PROFILE_INDEX$preset;
1822
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1569
1823
  _classCallCheck(this, ProgressFlowRenderer);
1570
1824
  var gl = canvas.getContext('webgl2', {
1571
1825
  alpha: false,
1572
1826
  antialias: true,
1573
1827
  premultipliedAlpha: false,
1574
- powerPreference: 'high-performance'
1828
+ powerPreference: options.powerPreference || 'high-performance'
1575
1829
  });
1576
1830
  if (!gl) throw new Error('WebGL2 unavailable');
1577
1831
  this.canvas = canvas;
1832
+ this.options = options;
1578
1833
  this.gl = gl;
1579
- this.program = createProgram(gl);
1580
1834
  this.profile = preset.edgeStyle === 'tide' ? 3 : (_PROFILE_INDEX$preset = PROFILE_INDEX[preset.id]) !== null && _PROFILE_INDEX$preset !== void 0 ? _PROFILE_INDEX$preset : 2;
1581
1835
  this.seed = stringSeed$1("".concat(preset.id, "-shader")) * 13.7 + 1.0;
1582
1836
  this.colors = preset.colors.map(hexToRgb01);
1583
1837
  this.motionData = getProgressMotionData(preset.id, preset.edgeStyle);
1584
1838
  var motionFactor = preset.edgeStyle === 'tide' ? MOTION_SCALE_FACTORS.tide : MOTION_SCALE_FACTORS[preset.id] || 1.04;
1585
1839
  this.motionScale = PROGRESS_MOTION_MAX_PX * motionFactor / 1257;
1586
- this.position = gl.getAttribLocation(this.program, 'a_position');
1587
- this.uniforms = {
1588
- resolution: gl.getUniformLocation(this.program, 'u_resolution'),
1589
- time: gl.getUniformLocation(this.program, 'u_time'),
1590
- progress: gl.getUniformLocation(this.program, 'u_progress'),
1591
- seed: gl.getUniformLocation(this.program, 'u_seed'),
1592
- profile: gl.getUniformLocation(this.program, 'u_profile'),
1593
- motion: gl.getUniformLocation(this.program, 'u_motion'),
1594
- effect: gl.getUniformLocation(this.program, 'u_effect'),
1595
- hasEffect: gl.getUniformLocation(this.program, 'u_hasEffect'),
1596
- effectFrames: gl.getUniformLocation(this.program, 'u_effectFrames'),
1597
- motionDuration: gl.getUniformLocation(this.program, 'u_motionDuration'),
1598
- motionScale: gl.getUniformLocation(this.program, 'u_motionScale'),
1599
- dark: gl.getUniformLocation(this.program, 'u_dark'),
1600
- accentA: gl.getUniformLocation(this.program, 'u_accentA'),
1601
- accentB: gl.getUniformLocation(this.program, 'u_accentB'),
1602
- glow: gl.getUniformLocation(this.program, 'u_glow')
1603
- };
1604
- this.buffer = gl.createBuffer();
1605
- gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
1606
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
1607
- this.motionTexture = gl.createTexture();
1608
- gl.activeTexture(gl.TEXTURE0);
1609
- gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
1610
- gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
1611
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, PROGRESS_MOTION_WIDTH, PROGRESS_MOTION_HEIGHT, 0, gl.RED, gl.UNSIGNED_BYTE, this.motionData);
1612
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1613
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1614
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
1615
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1616
- this.effectTexture = gl.createTexture();
1617
- gl.activeTexture(gl.TEXTURE1);
1618
- gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
1619
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([32, 33, 38]));
1620
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1621
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1622
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1623
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1624
- this.effectUploaded = false;
1840
+ this.disposed = false;
1841
+ this.contextLost = false;
1842
+ this.setupResources();
1843
+ this.bindContextEvents();
1625
1844
  }
1626
1845
  return _createClass(ProgressFlowRenderer, [{
1846
+ key: "setupResources",
1847
+ value: function setupResources() {
1848
+ var gl = this.gl;
1849
+ this.program = createProgram(gl);
1850
+ this.position = gl.getAttribLocation(this.program, 'a_position');
1851
+ this.uniforms = {
1852
+ resolution: gl.getUniformLocation(this.program, 'u_resolution'),
1853
+ time: gl.getUniformLocation(this.program, 'u_time'),
1854
+ progress: gl.getUniformLocation(this.program, 'u_progress'),
1855
+ seed: gl.getUniformLocation(this.program, 'u_seed'),
1856
+ profile: gl.getUniformLocation(this.program, 'u_profile'),
1857
+ motion: gl.getUniformLocation(this.program, 'u_motion'),
1858
+ effect: gl.getUniformLocation(this.program, 'u_effect'),
1859
+ hasEffect: gl.getUniformLocation(this.program, 'u_hasEffect'),
1860
+ effectFrames: gl.getUniformLocation(this.program, 'u_effectFrames'),
1861
+ motionDuration: gl.getUniformLocation(this.program, 'u_motionDuration'),
1862
+ motionScale: gl.getUniformLocation(this.program, 'u_motionScale'),
1863
+ dark: gl.getUniformLocation(this.program, 'u_dark'),
1864
+ accentA: gl.getUniformLocation(this.program, 'u_accentA'),
1865
+ accentB: gl.getUniformLocation(this.program, 'u_accentB'),
1866
+ glow: gl.getUniformLocation(this.program, 'u_glow')
1867
+ };
1868
+ this.buffer = gl.createBuffer();
1869
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
1870
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
1871
+ this.motionTexture = gl.createTexture();
1872
+ gl.activeTexture(gl.TEXTURE0);
1873
+ gl.bindTexture(gl.TEXTURE_2D, this.motionTexture);
1874
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
1875
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, PROGRESS_MOTION_WIDTH, PROGRESS_MOTION_HEIGHT, 0, gl.RED, gl.UNSIGNED_BYTE, this.motionData);
1876
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1877
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1878
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
1879
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1880
+ this.effectTexture = gl.createTexture();
1881
+ gl.activeTexture(gl.TEXTURE1);
1882
+ gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
1883
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([32, 33, 38]));
1884
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1885
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1886
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1887
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1888
+ this.effectUploaded = false;
1889
+ }
1890
+ }, {
1891
+ key: "bindContextEvents",
1892
+ value: function bindContextEvents() {
1893
+ var _this = this;
1894
+ this.onContextLost = function (event) {
1895
+ event.preventDefault();
1896
+ if (_this.disposed) return;
1897
+ _this.contextLost = true;
1898
+ if (typeof _this.options.onContextLost === 'function') _this.options.onContextLost(event);
1899
+ };
1900
+ this.onContextRestored = function () {
1901
+ if (_this.disposed) return;
1902
+ _this.setupResources();
1903
+ _this.contextLost = false;
1904
+ _this.gl.viewport(0, 0, _this.canvas.width, _this.canvas.height);
1905
+ if (typeof _this.options.onContextRestored === 'function') _this.options.onContextRestored();
1906
+ };
1907
+ this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
1908
+ this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
1909
+ }
1910
+ }, {
1627
1911
  key: "resize",
1628
1912
  value: function resize(width, height, dpr) {
1629
1913
  var pixelWidth = Math.max(1, Math.round(width * dpr));
@@ -1638,6 +1922,7 @@
1638
1922
  key: "draw",
1639
1923
  value: function draw(time, progress) {
1640
1924
  var effectImage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
1925
+ if (this.disposed || this.contextLost) return;
1641
1926
  var gl = this.gl;
1642
1927
  gl.useProgram(this.program);
1643
1928
  gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
@@ -1654,7 +1939,8 @@
1654
1939
  gl.uniform1f(this.uniforms.motionDuration, PROGRESS_MOTION_DURATION);
1655
1940
  gl.uniform1f(this.uniforms.motionScale, this.motionScale);
1656
1941
  var hasEffect = this.effectUploaded ? 1 : 0;
1657
- if (!this.effectUploaded && effectImage && effectImage.complete && effectImage.naturalWidth > 0) {
1942
+ var effectWidth = effectImage && (effectImage.naturalWidth || effectImage.width || 0);
1943
+ if (!this.effectUploaded && effectImage && effectWidth > 0) {
1658
1944
  gl.activeTexture(gl.TEXTURE1);
1659
1945
  gl.bindTexture(gl.TEXTURE_2D, this.effectTexture);
1660
1946
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
@@ -1686,7 +1972,10 @@
1686
1972
  }, {
1687
1973
  key: "dispose",
1688
1974
  value: function dispose() {
1975
+ this.disposed = true;
1689
1976
  var gl = this.gl;
1977
+ this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
1978
+ this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
1690
1979
  gl.deleteBuffer(this.buffer);
1691
1980
  gl.deleteTexture(this.motionTexture);
1692
1981
  gl.deleteTexture(this.effectTexture);
@@ -1697,8 +1986,9 @@
1697
1986
  }]);
1698
1987
  }();
1699
1988
  function createProgressFlowRenderer(canvas, preset) {
1989
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1700
1990
  try {
1701
- return new ProgressFlowRenderer(canvas, preset);
1991
+ return new ProgressFlowRenderer(canvas, preset, options);
1702
1992
  } catch (error) {
1703
1993
  console.warn('[画境观屿] 进度流体 WebGL2 不可用,使用 Canvas 2D 降级。', error);
1704
1994
  return null;
@@ -1714,7 +2004,8 @@
1714
2004
  'model-training': ['#20131f', '#ff3f94', '#ff8a3d', '#fff06a'],
1715
2005
  'agent-migration': ['#111a31', '#245bff', '#00cfff', '#5dffe6'],
1716
2006
  'visual-training': ['#1f172b', '#7042ff', '#42f58d', '#c4ff8a'],
1717
- 'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff']
2007
+ 'tide': ['#0a2239', '#2e9bff', '#7fe3ff', '#eaf9ff'],
2008
+ 'botanic-lab': ['#102a23', '#287a58', '#9bd65b', '#f2ffc4']
1718
2009
  };
1719
2010
  function drawCloud(context, x, y, radiusX, radiusY, color, alpha) {
1720
2011
  context.save();
@@ -1758,50 +2049,49 @@
1758
2049
  context.fillRect(20, 0, 44, FRAME_HEIGHT);
1759
2050
  context.restore();
1760
2051
  }
1761
- var image = new Image();
1762
- image.decoding = 'async';
1763
- var state = {
1764
- image: image,
1765
- ready: false
2052
+
2053
+ // A canvas is a valid TexImageSource. Keeping it directly avoids a PNG
2054
+ // encode -> base64 allocation -> Image decode round trip at startup.
2055
+ return {
2056
+ image: canvas,
2057
+ ready: true
1766
2058
  };
1767
- image.addEventListener('load', function () {
1768
- state.ready = true;
1769
- }, {
1770
- once: true
1771
- });
1772
- image.addEventListener('error', function () {
1773
- state.ready = false;
1774
- }, {
1775
- once: true
1776
- });
1777
- image.src = canvas.toDataURL('image/png');
1778
- return state;
1779
2059
  }
1780
2060
  function getProgressReferenceAtlas(id) {
1781
2061
  if (!CACHE[id]) CACHE[id] = createAtlas(id);
1782
2062
  return CACHE[id];
1783
2063
  }
1784
2064
 
1785
- /**
1786
- * Attach a WebGL2 fluid overlay to a progress capsule root.
1787
- *
1788
- * @param {object} params
1789
- * @param {HTMLElement} params.root progress capsule root element
1790
- * @param {HTMLCanvasElement} params.canvas 2D fallback canvas (kept beneath)
1791
- * @param {object} params.preset progress preset
1792
- * @param {() => number} params.getProgress reads the current progress value
1793
- * @returns {{ update(flowTime: number): void, dispose(): void } | null}
2065
+ /**
2066
+ * Attach a WebGL2 fluid overlay to a progress capsule root.
2067
+ *
2068
+ * @param {object} params
2069
+ * @param {HTMLElement} params.root progress capsule root element
2070
+ * @param {HTMLCanvasElement} params.canvas 2D fallback canvas (kept beneath)
2071
+ * @param {object} params.preset progress preset
2072
+ * @param {() => number} params.getProgress reads the current progress value
2073
+ * @returns {{ update(flowTime: number): void, setDprCap(cap: number): void, dispose(): void } | null}
1794
2074
  */
1795
2075
  function attachProgressFlowOverlay(_ref) {
1796
2076
  var root = _ref.root,
1797
2077
  canvas = _ref.canvas,
1798
2078
  preset = _ref.preset,
1799
- getProgress = _ref.getProgress;
2079
+ getProgress = _ref.getProgress,
2080
+ _ref$dprCap = _ref.dprCap,
2081
+ dprCap = _ref$dprCap === void 0 ? 2 : _ref$dprCap,
2082
+ _ref$powerPreference = _ref.powerPreference,
2083
+ powerPreference = _ref$powerPreference === void 0 ? 'high-performance' : _ref$powerPreference,
2084
+ onContextLost = _ref.onContextLost,
2085
+ onContextRestored = _ref.onContextRestored;
1800
2086
  var overlay = document.createElement('canvas');
1801
2087
  overlay.className = 'hj-progress-canvas hj-progress-overlay';
1802
2088
  overlay.setAttribute('aria-hidden', 'true');
1803
2089
  canvas.insertAdjacentElement('afterend', overlay);
1804
- var renderer = createProgressFlowRenderer(overlay, preset);
2090
+ var renderer = createProgressFlowRenderer(overlay, preset, {
2091
+ powerPreference: powerPreference,
2092
+ onContextLost: onContextLost,
2093
+ onContextRestored: onContextRestored
2094
+ });
1805
2095
  if (!renderer) {
1806
2096
  overlay.remove();
1807
2097
  return null;
@@ -1812,7 +2102,7 @@
1812
2102
  var bounds = root.getBoundingClientRect();
1813
2103
  var width = Math.max(1, bounds.width);
1814
2104
  var height = Math.max(1, bounds.height);
1815
- var dpr = Math.min(window.devicePixelRatio || 1, 2);
2105
+ var dpr = Math.min(window.devicePixelRatio || 1, dprCap);
1816
2106
  overlay.style.width = "".concat(width, "px");
1817
2107
  overlay.style.height = "".concat(height, "px");
1818
2108
  renderer.resize(width, height, dpr);
@@ -1836,6 +2126,10 @@
1836
2126
  setColors: function setColors(colors) {
1837
2127
  renderer.setColors(colors);
1838
2128
  },
2129
+ setDprCap: function setDprCap(cap) {
2130
+ dprCap = cap;
2131
+ resize();
2132
+ },
1839
2133
  dispose: function dispose() {
1840
2134
  if (resizeObserver) resizeObserver.disconnect();
1841
2135
  if (onWindowResize) window.removeEventListener('resize', onWindowResize);
@@ -1846,6 +2140,47 @@
1846
2140
  };
1847
2141
  }
1848
2142
 
2143
+ function normalizeRange(min, max) {
2144
+ var fallbackMin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
2145
+ var fallbackMax = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 100;
2146
+ var nextMin = toFiniteNumber(min);
2147
+ var nextMax = toFiniteNumber(max);
2148
+ if (nextMin === null) nextMin = fallbackMin;
2149
+ if (nextMax === null) nextMax = fallbackMax;
2150
+ if (nextMin > nextMax) {
2151
+ var swap = nextMin;
2152
+ nextMin = nextMax;
2153
+ nextMax = swap;
2154
+ }
2155
+ return [nextMin, nextMax];
2156
+ }
2157
+ function clampProgressValue(value, min, max) {
2158
+ var number = toFiniteNumber(value);
2159
+ if (number === null) return null;
2160
+ return Math.min(Math.max(number, min), max);
2161
+ }
2162
+ function normalizeProgressStep(step) {
2163
+ var fallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'any';
2164
+ if (step === 'any' || step == null) return 'any';
2165
+ var increment = toFiniteNumber(step);
2166
+ return increment !== null && increment > 0 ? increment : fallback;
2167
+ }
2168
+ function snapProgressValue(value, min, max) {
2169
+ var step = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'any';
2170
+ var clamped = clampProgressValue(value, min, max);
2171
+ if (clamped === null) return null;
2172
+ if (step === 'any' || step == null) return clamped;
2173
+ var increment = normalizeProgressStep(step);
2174
+ if (increment === 'any') return clamped;
2175
+ var snapped = min + Math.round((clamped - min) / increment) * increment;
2176
+ return Math.min(Math.max(Number(snapped.toFixed(12)), min), max);
2177
+ }
2178
+ function progressRatio(value, min, max) {
2179
+ var range = max - min;
2180
+ if (!Number.isFinite(range) || range <= 0) return 0;
2181
+ return Math.min(Math.max((value - min) / range, 0), 1);
2182
+ }
2183
+
1849
2184
  var clamp = function clamp(value, min, max) {
1850
2185
  return Math.min(Math.max(value, min), max);
1851
2186
  };
@@ -1872,9 +2207,6 @@
1872
2207
  return values[key] !== undefined ? values[key] : '';
1873
2208
  });
1874
2209
  }
1875
- function prefersReducedMotion$1() {
1876
- return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
1877
- }
1878
2210
  var FLOW_PROFILES = {
1879
2211
  'model-training': {
1880
2212
  cycles: [0.98, 3.05, 5.45],
@@ -1935,8 +2267,6 @@
1935
2267
  var ProgressCapsuleController = /*#__PURE__*/function () {
1936
2268
  function ProgressCapsuleController(_ref) {
1937
2269
  var _options$valueSuffix,
1938
- _options$min,
1939
- _options$max,
1940
2270
  _options$value,
1941
2271
  _this = this;
1942
2272
  var root = _ref.root,
@@ -1958,11 +2288,18 @@
1958
2288
  this.dirty = dirty;
1959
2289
  this.valueSuffix = (_options$valueSuffix = options.valueSuffix) !== null && _options$valueSuffix !== void 0 ? _options$valueSuffix : copy.valueSuffix;
1960
2290
  this.profile = preset.edgeStyle === 'tide' ? FLOW_PROFILES.tide : FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training'];
1961
- this.min = (_options$min = options.min) !== null && _options$min !== void 0 ? _options$min : 0;
1962
- this.max = (_options$max = options.max) !== null && _options$max !== void 0 ? _options$max : 100;
1963
- this.dprCap = dprCapFor(options.quality);
2291
+ var _normalizeRange = normalizeRange(options.min, options.max);
2292
+ var _normalizeRange2 = _slicedToArray(_normalizeRange, 2);
2293
+ this.min = _normalizeRange2[0];
2294
+ this.max = _normalizeRange2[1];
2295
+ this.step = normalizeProgressStep(options.step);
2296
+ this.precision = Number.isInteger(Number(options.precision)) ? clamp(Number(options.precision), 0, 20) : 0;
2297
+ this.formatValue = typeof options.formatValue === 'function' ? options.formatValue : null;
2298
+ this.direction = options.direction === 'rtl' ? 'rtl' : 'ltr';
2299
+ this.dprCap = effectiveDprCap(options.quality, options.renderScale);
1964
2300
  this.ctx = canvas.getContext('2d');
1965
- this.value = clamp((_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : preset.initialProgress, this.min, this.max);
2301
+ this.value = snapProgressValue((_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : preset.initialProgress, this.min, this.max, this.step);
2302
+ if (this.value === null) this.value = clamp(preset.initialProgress, this.min, this.max);
1966
2303
  this.dragging = false;
1967
2304
  this.webglActive = false;
1968
2305
  this.flowTime = stringSeed(preset.id) * 31;
@@ -1972,10 +2309,12 @@
1972
2309
  this.width = 0;
1973
2310
  this.height = 0;
1974
2311
  this.handlers = {};
1975
- this.resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(function () {
1976
- return _this.resizeCanvas();
1977
- }) : null;
1978
- if (this.resizeObserver) this.resizeObserver.observe(this.root);else window.addEventListener('resize', this.resizeCanvas);
2312
+ this.onResize = function () {
2313
+ _this.resizeCanvas();
2314
+ if (typeof _this.options.onResize === 'function') _this.options.onResize();
2315
+ };
2316
+ this.resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(this.onResize) : null;
2317
+ if (this.resizeObserver) this.resizeObserver.observe(this.root);else window.addEventListener('resize', this.onResize);
1979
2318
  this.suppressEvents = true;
1980
2319
  this.setProgress(this.value, 'init');
1981
2320
  this.suppressEvents = false;
@@ -1988,16 +2327,37 @@
1988
2327
  this.randomState = Math.imul(this.randomState, 1664525) + 1013904223 >>> 0;
1989
2328
  return this.randomState / 4294967296;
1990
2329
  }
2330
+ }, {
2331
+ key: "displayText",
2332
+ value: function displayText() {
2333
+ if (this.formatValue) {
2334
+ return String(this.formatValue(this.value, {
2335
+ min: this.min,
2336
+ max: this.max,
2337
+ suffix: this.valueSuffix
2338
+ }));
2339
+ }
2340
+ var number = this.precision > 0 ? this.value.toFixed(this.precision) : String(Math.round(this.value));
2341
+ return "".concat(number).concat(this.valueSuffix);
2342
+ }
2343
+ }, {
2344
+ key: "syncValueDom",
2345
+ value: function syncValueDom() {
2346
+ var text = this.displayText();
2347
+ this.root.style.setProperty('--progress', this.value.toFixed(2));
2348
+ this.root.style.setProperty('--progress-ratio', progressRatio(this.value, this.min, this.max).toFixed(4));
2349
+ this.root.setAttribute('aria-valuenow', String(this.value));
2350
+ this.root.setAttribute('aria-valuetext', text);
2351
+ if (this.valueElement) this.valueElement.textContent = text;
2352
+ }
1991
2353
  }, {
1992
2354
  key: "setProgress",
1993
2355
  value: function setProgress(nextValue) {
1994
2356
  var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'auto';
1995
- this.value = clamp(nextValue, this.min, this.max);
1996
- var rounded = Math.round(this.value);
1997
- this.root.style.setProperty('--progress', this.value.toFixed(2));
1998
- this.root.setAttribute('aria-valuenow', String(rounded));
1999
- this.root.setAttribute('aria-valuetext', "".concat(rounded).concat(this.valueSuffix));
2000
- if (this.valueElement) this.valueElement.textContent = "".concat(rounded).concat(this.valueSuffix);
2357
+ var next = snapProgressValue(nextValue, this.min, this.max, this.step);
2358
+ if (next === null) return false;
2359
+ this.value = next;
2360
+ this.syncValueDom();
2001
2361
  if (!this.suppressEvents) {
2002
2362
  if (this.dirty) this.dirty.value = true;
2003
2363
  this.emitter.emit('change', {
@@ -2005,19 +2365,58 @@
2005
2365
  source: source
2006
2366
  });
2007
2367
  }
2368
+ return true;
2008
2369
  }
2009
2370
  }, {
2010
2371
  key: "setValueSuffix",
2011
2372
  value: function setValueSuffix(suffix) {
2373
+ if (this.dirty) this.dirty.valueSuffix = true;
2012
2374
  this.valueSuffix = String(suffix == null ? '' : suffix);
2013
- var rounded = Math.round(this.value);
2014
- if (this.valueElement) this.valueElement.textContent = "".concat(rounded).concat(this.valueSuffix);
2015
- this.root.setAttribute('aria-valuetext', "".concat(rounded).concat(this.valueSuffix));
2375
+ this.syncValueDom();
2376
+ return this;
2377
+ }
2378
+ }, {
2379
+ key: "setStep",
2380
+ value: function setStep(step) {
2381
+ var nextStep = normalizeProgressStep(step, this.step);
2382
+ if (nextStep === this.step) return this;
2383
+ if (this.dirty) this.dirty.step = true;
2384
+ this.step = nextStep;
2385
+ var next = snapProgressValue(this.value, this.min, this.max, this.step);
2386
+ if (next !== null && next !== this.value) this.setProgress(next, 'prop');
2387
+ return this;
2388
+ }
2389
+ }, {
2390
+ key: "setPrecision",
2391
+ value: function setPrecision(precision) {
2392
+ var value = Number(precision);
2393
+ if (!Number.isInteger(value) || value < 0 || value > 20) return this;
2394
+ if (this.dirty) this.dirty.precision = true;
2395
+ this.precision = value;
2396
+ this.syncValueDom();
2397
+ return this;
2398
+ }
2399
+ }, {
2400
+ key: "setFormatValue",
2401
+ value: function setFormatValue(formatter) {
2402
+ if (formatter != null && typeof formatter !== 'function') return this;
2403
+ if (this.dirty) this.dirty.formatValue = true;
2404
+ this.formatValue = formatter || null;
2405
+ this.syncValueDom();
2406
+ return this;
2407
+ }
2408
+ }, {
2409
+ key: "setDirection",
2410
+ value: function setDirection(direction) {
2411
+ if (this.dirty) this.dirty.direction = true;
2412
+ this.direction = direction === 'rtl' ? 'rtl' : 'ltr';
2413
+ this.root.dataset.direction = this.direction;
2016
2414
  return this;
2017
2415
  }
2018
2416
  }, {
2019
2417
  key: "setShowValue",
2020
2418
  value: function setShowValue(show) {
2419
+ if (this.dirty) this.dirty.showValue = true;
2021
2420
  if (this.valueElement) this.valueElement.hidden = show === false;
2022
2421
  return this;
2023
2422
  }
@@ -2182,15 +2581,14 @@
2182
2581
  }, {
2183
2582
  key: "setRange",
2184
2583
  value: function setRange(min, max) {
2185
- if (min > max) {
2186
- var swap = min;
2187
- min = max;
2188
- max = swap;
2189
- }
2190
- this.min = min;
2191
- this.max = max;
2192
- var clamped = clamp(this.value, this.min, this.max);
2193
- if (clamped !== this.value) this.setProgress(clamped, 'prop');
2584
+ var nextRange = normalizeRange(min, max, this.min, this.max);
2585
+ this.min = nextRange[0];
2586
+ this.max = nextRange[1];
2587
+ this.root.setAttribute('aria-valuemin', String(this.min));
2588
+ this.root.setAttribute('aria-valuemax', String(this.max));
2589
+ var next = snapProgressValue(this.value, this.min, this.max, this.step);
2590
+ if (next !== this.value) this.setProgress(next, 'prop');else this.syncValueDom();
2591
+ return this;
2194
2592
  }
2195
2593
  }, {
2196
2594
  key: "drawReferenceFlow",
@@ -2315,8 +2713,39 @@
2315
2713
  value: function updateFromPointer(event) {
2316
2714
  var bounds = this.root.getBoundingClientRect();
2317
2715
  var ratio = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
2716
+ if (this.direction === 'rtl') ratio = 1 - ratio;
2318
2717
  this.setProgress(this.min + ratio * (this.max - this.min), 'drag');
2319
2718
  }
2719
+ }, {
2720
+ key: "onKeyDown",
2721
+ value: function onKeyDown(event) {
2722
+ var range = this.max - this.min;
2723
+ var configuredStep = Number(this.step);
2724
+ var step = Number.isFinite(configuredStep) && configuredStep > 0 ? configuredStep : Math.max(range / 100, 1e-7);
2725
+ var direction = this.direction === 'rtl' ? -1 : 1;
2726
+ var aliases = {
2727
+ Left: 'ArrowLeft',
2728
+ Right: 'ArrowRight',
2729
+ Up: 'ArrowUp',
2730
+ Down: 'ArrowDown'
2731
+ };
2732
+ var keyCodes = {
2733
+ 35: 'End',
2734
+ 36: 'Home',
2735
+ 33: 'PageUp',
2736
+ 34: 'PageDown',
2737
+ 37: 'ArrowLeft',
2738
+ 38: 'ArrowUp',
2739
+ 39: 'ArrowRight',
2740
+ 40: 'ArrowDown'
2741
+ };
2742
+ var key = aliases[event.key] || event.key || keyCodes[event.keyCode];
2743
+ var next = null;
2744
+ if (key === 'ArrowRight') next = this.value + step * direction;else if (key === 'ArrowLeft') next = this.value - step * direction;else if (key === 'ArrowUp') next = this.value + step;else if (key === 'ArrowDown') next = this.value - step;else if (key === 'PageUp') next = this.value + step * 10;else if (key === 'PageDown') next = this.value - step * 10;else if (key === 'Home') next = this.min;else if (key === 'End') next = this.max;
2745
+ if (next === null) return;
2746
+ event.preventDefault();
2747
+ this.setProgress(next, 'keyboard');
2748
+ }
2320
2749
  }, {
2321
2750
  key: "beginDrag",
2322
2751
  value: function beginDrag(event) {
@@ -2404,6 +2833,12 @@
2404
2833
  this.root.addEventListener('pointerup', this.handlers.pointerup);
2405
2834
  this.root.addEventListener('pointercancel', this.handlers.pointercancel);
2406
2835
  }
2836
+ if (this.options.keyboard !== false) {
2837
+ this.handlers.keydown = function (event) {
2838
+ return _this3.onKeyDown(event);
2839
+ };
2840
+ this.root.addEventListener('keydown', this.handlers.keydown);
2841
+ }
2407
2842
  }
2408
2843
  }, {
2409
2844
  key: "update",
@@ -2422,20 +2857,19 @@
2422
2857
  key: "randomize",
2423
2858
  value: function randomize() {
2424
2859
  this.flowTime = this.random() * 40;
2860
+ return this.flowTime;
2425
2861
  }
2426
2862
  }, {
2427
2863
  key: "setColors",
2428
2864
  value: function setColors(colors) {
2429
2865
  if (!Array.isArray(colors) || colors.length !== 4) return;
2430
- this.preset = _objectSpread2(_objectSpread2({}, this.preset), {}, {
2431
- colors: colors
2432
- });
2866
+ this.preset.colors = _toConsumableArray(colors);
2433
2867
  }
2434
2868
  }, {
2435
2869
  key: "dispose",
2436
2870
  value: function dispose() {
2437
2871
  if (this._dragRaf) cancelAnimationFrame(this._dragRaf);
2438
- if (this.resizeObserver) this.resizeObserver.disconnect();else window.removeEventListener('resize', this.resizeCanvas);
2872
+ if (this.resizeObserver) this.resizeObserver.disconnect();else window.removeEventListener('resize', this.onResize);
2439
2873
  for (var _i = 0, _Object$keys = Object.keys(this.handlers); _i < _Object$keys.length; _i++) {
2440
2874
  var name = _Object$keys[_i];
2441
2875
  var handler = this.handlers[name];
@@ -2446,15 +2880,16 @@
2446
2880
  }]);
2447
2881
  }();
2448
2882
 
2449
- /**
2883
+ /**
2450
2884
  * Mount a fluid progress capsule into `container`.
2451
2885
  *
2452
- * Options: preset (literary name), width, height, value, min, max,
2453
- * draggable, colors, edgeStyle, textRatio (0-100, text region
2886
+ * Options: preset (literary name), width, height, value/modelValue, min, max,
2887
+ * step, draggable, keyboard, direction, precision/formatValue, colors,
2888
+ * edgeStyle, textRatio (0-100, text region
2454
2889
  * width in percent), text (HTML string or DOM nodes for the text slot),
2455
2890
  * colorContent (HTML string or DOM nodes for the color slot),
2456
- * showValue (show/hide the right-side percentage), quality,
2457
- * respectReducedMotion, cssVars.
2891
+ * showValue (show/hide the right-side percentage), quality, renderScale,
2892
+ * powerPreference, fps, paused/static, respectReducedMotion, cssVars.
2458
2893
  */
2459
2894
  function createProgressCapsule(container) {
2460
2895
  var _options$preset, _merged$min, _merged$max;
@@ -2464,14 +2899,20 @@
2464
2899
  }
2465
2900
  var preset = _objectSpread2({}, getPreset('progress', (_options$preset = options.preset) !== null && _options$preset !== void 0 ? _options$preset : '星火'));
2466
2901
  var merged = normalizeOptions(DEFAULTS.progress, preset, options);
2467
- if (merged.min > merged.max) {
2468
- var swap = merged.min;
2469
- merged.min = merged.max;
2470
- merged.max = swap;
2471
- }
2472
- var normalizedColors = merged.colors.map(normalizeColor);
2473
- if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2474
- preset.edgeStyle = merged.edgeStyle;
2902
+ var _normalizeRange3 = normalizeRange(merged.min, merged.max);
2903
+ var _normalizeRange4 = _slicedToArray(_normalizeRange3, 2);
2904
+ merged.min = _normalizeRange4[0];
2905
+ merged.max = _normalizeRange4[1];
2906
+ if (merged.modelValue !== undefined) merged.value = merged.modelValue;
2907
+ var normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
2908
+ if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2909
+ var initialEdgeStyle = merged.edgeStyle === 'tide' || merged.edgeStyle === 'flow' ? merged.edgeStyle : preset.edgeStyle;
2910
+ preset.edgeStyle = initialEdgeStyle;
2911
+ merged.edgeStyle = initialEdgeStyle;
2912
+ merged.colors = _toConsumableArray(preset.colors);
2913
+ var optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
2914
+ var colorOverride = optionColors.length === 4 && optionColors.every(Boolean) ? _toConsumableArray(optionColors) : null;
2915
+ var edgeStyleOverride = options.edgeStyle === 'flow' || options.edgeStyle === 'tide' ? merged.edgeStyle : null;
2475
2916
  var copy = COPY;
2476
2917
  var customLabel = typeof options.label === 'string' && options.label ? options.label : null;
2477
2918
  // Vue 的裸布尔属性(<ProgressCapsule disabled />)会传成空字符串,
@@ -2480,18 +2921,31 @@
2480
2921
  var readonly = merged.readonly === true || merged.readonly === '' || merged.readonly === 'true';
2481
2922
  var locked = disabled || readonly;
2482
2923
  var effectiveDraggable = locked ? false : merged.draggable !== false;
2924
+ var effectiveKeyboard = locked ? false : merged.keyboard !== false;
2483
2925
  var dirty = {
2484
2926
  preset: false,
2485
2927
  colors: false,
2486
2928
  textRatio: false,
2487
2929
  cssVars: false,
2488
2930
  edgeStyle: false,
2489
- value: false
2931
+ value: false,
2932
+ step: false,
2933
+ precision: false,
2934
+ formatValue: false,
2935
+ direction: false,
2936
+ showValue: false,
2937
+ valueSuffix: false,
2938
+ quality: false,
2939
+ renderScale: false,
2940
+ paused: false,
2941
+ static: false,
2942
+ fps: false
2490
2943
  };
2491
2944
  var root = document.createElement('div');
2492
2945
  root.className = 'hj-capsule-root hj-progress-root';
2493
2946
  root.setAttribute('role', 'slider');
2494
2947
  root.setAttribute('data-draggable', String(effectiveDraggable));
2948
+ root.dataset.direction = merged.direction === 'rtl' ? 'rtl' : 'ltr';
2495
2949
  if (disabled) {
2496
2950
  root.setAttribute('aria-disabled', 'true');
2497
2951
  root.classList.add('is-disabled');
@@ -2500,7 +2954,8 @@
2500
2954
  root.setAttribute('aria-readonly', 'true');
2501
2955
  root.classList.add('is-readonly');
2502
2956
  }
2503
- if (locked) root.setAttribute('tabindex', '-1');
2957
+ root.setAttribute('tabindex', locked ? '-1' : effectiveKeyboard ? '0' : '-1');
2958
+ root.setAttribute('aria-orientation', 'horizontal');
2504
2959
  root.setAttribute('aria-valuemin', String((_merged$min = merged.min) !== null && _merged$min !== void 0 ? _merged$min : 0));
2505
2960
  root.setAttribute('aria-valuemax', String((_merged$max = merged.max) !== null && _merged$max !== void 0 ? _merged$max : 100));
2506
2961
  root.setAttribute('aria-valuenow', String(preset.initialProgress));
@@ -2555,8 +3010,22 @@
2555
3010
  root.appendChild(valueElement);
2556
3011
  container.appendChild(root);
2557
3012
  var emitter = createEmitter();
2558
- var paused = merged.respectReducedMotion && prefersReducedMotion$1();
3013
+ var manuallyPaused = merged.paused === true;
3014
+ var reducedPaused = false;
3015
+ var staticMode = merged.static === true;
3016
+ var contextLost = false;
2559
3017
  var disposed = false;
3018
+ var renderOnce = function renderOnce() {};
3019
+ var onContextLost = function onContextLost() {
3020
+ contextLost = true;
3021
+ emitter.emit('contextlost', {});
3022
+ };
3023
+ var onContextRestored = function onContextRestored() {
3024
+ contextLost = false;
3025
+ emitter.emit('contextrestored', {});
3026
+ renderOnce();
3027
+ wakeScheduler();
3028
+ };
2560
3029
  var applySize = function applySize() {
2561
3030
  root.style.width = parseSize(merged.width);
2562
3031
  root.style.height = parseSize(merged.height);
@@ -2577,7 +3046,11 @@
2577
3046
  preset: preset,
2578
3047
  emitter: emitter,
2579
3048
  options: _objectSpread2(_objectSpread2({}, merged), {}, {
2580
- draggable: effectiveDraggable
3049
+ draggable: effectiveDraggable,
3050
+ keyboard: effectiveKeyboard,
3051
+ onResize: function onResize() {
3052
+ if (manuallyPaused || reducedPaused || staticMode) renderOnce();
3053
+ }
2581
3054
  }),
2582
3055
  copy: copy,
2583
3056
  dirty: dirty
@@ -2589,8 +3062,12 @@
2589
3062
  canvas: canvas,
2590
3063
  preset: preset,
2591
3064
  getProgress: function getProgress() {
2592
- return controller.value;
2593
- }
3065
+ return progressRatio(controller.value, controller.min, controller.max) * 100;
3066
+ },
3067
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
3068
+ powerPreference: merged.powerPreference,
3069
+ onContextLost: onContextLost,
3070
+ onContextRestored: onContextRestored
2594
3071
  });
2595
3072
  }
2596
3073
  if (overlay) controller.webglActive = true;else if (merged.renderer !== 'canvas2d') {
@@ -2601,17 +3078,34 @@
2601
3078
  });
2602
3079
  });
2603
3080
  }
2604
- var visibility = createVisibilityGuard(root);
3081
+ var visibility = createVisibilityGuard(root, wakeScheduler);
3082
+ var motionPreference = createReducedMotionPreference(merged.respectReducedMotion, function (matches) {
3083
+ reducedPaused = matches;
3084
+ if (matches) renderOnce();else wakeScheduler();
3085
+ });
3086
+ reducedPaused = motionPreference.matches();
3087
+ var isMotionPaused = function isMotionPaused() {
3088
+ return manuallyPaused || reducedPaused || staticMode || contextLost;
3089
+ };
2605
3090
  var flowTime = 0;
3091
+ var frameGate = createFrameGate(merged.fps);
3092
+ renderOnce = function renderOnce() {
3093
+ controller.draw();
3094
+ if (overlay) overlay.update(flowTime);
3095
+ };
3096
+ renderOnce();
3097
+ var offPausedChange = emitter.on('change', function () {
3098
+ if (isMotionPaused()) renderOnce();
3099
+ });
2606
3100
  var unsubscribe = subscribeScheduler(function (delta, now) {
2607
3101
  flowTime += delta;
2608
3102
  controller.update(delta, false);
2609
- if (visibility.isVisible()) {
3103
+ if (frameGate.shouldDraw(delta)) {
2610
3104
  controller.draw();
2611
3105
  if (overlay) overlay.update(flowTime);
2612
3106
  }
2613
3107
  }, function () {
2614
- return paused;
3108
+ return isMotionPaused() || !visibility.isVisible();
2615
3109
  });
2616
3110
  nextTick(function () {
2617
3111
  if (disposed) return;
@@ -2638,32 +3132,41 @@
2638
3132
  },
2639
3133
  setRange: function setRange(min, max) {
2640
3134
  controller.setRange(min, max);
3135
+ if (isMotionPaused()) renderOnce();
2641
3136
  return this;
2642
3137
  },
2643
3138
  setPreset: function setPreset(ref) {
2644
3139
  var next = getPreset('progress', ref);
2645
3140
  dirty.preset = true;
2646
3141
  Object.assign(preset, next);
3142
+ if (colorOverride) preset.colors = _toConsumableArray(colorOverride);
3143
+ if (edgeStyleOverride) preset.edgeStyle = edgeStyleOverride;
2647
3144
  var nextColors = preset.colors.map(normalizeColor);
2648
3145
  if (nextColors.every(Boolean)) preset.colors = nextColors;
2649
3146
  controller.preset = preset;
2650
- controller.profile = next.edgeStyle === 'tide' ? FLOW_PROFILES.tide : FLOW_PROFILES[next.id] || FLOW_PROFILES['visual-training'];
3147
+ controller.profile = preset.edgeStyle === 'tide' ? FLOW_PROFILES.tide : FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training'];
2651
3148
  controller.flowTime = stringSeed(next.id) * 31;
2652
3149
  controller.seed = stringSeed("".concat(next.id, "-reference")) * Math.PI * 2;
2653
3150
  syncDom();
2654
3151
  if (overlay) {
2655
3152
  overlay.dispose();
3153
+ contextLost = false;
2656
3154
  overlay = attachProgressFlowOverlay({
2657
3155
  root: root,
2658
3156
  canvas: canvas,
2659
3157
  preset: preset,
2660
3158
  getProgress: function getProgress() {
2661
- return controller.value;
2662
- }
3159
+ return progressRatio(controller.value, controller.min, controller.max) * 100;
3160
+ },
3161
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
3162
+ powerPreference: merged.powerPreference,
3163
+ onContextLost: onContextLost,
3164
+ onContextRestored: onContextRestored
2663
3165
  });
2664
3166
  }
2665
3167
  controller.webglActive = Boolean(overlay);
2666
3168
  controller.resizeCanvas();
3169
+ if (isMotionPaused()) renderOnce();
2667
3170
  emitter.emit('presetchange', {
2668
3171
  preset: _objectSpread2({}, preset)
2669
3172
  });
@@ -2673,21 +3176,28 @@
2673
3176
  var value = String(edgeStyle || '').toLowerCase();
2674
3177
  if (value !== 'flow' && value !== 'tide') return this;
2675
3178
  dirty.edgeStyle = true;
3179
+ edgeStyleOverride = value;
2676
3180
  preset.edgeStyle = value;
2677
3181
  controller.profile = value === 'tide' ? FLOW_PROFILES.tide : FLOW_PROFILES[preset.id] || FLOW_PROFILES['visual-training'];
2678
3182
  if (overlay) {
2679
3183
  overlay.dispose();
3184
+ contextLost = false;
2680
3185
  overlay = attachProgressFlowOverlay({
2681
3186
  root: root,
2682
3187
  canvas: canvas,
2683
3188
  preset: preset,
2684
3189
  getProgress: function getProgress() {
2685
- return controller.value;
2686
- }
3190
+ return progressRatio(controller.value, controller.min, controller.max) * 100;
3191
+ },
3192
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
3193
+ powerPreference: merged.powerPreference,
3194
+ onContextLost: onContextLost,
3195
+ onContextRestored: onContextRestored
2687
3196
  });
2688
3197
  }
2689
3198
  controller.webglActive = Boolean(overlay);
2690
3199
  controller.resizeCanvas();
3200
+ if (isMotionPaused()) renderOnce();
2691
3201
  return this;
2692
3202
  },
2693
3203
  setText: function setText(content) {
@@ -2725,6 +3235,22 @@
2725
3235
  controller.setValueSuffix(suffix);
2726
3236
  return this;
2727
3237
  },
3238
+ setStep: function setStep(step) {
3239
+ controller.setStep(step);
3240
+ return this;
3241
+ },
3242
+ setPrecision: function setPrecision(precision) {
3243
+ controller.setPrecision(precision);
3244
+ return this;
3245
+ },
3246
+ setFormatValue: function setFormatValue(formatter) {
3247
+ controller.setFormatValue(formatter);
3248
+ return this;
3249
+ },
3250
+ setDirection: function setDirection(direction) {
3251
+ controller.setDirection(direction);
3252
+ return this;
3253
+ },
2728
3254
  setShowValue: function setShowValue(show) {
2729
3255
  controller.setShowValue(show);
2730
3256
  return this;
@@ -2735,9 +3261,12 @@
2735
3261
  return !color;
2736
3262
  })) return this;
2737
3263
  dirty.colors = true;
3264
+ colorOverride = _toConsumableArray(next);
3265
+ preset.colors = _toConsumableArray(next);
2738
3266
  controller.setColors(next);
2739
3267
  if (overlay) overlay.setColors(next);
2740
3268
  controller.resizeCanvas();
3269
+ if (isMotionPaused()) renderOnce();
2741
3270
  return this;
2742
3271
  },
2743
3272
  setSize: function setSize(width, height) {
@@ -2751,30 +3280,68 @@
2751
3280
  }
2752
3281
  applySize();
2753
3282
  controller.resizeCanvas();
3283
+ if (isMotionPaused()) renderOnce();
2754
3284
  return this;
2755
3285
  },
2756
3286
  randomize: function randomize() {
2757
- controller.randomize();
3287
+ flowTime = controller.randomize();
3288
+ if (isMotionPaused()) renderOnce();
2758
3289
  return this;
2759
3290
  },
2760
3291
  pause: function pause() {
2761
- paused = true;
3292
+ dirty.paused = true;
3293
+ manuallyPaused = true;
3294
+ renderOnce();
2762
3295
  return this;
2763
3296
  },
2764
3297
  resume: function resume() {
2765
- paused = false;
3298
+ dirty.paused = true;
3299
+ manuallyPaused = false;
3300
+ wakeScheduler();
3301
+ return this;
3302
+ },
3303
+ setPaused: function setPaused(value) {
3304
+ return value ? this.pause() : this.resume();
3305
+ },
3306
+ setStatic: function setStatic(value) {
3307
+ dirty.static = true;
3308
+ staticMode = value === true;
3309
+ merged.static = staticMode;
3310
+ if (staticMode) renderOnce();else wakeScheduler();
3311
+ return this;
3312
+ },
3313
+ setFps: function setFps(fps) {
3314
+ dirty.fps = true;
3315
+ merged.fps = frameGate.setFps(fps);
3316
+ wakeScheduler();
2766
3317
  return this;
2767
3318
  },
2768
3319
  setQuality: function setQuality(quality) {
3320
+ dirty.quality = true;
2769
3321
  merged.quality = quality;
2770
- controller.dprCap = dprCapFor(quality);
3322
+ controller.dprCap = effectiveDprCap(quality, merged.renderScale);
3323
+ controller.resizeCanvas();
3324
+ if (overlay) overlay.setDprCap(controller.dprCap);
3325
+ if (isMotionPaused()) renderOnce();
3326
+ return this;
3327
+ },
3328
+ setRenderScale: function setRenderScale(renderScale) {
3329
+ var value = Number(renderScale);
3330
+ if (!Number.isFinite(value)) return this;
3331
+ dirty.renderScale = true;
3332
+ merged.renderScale = Math.min(1, Math.max(0.25, value));
3333
+ controller.dprCap = effectiveDprCap(merged.quality, merged.renderScale);
2771
3334
  controller.resizeCanvas();
3335
+ if (overlay) overlay.setDprCap(controller.dprCap);
3336
+ if (isMotionPaused()) renderOnce();
2772
3337
  return this;
2773
3338
  },
2774
3339
  dispose: function dispose() {
2775
3340
  disposed = true;
2776
3341
  unsubscribe();
3342
+ offPausedChange();
2777
3343
  visibility.dispose();
3344
+ motionPreference.dispose();
2778
3345
  if (overlay) overlay.dispose();
2779
3346
  controller.dispose();
2780
3347
  root.remove();
@@ -2785,26 +3352,89 @@
2785
3352
  get cssVars() {
2786
3353
  return merged.cssVars;
2787
3354
  },
3355
+ get min() {
3356
+ return controller.min;
3357
+ },
3358
+ get max() {
3359
+ return controller.max;
3360
+ },
3361
+ get step() {
3362
+ return controller.step;
3363
+ },
3364
+ get precision() {
3365
+ return controller.precision;
3366
+ },
3367
+ get formatValue() {
3368
+ return controller.formatValue;
3369
+ },
3370
+ get direction() {
3371
+ return controller.direction;
3372
+ },
3373
+ get showValue() {
3374
+ return !valueElement.hidden;
3375
+ },
3376
+ get valueSuffix() {
3377
+ return controller.valueSuffix;
3378
+ },
3379
+ get quality() {
3380
+ return merged.quality;
3381
+ },
3382
+ get renderScale() {
3383
+ return merged.renderScale;
3384
+ },
3385
+ get paused() {
3386
+ return manuallyPaused;
3387
+ },
3388
+ get static() {
3389
+ return staticMode;
3390
+ },
3391
+ get fps() {
3392
+ return frameGate.getFps();
3393
+ },
2788
3394
  dirty: dirty
2789
3395
  };
2790
3396
  }
2791
3397
 
2792
- function prefersReducedMotion() {
2793
- return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
3398
+ var HOST_STYLES = new WeakMap();
3399
+ function acquireHostStyles(host) {
3400
+ var active = HOST_STYLES.get(host);
3401
+ if (active) {
3402
+ active.count += 1;
3403
+ return;
3404
+ }
3405
+ var state = {
3406
+ count: 1,
3407
+ position: host.style.position,
3408
+ isolation: host.style.isolation
3409
+ };
3410
+ var computed = getComputedStyle(host);
3411
+ if (computed.position === 'static' || computed.position === '') host.style.position = 'relative';
3412
+ host.style.isolation = 'isolate';
3413
+ HOST_STYLES.set(host, state);
3414
+ }
3415
+ function releaseHostStyles(host) {
3416
+ var state = HOST_STYLES.get(host);
3417
+ if (!state) return;
3418
+ state.count -= 1;
3419
+ if (state.count > 0) return;
3420
+ host.style.position = state.position;
3421
+ host.style.isolation = state.isolation;
3422
+ HOST_STYLES.delete(host);
2794
3423
  }
2795
3424
 
2796
- /**
2797
- * Mount the cosmic (nebula) material as a background layer inside `host`.
2798
- *
2799
- * The layer is absolutely positioned and defaults to `z-index: -1`, so it
2800
- * paints behind the host's in-flow content: text/buttons/images inside the
2801
- * host sit on top with zero extra CSS. Host gets `position: relative;
2802
- * isolation: isolate` automatically so the layer can never escape its box
2803
- * (or break stacking outside it).
2804
- *
3425
+ /**
3426
+ * Mount the cosmic (nebula) material as a background layer inside `host`.
3427
+ *
3428
+ * The layer is absolutely positioned and defaults to `z-index: -1`, so it
3429
+ * paints behind the host's in-flow content: text/buttons/images inside the
3430
+ * host sit on top with zero extra CSS. Host gets `position: relative;
3431
+ * isolation: isolate` automatically so the layer can never escape its box
3432
+ * (or break stacking outside it).
3433
+ *
2805
3434
  * Options: preset (literary name), colors, seed, speed, quality,
2806
- * renderer, mouseColor, respectReducedMotion, opacity (0-1), fallbackColor
2807
- * (true=preset base color, a hex string, or false to disable).
3435
+ * renderer, renderScale, powerPreference, fps, paused/static, mouseColor,
3436
+ * respectReducedMotion, opacity (0-1), fallbackColor (true=preset base
3437
+ * color, a hex string, or false to disable).
2808
3438
  */
2809
3439
  function createColorBackground(host) {
2810
3440
  var _options$preset;
@@ -2818,18 +3448,29 @@
2818
3448
  renderer: DEFAULTS.capsule.renderer,
2819
3449
  respectReducedMotion: DEFAULTS.capsule.respectReducedMotion,
2820
3450
  mouseColor: true,
3451
+ renderScale: 1,
3452
+ powerPreference: 'high-performance',
3453
+ fps: DEFAULTS.capsule.fps,
3454
+ paused: false,
3455
+ static: false,
2821
3456
  opacity: 1,
2822
3457
  fallbackColor: true
2823
3458
  }, preset, options);
2824
- var normalizedColors = merged.colors.map(normalizeColor);
2825
- if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
2826
- preset.seed = merged.seed;
2827
- preset.speed = merged.speed;
2828
- var computed = getComputedStyle(host);
2829
- if (computed.position === 'static' || computed.position === '') {
2830
- host.style.position = 'relative';
2831
- }
2832
- host.style.isolation = 'isolate';
3459
+ merged.opacity = Number.isFinite(Number(merged.opacity)) ? Math.min(1, Math.max(0, Number(merged.opacity))) : 1;
3460
+ var normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
3461
+ if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
3462
+ var initialSeed = toFiniteNumber(merged.seed);
3463
+ var initialSpeed = toFiniteNumber(merged.speed);
3464
+ if (initialSeed !== null) preset.seed = initialSeed;
3465
+ if (initialSpeed !== null) preset.speed = initialSpeed;
3466
+ merged.colors = _toConsumableArray(preset.colors);
3467
+ merged.seed = preset.seed;
3468
+ merged.speed = preset.speed;
3469
+ var optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
3470
+ var colorOverride = optionColors.length === 4 && optionColors.every(Boolean) ? _toConsumableArray(optionColors) : null;
3471
+ var seedOverride = options.seed !== undefined ? toFiniteNumber(options.seed) : null;
3472
+ var speedOverride = options.speed !== undefined ? toFiniteNumber(options.speed) : null;
3473
+ acquireHostStyles(host);
2833
3474
  var layer = document.createElement('div');
2834
3475
  layer.className = 'dlc-color-layer';
2835
3476
  var syncLayerVars = function syncLayerVars() {
@@ -2844,8 +3485,12 @@
2844
3485
  layer.appendChild(canvas);
2845
3486
  host.appendChild(layer);
2846
3487
  var emitter = createEmitter();
2847
- var paused = merged.respectReducedMotion && prefersReducedMotion();
3488
+ var manuallyPaused = merged.paused === true;
3489
+ var reducedPaused = false;
3490
+ var staticMode = merged.static === true;
3491
+ var contextLost = false;
2848
3492
  var disposed = false;
3493
+ var renderOnce = function renderOnce() {};
2849
3494
  var dirty = {
2850
3495
  preset: false,
2851
3496
  seed: false,
@@ -2853,19 +3498,37 @@
2853
3498
  colors: false,
2854
3499
  opacity: false,
2855
3500
  fallbackColor: false,
2856
- mouseColor: false
3501
+ mouseColor: false,
3502
+ quality: false,
3503
+ renderScale: false,
3504
+ paused: false,
3505
+ static: false,
3506
+ fps: false
2857
3507
  };
2858
3508
  var renderer;
2859
3509
  var useWebgl = merged.renderer !== 'canvas2d';
2860
3510
  if (useWebgl) {
2861
3511
  try {
2862
3512
  renderer = new CosmicRenderer(canvas, merged, {
2863
- dprCap: dprCapFor(merged.quality),
3513
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
2864
3514
  mouseColor: merged.mouseColor !== false,
2865
- eventTarget: host
3515
+ eventTarget: host,
3516
+ powerPreference: merged.powerPreference,
3517
+ onContextLost: function onContextLost() {
3518
+ contextLost = true;
3519
+ emitter.emit('contextlost', {});
3520
+ },
3521
+ onContextRestored: function onContextRestored() {
3522
+ contextLost = false;
3523
+ emitter.emit('contextrestored', {});
3524
+ renderOnce();
3525
+ wakeScheduler();
3526
+ }
2866
3527
  });
2867
3528
  } catch (error) {
2868
- renderer = new FallbackRenderer(canvas, merged);
3529
+ renderer = new FallbackRenderer(canvas, merged, {
3530
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale)
3531
+ });
2869
3532
  nextTick(function () {
2870
3533
  if (disposed) return;
2871
3534
  emitter.emit('error', {
@@ -2874,19 +3537,37 @@
2874
3537
  });
2875
3538
  }
2876
3539
  } else {
2877
- renderer = new FallbackRenderer(canvas, merged);
3540
+ renderer = new FallbackRenderer(canvas, merged, {
3541
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale)
3542
+ });
2878
3543
  }
2879
- var resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(function () {
2880
- return renderer.resize();
2881
- }) : null;
2882
- if (resizeObserver) resizeObserver.observe(host);else window.addEventListener('resize', renderer.resize);
2883
- var visibility = createVisibilityGuard(layer);
3544
+ renderer.resize();
2884
3545
  var animationTime = 0;
3546
+ var frameGate = createFrameGate(merged.fps);
3547
+ renderOnce = function renderOnce() {
3548
+ return renderer.draw(animationTime);
3549
+ };
3550
+ var resize = function resize() {
3551
+ renderer.resize();
3552
+ if (manuallyPaused || reducedPaused || staticMode) renderOnce();
3553
+ };
3554
+ var resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
3555
+ if (resizeObserver) resizeObserver.observe(host);else window.addEventListener('resize', resize);
3556
+ var visibility = createVisibilityGuard(layer, wakeScheduler);
3557
+ var motionPreference = createReducedMotionPreference(merged.respectReducedMotion, function (matches) {
3558
+ reducedPaused = matches;
3559
+ if (matches) renderOnce();else wakeScheduler();
3560
+ });
3561
+ reducedPaused = motionPreference.matches();
3562
+ var isMotionPaused = function isMotionPaused() {
3563
+ return manuallyPaused || reducedPaused || staticMode || contextLost;
3564
+ };
3565
+ renderOnce();
2885
3566
  var unsubscribe = subscribeScheduler(function (delta) {
2886
3567
  animationTime += delta;
2887
- if (visibility.isVisible()) renderer.draw(animationTime);
3568
+ if (frameGate.shouldDraw(delta)) renderer.draw(animationTime);
2888
3569
  }, function () {
2889
- return paused;
3570
+ return isMotionPaused() || !visibility.isVisible();
2890
3571
  });
2891
3572
  nextTick(function () {
2892
3573
  if (disposed) return;
@@ -2905,11 +3586,15 @@
2905
3586
  var next = getPreset('capsule', ref);
2906
3587
  dirty.preset = true;
2907
3588
  Object.assign(preset, next);
3589
+ if (colorOverride) preset.colors = _toConsumableArray(colorOverride);
3590
+ if (seedOverride !== null) preset.seed = seedOverride;
3591
+ if (speedOverride !== null) preset.speed = speedOverride;
2908
3592
  var nextColors = preset.colors.map(normalizeColor);
2909
3593
  if (nextColors.every(Boolean)) preset.colors = nextColors;
2910
3594
  renderer.setPreset(_objectSpread2({}, preset));
2911
3595
  renderer.resize();
2912
3596
  syncLayerVars();
3597
+ if (isMotionPaused()) renderOnce();
2913
3598
  emitter.emit('presetchange', {
2914
3599
  preset: _objectSpread2({}, preset)
2915
3600
  });
@@ -2921,27 +3606,33 @@
2921
3606
  return !color;
2922
3607
  })) return this;
2923
3608
  dirty.colors = true;
3609
+ colorOverride = _toConsumableArray(next);
2924
3610
  preset.colors = next;
2925
3611
  renderer.setPreset(_objectSpread2(_objectSpread2({}, preset), {}, {
2926
3612
  colors: next
2927
3613
  }));
2928
3614
  syncLayerVars();
3615
+ if (isMotionPaused()) renderOnce();
2929
3616
  return this;
2930
3617
  },
2931
3618
  setSeed: function setSeed(seed) {
2932
- var value = Number(seed);
2933
- if (!Number.isFinite(value)) return this;
3619
+ var value = toFiniteNumber(seed);
3620
+ if (value === null) return this;
2934
3621
  dirty.seed = true;
3622
+ seedOverride = value;
2935
3623
  preset.seed = value;
2936
3624
  renderer.setPreset(_objectSpread2({}, preset));
3625
+ if (isMotionPaused()) renderOnce();
2937
3626
  return this;
2938
3627
  },
2939
3628
  setSpeed: function setSpeed(speed) {
2940
- var value = Number(speed);
2941
- if (!Number.isFinite(value)) return this;
3629
+ var value = toFiniteNumber(speed);
3630
+ if (value === null) return this;
2942
3631
  dirty.speed = true;
3632
+ speedOverride = value;
2943
3633
  preset.speed = value;
2944
3634
  renderer.setPreset(_objectSpread2({}, preset));
3635
+ if (isMotionPaused()) renderOnce();
2945
3636
  return this;
2946
3637
  },
2947
3638
  setFallbackColor: function setFallbackColor(value) {
@@ -2957,8 +3648,23 @@
2957
3648
  return this;
2958
3649
  },
2959
3650
  setQuality: function setQuality(quality) {
3651
+ dirty.quality = true;
2960
3652
  merged.quality = quality;
2961
- if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
3653
+ if (typeof renderer.setDprCap === 'function') {
3654
+ renderer.setDprCap(effectiveDprCap(quality, merged.renderScale));
3655
+ }
3656
+ if (isMotionPaused()) renderOnce();
3657
+ return this;
3658
+ },
3659
+ setRenderScale: function setRenderScale(renderScale) {
3660
+ var value = Number(renderScale);
3661
+ if (!Number.isFinite(value)) return this;
3662
+ dirty.renderScale = true;
3663
+ merged.renderScale = Math.min(1, Math.max(0.25, value));
3664
+ if (typeof renderer.setDprCap === 'function') {
3665
+ renderer.setDprCap(effectiveDprCap(merged.quality, merged.renderScale));
3666
+ }
3667
+ if (isMotionPaused()) renderOnce();
2962
3668
  return this;
2963
3669
  },
2964
3670
  setOpacity: function setOpacity(value) {
@@ -2974,20 +3680,42 @@
2974
3680
  return this;
2975
3681
  },
2976
3682
  pause: function pause() {
2977
- paused = true;
3683
+ dirty.paused = true;
3684
+ manuallyPaused = true;
3685
+ renderOnce();
2978
3686
  return this;
2979
3687
  },
2980
3688
  resume: function resume() {
2981
- paused = false;
3689
+ dirty.paused = true;
3690
+ manuallyPaused = false;
3691
+ wakeScheduler();
3692
+ return this;
3693
+ },
3694
+ setPaused: function setPaused(value) {
3695
+ return value ? this.pause() : this.resume();
3696
+ },
3697
+ setStatic: function setStatic(value) {
3698
+ dirty.static = true;
3699
+ staticMode = value === true;
3700
+ merged.static = staticMode;
3701
+ if (staticMode) renderOnce();else wakeScheduler();
3702
+ return this;
3703
+ },
3704
+ setFps: function setFps(fps) {
3705
+ dirty.fps = true;
3706
+ merged.fps = frameGate.setFps(fps);
3707
+ wakeScheduler();
2982
3708
  return this;
2983
3709
  },
2984
3710
  dispose: function dispose() {
2985
3711
  disposed = true;
2986
3712
  unsubscribe();
2987
3713
  visibility.dispose();
2988
- if (resizeObserver) resizeObserver.disconnect();else window.removeEventListener('resize', renderer.resize);
3714
+ motionPreference.dispose();
3715
+ if (resizeObserver) resizeObserver.disconnect();else window.removeEventListener('resize', resize);
2989
3716
  renderer.dispose();
2990
3717
  layer.remove();
3718
+ releaseHostStyles(host);
2991
3719
  },
2992
3720
  get opacity() {
2993
3721
  return merged.opacity;
@@ -2998,6 +3726,21 @@
2998
3726
  get mouseColor() {
2999
3727
  return merged.mouseColor;
3000
3728
  },
3729
+ get quality() {
3730
+ return merged.quality;
3731
+ },
3732
+ get renderScale() {
3733
+ return merged.renderScale;
3734
+ },
3735
+ get paused() {
3736
+ return manuallyPaused;
3737
+ },
3738
+ get static() {
3739
+ return staticMode;
3740
+ },
3741
+ get fps() {
3742
+ return frameGate.getFps();
3743
+ },
3001
3744
  dirty: dirty
3002
3745
  };
3003
3746
  }
@@ -3016,6 +3759,7 @@
3016
3759
  exports.createColorBackground = createColorBackground;
3017
3760
  exports.createProgressCapsule = createProgressCapsule;
3018
3761
  exports.dprCapFor = dprCapFor;
3762
+ exports.effectiveDprCap = effectiveDprCap;
3019
3763
  exports.getPreset = getPreset;
3020
3764
  exports.hexToRgb01 = hexToRgb01$1;
3021
3765
  exports.hexToRgba = hexToRgba;