@5even7/dlc-ui 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/capsule.cjs CHANGED
@@ -182,9 +182,9 @@ function _unsupportedIterableToArray(r, a) {
182
182
  }
183
183
  }
184
184
 
185
- /**
186
- * Tiny event emitter. Accepts any event name so the API stays open for
187
- * future events (hover, click, custom) without breaking changes.
185
+ /**
186
+ * Tiny event emitter. Accepts any event name so the API stays open for
187
+ * future events (hover, click, custom) without breaking changes.
188
188
  */
189
189
  function createEmitter() {
190
190
  // Plain object storage (no Map/Set) so the legacy build has no API
@@ -234,9 +234,9 @@ function createEmitter() {
234
234
 
235
235
  var UNIT_PATTERN = /^[\d.]+(?:px|%|vw|vh|vmin|vmax|em|rem)$/;
236
236
 
237
- /**
238
- * Normalize a size option to a CSS length string.
239
- * Accepts numbers (treated as px) or strings with px/%, vw/vh/vmin/vmax, em/rem.
237
+ /**
238
+ * Normalize a size option to a CSS length string.
239
+ * Accepts numbers (treated as px) or strings with px/%, vw/vh/vmin/vmax, em/rem.
240
240
  */
241
241
  function parseSize(value) {
242
242
  if (typeof value === 'number') {
@@ -275,11 +275,17 @@ function dprCapFor(quality) {
275
275
  var tier = QUALITY_TIERS[quality] || QUALITY_TIERS.medium;
276
276
  return tier.dpr;
277
277
  }
278
+ function effectiveDprCap(quality) {
279
+ var renderScale = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
280
+ var scale = Number(renderScale);
281
+ var normalizedScale = Number.isFinite(scale) ? Math.min(1, Math.max(0.25, scale)) : 1;
282
+ return Math.max(0.5, dprCapFor(quality) * normalizedScale);
283
+ }
278
284
 
279
- /**
280
- * Merge defaults < preset < user. Unknown user keys are preserved so the
281
- * component API can grow (new props, cssVars, callbacks) without a breaking
282
- * change.
285
+ /**
286
+ * Merge defaults < preset < user. Unknown user keys are preserved so the
287
+ * component API can grow (new props, cssVars, callbacks) without a breaking
288
+ * change.
283
289
  */
284
290
  function normalizeOptions(defaults, preset, user) {
285
291
  // undefined means "not provided" (e.g. Vue $props with unset props):
@@ -297,9 +303,9 @@ function normalizeOptions(defaults, preset, user) {
297
303
  return _objectSpread2(_objectSpread2(_objectSpread2({}, defaults), presetOptions), userOptions);
298
304
  }
299
305
 
300
- /**
301
- * 公共色板:NC-01~NC-06 的四色组(底色 / 主色 / 辅色 / 高光色)。
302
- * Capsule 预置与 dlc-color 等组件共用,新增色板只改这里。
306
+ /**
307
+ * 公共色板:NC-01~NC-06 的四色组(底色 / 主色 / 辅色 / 高光色)。
308
+ * Capsule 预置与 dlc-color 等组件共用,新增色板只改这里。
303
309
  */
304
310
  var PALETTES = {
305
311
  original: ['#FFF3EA', '#F5B27A', '#F67BC6', '#A978E8'],
@@ -310,9 +316,9 @@ var PALETTES = {
310
316
  plus: ['#FFF0E6', '#F6C26B', '#F98A64', '#E86D74']
311
317
  };
312
318
 
313
- /**
314
- * Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
315
- * 颜色引用公共色板,seed/speed 决定形态与流速。
319
+ /**
320
+ * Capsule 预置:仅星云材质 NC-01~NC-06(NC-07~09 aurora 已移除)。
321
+ * 颜色引用公共色板,seed/speed 决定形态与流速。
316
322
  */
317
323
  var CAPSULE_PRESETS = [{
318
324
  id: 'original',
@@ -430,6 +436,11 @@ var DEFAULTS = {
430
436
  height: 160,
431
437
  quality: 'auto',
432
438
  renderer: 'auto',
439
+ renderScale: 1,
440
+ powerPreference: 'high-performance',
441
+ fps: 60,
442
+ paused: false,
443
+ static: false,
433
444
  respectReducedMotion: true,
434
445
  mouseColor: true,
435
446
  textRatio: 39
@@ -700,7 +711,7 @@ var CosmicRenderer = /*#__PURE__*/function () {
700
711
  alpha: false,
701
712
  antialias: false,
702
713
  depth: false,
703
- powerPreference: 'high-performance',
714
+ powerPreference: this.options.powerPreference || 'high-performance',
704
715
  preserveDrawingBuffer: false
705
716
  });
706
717
  if (!this.gl) throw new Error('WebGL2 is not available');
@@ -711,16 +722,19 @@ var CosmicRenderer = /*#__PURE__*/function () {
711
722
  this.motion = 0;
712
723
  this.motionTarget = 0;
713
724
  this.timeOffset = preset.seed * 0.73;
725
+ this.colors = preset.colors.map(hexToRgb01);
714
726
  this.visible = true;
715
727
  this.disposed = false;
716
728
  _assertClassBrand(_CosmicRenderer_brand, this, _setupGeometry).call(this);
717
729
  _assertClassBrand(_CosmicRenderer_brand, this, _bindEvents).call(this);
730
+ _assertClassBrand(_CosmicRenderer_brand, this, _bindContextEvents).call(this);
718
731
  this.resize();
719
732
  }
720
733
  return _createClass(CosmicRenderer, [{
721
734
  key: "setPreset",
722
735
  value: function setPreset(preset) {
723
736
  this.preset = _objectSpread2({}, preset);
737
+ this.colors = preset.colors.map(hexToRgb01);
724
738
  this.timeOffset = preset.seed * 0.73;
725
739
  }
726
740
  }, {
@@ -765,15 +779,14 @@ var CosmicRenderer = /*#__PURE__*/function () {
765
779
  if (this.canvas.width !== width || this.canvas.height !== height) {
766
780
  this.canvas.width = width;
767
781
  this.canvas.height = height;
768
- this.gl.viewport(0, 0, width, height);
769
782
  }
783
+ this.gl.viewport(0, 0, width, height);
770
784
  }
771
785
  }, {
772
786
  key: "draw",
773
787
  value: function draw(elapsedSeconds) {
774
788
  var paused = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
775
789
  if (this.disposed || !this.visible) return;
776
- this.resize();
777
790
  var gl = this.gl;
778
791
  this.pointer[0] += (this.pointerTarget[0] - this.pointer[0]) * 0.08;
779
792
  this.pointer[1] += (this.pointerTarget[1] - this.pointer[1]) * 0.08;
@@ -782,16 +795,15 @@ var CosmicRenderer = /*#__PURE__*/function () {
782
795
  gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
783
796
  gl.enableVertexAttribArray(this.locations.position);
784
797
  gl.vertexAttribPointer(this.locations.position, 2, gl.FLOAT, false, 0, 0);
785
- var colors = this.preset.colors.map(hexToRgb01);
786
798
  gl.uniform2f(this.locations.resolution, this.canvas.width, this.canvas.height);
787
799
  gl.uniform1f(this.locations.time, this.timeOffset + (paused ? 0 : elapsedSeconds * this.preset.speed));
788
800
  gl.uniform1f(this.locations.seed, this.preset.seed);
789
801
  gl.uniform1f(this.locations.motion, this.motion);
790
802
  gl.uniform2f(this.locations.pointer, this.pointer[0], this.pointer[1]);
791
- gl.uniform3fv(this.locations.colorA, colors[0]);
792
- gl.uniform3fv(this.locations.colorB, colors[1]);
793
- gl.uniform3fv(this.locations.colorC, colors[2]);
794
- gl.uniform3fv(this.locations.colorD, colors[3]);
803
+ gl.uniform3fv(this.locations.colorA, this.colors[0]);
804
+ gl.uniform3fv(this.locations.colorB, this.colors[1]);
805
+ gl.uniform3fv(this.locations.colorC, this.colors[2]);
806
+ gl.uniform3fv(this.locations.colorD, this.colors[3]);
795
807
  gl.drawArrays(gl.TRIANGLES, 0, 3);
796
808
  }
797
809
  }, {
@@ -802,6 +814,8 @@ var CosmicRenderer = /*#__PURE__*/function () {
802
814
  target.removeEventListener('pointermove', this.onPointerMove);
803
815
  target.removeEventListener('pointerdown', this.onPointerMove);
804
816
  target.removeEventListener('pointerleave', this.onPointerLeave);
817
+ this.canvas.removeEventListener('webglcontextlost', this.onContextLost, false);
818
+ this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored, false);
805
819
  this.gl.deleteBuffer(this.buffer);
806
820
  this.gl.deleteProgram(this.program);
807
821
  var lose = this.gl.getExtension('WEBGL_lose_context');
@@ -858,6 +872,26 @@ function _bindEvents() {
858
872
  passive: true
859
873
  });
860
874
  }
875
+ function _bindContextEvents() {
876
+ var _this3 = this;
877
+ this.onContextLost = function (event) {
878
+ event.preventDefault();
879
+ if (_this3.disposed) return;
880
+ _this3.visible = false;
881
+ if (typeof _this3.options.onContextLost === 'function') _this3.options.onContextLost(event);
882
+ };
883
+ this.onContextRestored = function () {
884
+ if (_this3.disposed) return;
885
+ _this3.program = createProgram(_this3.gl);
886
+ _this3.locations = _assertClassBrand(_CosmicRenderer_brand, _this3, _getLocations).call(_this3);
887
+ _assertClassBrand(_CosmicRenderer_brand, _this3, _setupGeometry).call(_this3);
888
+ _this3.visible = true;
889
+ _this3.resize();
890
+ if (typeof _this3.options.onContextRestored === 'function') _this3.options.onContextRestored();
891
+ };
892
+ this.canvas.addEventListener('webglcontextlost', this.onContextLost, false);
893
+ this.canvas.addEventListener('webglcontextrestored', this.onContextRestored, false);
894
+ }
861
895
 
862
896
  function rgb(color) {
863
897
  var alpha = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
@@ -872,18 +906,20 @@ function rgb(color) {
872
906
  }
873
907
  var FallbackRenderer = /*#__PURE__*/function () {
874
908
  function FallbackRenderer(canvas, preset) {
909
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
875
910
  _classCallCheck(this, FallbackRenderer);
876
911
  this.canvas = canvas;
877
912
  this.preset = preset;
878
913
  this.context = canvas.getContext('2d');
879
914
  this.visible = true;
880
915
  this.timePhase = 0;
916
+ this.dprCap = options.dprCap || 1.5;
881
917
  }
882
918
  return _createClass(FallbackRenderer, [{
883
919
  key: "resize",
884
920
  value: function resize() {
885
921
  var rect = this.canvas.getBoundingClientRect();
886
- var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
922
+ var dpr = Math.min(window.devicePixelRatio || 1, this.dprCap);
887
923
  var width = Math.max(2, Math.round(rect.width * dpr));
888
924
  var height = Math.max(2, Math.round(rect.height * dpr));
889
925
  if (this.canvas.width !== width || this.canvas.height !== height) {
@@ -924,7 +960,6 @@ var FallbackRenderer = /*#__PURE__*/function () {
924
960
  key: "draw",
925
961
  value: function draw(time) {
926
962
  if (!this.visible) return;
927
- this.resize();
928
963
  this.drawNebula(time);
929
964
  }
930
965
  }, {
@@ -932,6 +967,12 @@ var FallbackRenderer = /*#__PURE__*/function () {
932
967
  value: function setPreset(preset) {
933
968
  this.preset = preset;
934
969
  }
970
+ }, {
971
+ key: "setDprCap",
972
+ value: function setDprCap(cap) {
973
+ this.dprCap = cap;
974
+ this.resize();
975
+ }
935
976
  }, {
936
977
  key: "randomize",
937
978
  value: function randomize() {
@@ -949,10 +990,10 @@ var FallbackRenderer = /*#__PURE__*/function () {
949
990
  }]);
950
991
  }();
951
992
 
952
- /**
953
- * Document-level shared rAF scheduler. Every component instance subscribes
954
- * its own frame callback; the whole page runs ONE animation loop (like the
955
- * original demo), which avoids jank from many competing rAF loops.
993
+ /**
994
+ * Document-level shared rAF scheduler. Every component instance subscribes
995
+ * its own frame callback; the whole page runs ONE animation loop (like the
996
+ * original demo), which avoids jank from many competing rAF loops.
956
997
  */
957
998
  var subscribers = [];
958
999
  var running = false;
@@ -960,11 +1001,7 @@ var rafId = 0;
960
1001
  var last = 0;
961
1002
  function tick(now) {
962
1003
  if (!running) return;
963
- var delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
964
- last = now;
965
- // Schedule the next frame BEFORE running callbacks so one throwing
966
- // subscriber can never kill the whole animation loop.
967
- rafId = requestAnimationFrame(tick);
1004
+ var activeItems = [];
968
1005
  {
969
1006
  var _iterator = _createForOfIteratorHelper(subscribers.slice()),
970
1007
  _step;
@@ -972,10 +1009,8 @@ function tick(now) {
972
1009
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
973
1010
  var item = _step.value;
974
1011
  try {
975
- if (!item.isPaused()) item.onFrame(delta, now);
976
- } catch (error) {
977
- console.warn('[dlc-ui] frame error:', error);
978
- }
1012
+ if (!item.isPaused()) activeItems.push(item);
1013
+ } catch (_unused) {}
979
1014
  }
980
1015
  } catch (err) {
981
1016
  _iterator.e(err);
@@ -983,6 +1018,25 @@ function tick(now) {
983
1018
  _iterator.f();
984
1019
  }
985
1020
  }
1021
+ if (activeItems.length === 0) {
1022
+ running = false;
1023
+ rafId = 0;
1024
+ last = 0;
1025
+ return;
1026
+ }
1027
+ var delta = last ? Math.min((now - last) / 1000, 0.05) : 0;
1028
+ last = now;
1029
+ // Schedule the next frame BEFORE running callbacks so one throwing
1030
+ // subscriber can never kill the whole animation loop.
1031
+ rafId = requestAnimationFrame(tick);
1032
+ for (var _i = 0, _activeItems = activeItems; _i < _activeItems.length; _i++) {
1033
+ var _item = _activeItems[_i];
1034
+ try {
1035
+ _item.onFrame(delta, now);
1036
+ } catch (error) {
1037
+ console.warn('[dlc-ui] frame error:', error);
1038
+ }
1039
+ }
986
1040
  if (subscribers.length === 0) {
987
1041
  cancelAnimationFrame(rafId);
988
1042
  running = false;
@@ -995,6 +1049,9 @@ function start() {
995
1049
  last = 0;
996
1050
  rafId = requestAnimationFrame(tick);
997
1051
  }
1052
+ function wakeScheduler() {
1053
+ start();
1054
+ }
998
1055
  function subscribeScheduler(onFrame, isPaused) {
999
1056
  var item = {
1000
1057
  onFrame: onFrame,
@@ -1013,11 +1070,12 @@ function subscribeScheduler(onFrame, isPaused) {
1013
1070
  };
1014
1071
  }
1015
1072
 
1016
- /**
1017
- * Gates drawing on "element intersects viewport AND the page tab is visible".
1018
- * Falls back to always-visible when IntersectionObserver is unavailable.
1073
+ /**
1074
+ * Gates drawing on "element intersects viewport AND the page tab is visible".
1075
+ * Falls back to always-visible when IntersectionObserver is unavailable.
1019
1076
  */
1020
1077
  function createVisibilityGuard(element) {
1078
+ var onChange = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
1021
1079
  var intersecting = true;
1022
1080
  var pageVisible = typeof document === 'undefined' || !document.hidden;
1023
1081
  var disposed = false;
@@ -1027,6 +1085,7 @@ function createVisibilityGuard(element) {
1027
1085
  intersecting = entries.some(function (entry) {
1028
1086
  return entry.isIntersecting;
1029
1087
  });
1088
+ if (typeof onChange === 'function') onChange();
1030
1089
  }, {
1031
1090
  rootMargin: '180px'
1032
1091
  });
@@ -1034,6 +1093,7 @@ function createVisibilityGuard(element) {
1034
1093
  }
1035
1094
  var onVisibilityChange = function onVisibilityChange() {
1036
1095
  pageVisible = typeof document !== 'undefined' && !document.hidden;
1096
+ if (typeof onChange === 'function') onChange();
1037
1097
  };
1038
1098
  if (typeof document !== 'undefined') {
1039
1099
  document.addEventListener('visibilitychange', onVisibilityChange);
@@ -1069,18 +1129,69 @@ function nextTick(fn) {
1069
1129
  }
1070
1130
  }
1071
1131
 
1072
- function prefersReducedMotion() {
1073
- return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
1132
+ function normalizeFps(value) {
1133
+ var fallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 60;
1134
+ var fps = Number(value);
1135
+ if (!Number.isFinite(fps)) return fallback;
1136
+ return Math.min(60, Math.max(1, fps));
1137
+ }
1138
+ function createFrameGate() {
1139
+ var initialFps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 60;
1140
+ var fps = normalizeFps(initialFps);
1141
+ var elapsed = 0;
1142
+ return {
1143
+ shouldDraw: function shouldDraw(delta) {
1144
+ elapsed += delta;
1145
+ var interval = 1 / fps;
1146
+ if (elapsed + 0.0001 < interval) return false;
1147
+ elapsed %= interval;
1148
+ return true;
1149
+ },
1150
+ setFps: function setFps(value) {
1151
+ fps = normalizeFps(value, fps);
1152
+ elapsed = 0;
1153
+ return fps;
1154
+ },
1155
+ getFps: function getFps() {
1156
+ return fps;
1157
+ }
1158
+ };
1159
+ }
1160
+ function createReducedMotionPreference(enabled, onChange) {
1161
+ var query = enabled && typeof matchMedia !== 'undefined' ? matchMedia('(prefers-reduced-motion: reduce)') : null;
1162
+ var notify = function notify() {
1163
+ if (typeof onChange === 'function') onChange(Boolean(query && query.matches));
1164
+ };
1165
+ if (query) {
1166
+ if (typeof query.addEventListener === 'function') query.addEventListener('change', notify);else if (typeof query.addListener === 'function') query.addListener(notify);
1167
+ }
1168
+ return {
1169
+ matches: function matches() {
1170
+ return Boolean(query && query.matches);
1171
+ },
1172
+ dispose: function dispose() {
1173
+ if (!query) return;
1174
+ if (typeof query.removeEventListener === 'function') query.removeEventListener('change', notify);else if (typeof query.removeListener === 'function') query.removeListener(notify);
1175
+ }
1176
+ };
1177
+ }
1178
+
1179
+ function toFiniteNumber(value) {
1180
+ if (value == null || typeof value === 'boolean') return null;
1181
+ if (typeof value === 'string' && value.trim() === '') return null;
1182
+ var number = Number(value);
1183
+ return Number.isFinite(number) ? number : null;
1074
1184
  }
1075
1185
 
1076
- /**
1186
+ /**
1077
1187
  * Mount a cosmic (nebula) capsule into `container`.
1078
1188
  *
1079
1189
  * Options: preset (literary name), width, height (number=px or string with
1080
1190
  * px/%/vw/vh/em/rem), colors, seed, speed, textRatio (0-100, text region
1081
1191
  * width in percent), text (HTML string or DOM nodes for the text slot),
1082
1192
  * colorContent (HTML string or DOM nodes for the color slot), quality,
1083
- * renderer, respectReducedMotion, mouseColor, cssVars.
1193
+ * renderer, quality, renderScale, powerPreference, fps, paused/static,
1194
+ * respectReducedMotion, mouseColor, cssVars.
1084
1195
  */
1085
1196
  function createCapsule(container) {
1086
1197
  var _options$preset;
@@ -1090,10 +1201,19 @@ function createCapsule(container) {
1090
1201
  }
1091
1202
  var preset = _objectSpread2({}, getPreset('capsule', (_options$preset = options.preset) !== null && _options$preset !== void 0 ? _options$preset : '初光'));
1092
1203
  var merged = normalizeOptions(DEFAULTS.capsule, preset, options);
1093
- var normalizedColors = merged.colors.map(normalizeColor);
1094
- if (normalizedColors.every(Boolean)) preset.colors = normalizedColors;
1095
- preset.seed = merged.seed;
1096
- preset.speed = merged.speed;
1204
+ var normalizedColors = Array.isArray(merged.colors) ? merged.colors.map(normalizeColor) : [];
1205
+ if (normalizedColors.length === 4 && normalizedColors.every(Boolean)) preset.colors = normalizedColors;
1206
+ var initialSeed = toFiniteNumber(merged.seed);
1207
+ var initialSpeed = toFiniteNumber(merged.speed);
1208
+ if (initialSeed !== null) preset.seed = initialSeed;
1209
+ if (initialSpeed !== null) preset.speed = initialSpeed;
1210
+ merged.colors = _toConsumableArray(preset.colors);
1211
+ merged.seed = preset.seed;
1212
+ merged.speed = preset.speed;
1213
+ var optionColors = Array.isArray(options.colors) ? options.colors.map(normalizeColor) : [];
1214
+ var colorOverride = optionColors.length === 4 && optionColors.every(Boolean) ? _toConsumableArray(optionColors) : null;
1215
+ var seedOverride = options.seed !== undefined ? toFiniteNumber(options.seed) : null;
1216
+ var speedOverride = options.speed !== undefined ? toFiniteNumber(options.speed) : null;
1097
1217
  var customLabel = typeof options.label === 'string' && options.label ? options.label : null;
1098
1218
  var root = document.createElement('div');
1099
1219
  root.className = 'hj-capsule-root hj-capsule-cosmic';
@@ -1145,25 +1265,49 @@ function createCapsule(container) {
1145
1265
  root.appendChild(canvas);
1146
1266
  container.appendChild(root);
1147
1267
  var emitter = createEmitter();
1148
- var paused = merged.respectReducedMotion && prefersReducedMotion();
1268
+ var manuallyPaused = merged.paused === true;
1269
+ var reducedPaused = false;
1270
+ var staticMode = merged.static === true;
1271
+ var contextLost = false;
1149
1272
  var disposed = false;
1273
+ var renderOnce = function renderOnce() {};
1150
1274
  var dirty = {
1151
1275
  preset: false,
1152
1276
  seed: false,
1153
1277
  speed: false,
1154
1278
  colors: false,
1155
1279
  textRatio: false,
1156
- cssVars: false
1280
+ cssVars: false,
1281
+ mouseColor: false,
1282
+ quality: false,
1283
+ renderScale: false,
1284
+ paused: false,
1285
+ static: false,
1286
+ fps: false
1157
1287
  };
1158
1288
  var renderer;
1159
1289
  var useWebgl = merged.renderer !== 'canvas2d';
1160
1290
  if (useWebgl) {
1161
1291
  try {
1162
1292
  renderer = new CosmicRenderer(canvas, merged, {
1163
- dprCap: dprCapFor(merged.quality)
1293
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale),
1294
+ mouseColor: merged.mouseColor !== false,
1295
+ powerPreference: merged.powerPreference,
1296
+ onContextLost: function onContextLost() {
1297
+ contextLost = true;
1298
+ emitter.emit('contextlost', {});
1299
+ },
1300
+ onContextRestored: function onContextRestored() {
1301
+ contextLost = false;
1302
+ emitter.emit('contextrestored', {});
1303
+ renderOnce();
1304
+ wakeScheduler();
1305
+ }
1164
1306
  });
1165
1307
  } catch (error) {
1166
- renderer = new FallbackRenderer(canvas, merged);
1308
+ renderer = new FallbackRenderer(canvas, merged, {
1309
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale)
1310
+ });
1167
1311
  nextTick(function () {
1168
1312
  if (disposed) return;
1169
1313
  emitter.emit('error', {
@@ -1172,8 +1316,15 @@ function createCapsule(container) {
1172
1316
  });
1173
1317
  }
1174
1318
  } else {
1175
- renderer = new FallbackRenderer(canvas, merged);
1319
+ renderer = new FallbackRenderer(canvas, merged, {
1320
+ dprCap: effectiveDprCap(merged.quality, merged.renderScale)
1321
+ });
1176
1322
  }
1323
+ var animationTime = 0;
1324
+ var frameGate = createFrameGate(merged.fps);
1325
+ renderOnce = function renderOnce() {
1326
+ return renderer.draw(animationTime);
1327
+ };
1177
1328
  var applySize = function applySize() {
1178
1329
  root.style.width = parseSize(merged.width);
1179
1330
  root.style.height = parseSize(merged.height);
@@ -1191,11 +1342,21 @@ function createCapsule(container) {
1191
1342
  renderer.resize();
1192
1343
  };
1193
1344
  applySize();
1194
- var resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(function () {
1195
- return renderer.resize();
1196
- }) : null;
1197
- if (resizeObserver) resizeObserver.observe(root);else window.addEventListener('resize', renderer.resize);
1198
- var visibility = createVisibilityGuard(root);
1345
+ var resize = function resize() {
1346
+ renderer.resize();
1347
+ if (manuallyPaused || reducedPaused || staticMode) renderOnce();
1348
+ };
1349
+ var resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
1350
+ if (resizeObserver) resizeObserver.observe(root);else window.addEventListener('resize', resize);
1351
+ var visibility = createVisibilityGuard(root, wakeScheduler);
1352
+ var motionPreference = createReducedMotionPreference(merged.respectReducedMotion, function (matches) {
1353
+ reducedPaused = matches;
1354
+ if (matches) renderOnce();else wakeScheduler();
1355
+ });
1356
+ reducedPaused = motionPreference.matches();
1357
+ var isMotionPaused = function isMotionPaused() {
1358
+ return manuallyPaused || reducedPaused || staticMode || contextLost;
1359
+ };
1199
1360
  root.addEventListener('pointerenter', function () {
1200
1361
  return emitter.emit('pointerenter', {
1201
1362
  preset: _objectSpread2({}, preset)
@@ -1230,12 +1391,12 @@ function createCapsule(container) {
1230
1391
  preset: _objectSpread2({}, preset)
1231
1392
  });
1232
1393
  });
1233
- var animationTime = 0;
1394
+ renderOnce();
1234
1395
  var unsubscribe = subscribeScheduler(function (delta) {
1235
1396
  animationTime += delta;
1236
- if (visibility.isVisible()) renderer.draw(animationTime);
1397
+ if (frameGate.shouldDraw(delta)) renderer.draw(animationTime);
1237
1398
  }, function () {
1238
- return paused;
1399
+ return isMotionPaused() || !visibility.isVisible();
1239
1400
  });
1240
1401
  nextTick(function () {
1241
1402
  if (disposed) return;
@@ -1258,11 +1419,15 @@ function createCapsule(container) {
1258
1419
  var next = getPreset('capsule', ref);
1259
1420
  dirty.preset = true;
1260
1421
  Object.assign(preset, next);
1422
+ if (colorOverride) preset.colors = _toConsumableArray(colorOverride);
1423
+ if (seedOverride !== null) preset.seed = seedOverride;
1424
+ if (speedOverride !== null) preset.speed = speedOverride;
1261
1425
  var nextColors = preset.colors.map(normalizeColor);
1262
1426
  if (nextColors.every(Boolean)) preset.colors = nextColors;
1263
1427
  renderer.setPreset(_objectSpread2({}, preset));
1264
1428
  syncTheme();
1265
1429
  renderer.resize();
1430
+ if (isMotionPaused()) renderOnce();
1266
1431
  emitter.emit('presetchange', {
1267
1432
  preset: _objectSpread2({}, next)
1268
1433
  });
@@ -1274,26 +1439,32 @@ function createCapsule(container) {
1274
1439
  return !color;
1275
1440
  })) return this;
1276
1441
  dirty.colors = true;
1442
+ colorOverride = _toConsumableArray(next);
1277
1443
  preset.colors = next;
1278
1444
  renderer.setPreset(_objectSpread2(_objectSpread2({}, preset), {}, {
1279
1445
  colors: next
1280
1446
  }));
1447
+ if (isMotionPaused()) renderOnce();
1281
1448
  return this;
1282
1449
  },
1283
1450
  setSeed: function setSeed(seed) {
1284
- var value = Number(seed);
1285
- if (!Number.isFinite(value)) return this;
1451
+ var value = toFiniteNumber(seed);
1452
+ if (value === null) return this;
1286
1453
  dirty.seed = true;
1454
+ seedOverride = value;
1287
1455
  preset.seed = value;
1288
1456
  renderer.setPreset(_objectSpread2({}, preset));
1457
+ if (isMotionPaused()) renderOnce();
1289
1458
  return this;
1290
1459
  },
1291
1460
  setSpeed: function setSpeed(speed) {
1292
- var value = Number(speed);
1293
- if (!Number.isFinite(value)) return this;
1461
+ var value = toFiniteNumber(speed);
1462
+ if (value === null) return this;
1294
1463
  dirty.speed = true;
1464
+ speedOverride = value;
1295
1465
  preset.speed = value;
1296
1466
  renderer.setPreset(_objectSpread2({}, preset));
1467
+ if (isMotionPaused()) renderOnce();
1297
1468
  return this;
1298
1469
  },
1299
1470
  setText: function setText(content) {
@@ -1329,6 +1500,7 @@ function createCapsule(container) {
1329
1500
  return this;
1330
1501
  },
1331
1502
  setMouseColor: function setMouseColor(value) {
1503
+ dirty.mouseColor = true;
1332
1504
  merged.mouseColor = value !== false;
1333
1505
  if (typeof renderer.setMouseColor === 'function') renderer.setMouseColor(merged.mouseColor);
1334
1506
  return this;
@@ -1343,6 +1515,7 @@ function createCapsule(container) {
1343
1515
  merged.height = height;
1344
1516
  }
1345
1517
  applySize();
1518
+ if (isMotionPaused()) renderOnce();
1346
1519
  return this;
1347
1520
  },
1348
1521
  randomize: function randomize() {
@@ -1350,23 +1523,59 @@ function createCapsule(container) {
1350
1523
  return this;
1351
1524
  },
1352
1525
  pause: function pause() {
1353
- paused = true;
1526
+ dirty.paused = true;
1527
+ manuallyPaused = true;
1528
+ renderOnce();
1354
1529
  return this;
1355
1530
  },
1356
1531
  resume: function resume() {
1357
- paused = false;
1532
+ dirty.paused = true;
1533
+ manuallyPaused = false;
1534
+ wakeScheduler();
1535
+ return this;
1536
+ },
1537
+ setPaused: function setPaused(value) {
1538
+ return value ? this.pause() : this.resume();
1539
+ },
1540
+ setStatic: function setStatic(value) {
1541
+ dirty.static = true;
1542
+ staticMode = value === true;
1543
+ merged.static = staticMode;
1544
+ if (staticMode) renderOnce();else wakeScheduler();
1545
+ return this;
1546
+ },
1547
+ setFps: function setFps(fps) {
1548
+ dirty.fps = true;
1549
+ merged.fps = frameGate.setFps(fps);
1550
+ wakeScheduler();
1358
1551
  return this;
1359
1552
  },
1360
1553
  setQuality: function setQuality(quality) {
1554
+ dirty.quality = true;
1361
1555
  merged.quality = quality;
1362
- if (typeof renderer.setDprCap === 'function') renderer.setDprCap(dprCapFor(quality));
1556
+ if (typeof renderer.setDprCap === 'function') {
1557
+ renderer.setDprCap(effectiveDprCap(quality, merged.renderScale));
1558
+ }
1559
+ if (isMotionPaused()) renderOnce();
1560
+ return this;
1561
+ },
1562
+ setRenderScale: function setRenderScale(renderScale) {
1563
+ var value = Number(renderScale);
1564
+ if (!Number.isFinite(value)) return this;
1565
+ dirty.renderScale = true;
1566
+ merged.renderScale = Math.min(1, Math.max(0.25, value));
1567
+ if (typeof renderer.setDprCap === 'function') {
1568
+ renderer.setDprCap(effectiveDprCap(merged.quality, merged.renderScale));
1569
+ }
1570
+ if (isMotionPaused()) renderOnce();
1363
1571
  return this;
1364
1572
  },
1365
1573
  dispose: function dispose() {
1366
1574
  disposed = true;
1367
1575
  unsubscribe();
1368
1576
  visibility.dispose();
1369
- if (resizeObserver) resizeObserver.disconnect();else window.removeEventListener('resize', renderer.resize);
1577
+ motionPreference.dispose();
1578
+ if (resizeObserver) resizeObserver.disconnect();else window.removeEventListener('resize', resize);
1370
1579
  renderer.dispose();
1371
1580
  root.remove();
1372
1581
  },
@@ -1376,6 +1585,24 @@ function createCapsule(container) {
1376
1585
  get cssVars() {
1377
1586
  return merged.cssVars;
1378
1587
  },
1588
+ get mouseColor() {
1589
+ return merged.mouseColor;
1590
+ },
1591
+ get quality() {
1592
+ return merged.quality;
1593
+ },
1594
+ get renderScale() {
1595
+ return merged.renderScale;
1596
+ },
1597
+ get paused() {
1598
+ return manuallyPaused;
1599
+ },
1600
+ get static() {
1601
+ return staticMode;
1602
+ },
1603
+ get fps() {
1604
+ return frameGate.getFps();
1605
+ },
1379
1606
  dirty: dirty
1380
1607
  };
1381
1608
  }