tweakit 0.6.0 → 0.7.0

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.
Files changed (132) hide show
  1. package/AGENTS.md +5 -5
  2. package/README.md +3 -3
  3. package/dist/tweaks/auto.js +1 -1
  4. package/dist/tweaks/auto.js.map +1 -1
  5. package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
  6. package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
  7. package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
  8. package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
  9. package/dist/tweaks/chunk-ETB35KFV.js +2 -0
  10. package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
  11. package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
  12. package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
  13. package/dist/tweaks/clip-A3VI53BK.js +2 -0
  14. package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
  15. package/dist/tweaks/color-UGYMAF3W.js +2 -0
  16. package/dist/tweaks/core.js +1 -1
  17. package/dist/tweaks/core.js.map +3 -3
  18. package/dist/tweaks/fold-GY47HIQA.js +2 -0
  19. package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
  20. package/dist/tweaks/gradient-BTX25WSH.js +2 -0
  21. package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
  22. package/dist/tweaks/{heavy-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
  23. package/dist/tweaks/image-HTQQ4WX6.js +4 -0
  24. package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
  25. package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
  26. package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
  27. package/dist/tweaks/interval-BHPI4LRN.js +2 -0
  28. package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
  29. package/dist/tweaks/markup-QM53TQJZ.js +2 -0
  30. package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
  31. package/dist/tweaks/monitor-722WIYGW.js +3 -0
  32. package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
  33. package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
  34. package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
  35. package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
  36. package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
  37. package/dist/tweaks/point-XDXKMDXH.js +2 -0
  38. package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
  39. package/dist/tweaks/presets-EUWNSCUW.js +2 -0
  40. package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
  41. package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
  42. package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
  43. package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
  44. package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
  45. package/dist/tweaks/sides-JWIURDA4.js +2 -0
  46. package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
  47. package/dist/tweaks/spring-BF5GBTPM.js +2 -0
  48. package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
  49. package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
  50. package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
  51. package/dist/tweaks.css +1 -1
  52. package/dist/tweaks.js +5 -4
  53. package/dist/tweaks.js.map +3 -3
  54. package/dist/types/tweaks/enhance.d.ts +3 -2
  55. package/dist/types/tweaks/types.d.ts +278 -72
  56. package/package.json +5 -2
  57. package/src/tweaks/auto.ts +9 -5
  58. package/src/tweaks/clip.ts +84 -22
  59. package/src/tweaks/controls/basic.ts +463 -139
  60. package/src/tweaks/controls/bezier.ts +152 -47
  61. package/src/tweaks/controls/color.ts +610 -161
  62. package/src/tweaks/controls/gradient.ts +256 -77
  63. package/src/tweaks/controls/image.ts +388 -132
  64. package/src/tweaks/controls/interval.ts +126 -41
  65. package/src/tweaks/controls/monitor.ts +142 -36
  66. package/src/tweaks/controls/motion.ts +231 -56
  67. package/src/tweaks/controls/plot.ts +321 -101
  68. package/src/tweaks/controls/point.ts +134 -45
  69. package/src/tweaks/controls/shadow.ts +103 -30
  70. package/src/tweaks/controls/sides.ts +150 -40
  71. package/src/tweaks/controls/spring.ts +220 -60
  72. package/src/tweaks/controls/tabs.ts +39 -18
  73. package/src/tweaks/core.ts +1 -1
  74. package/src/tweaks/easing.ts +45 -20
  75. package/src/tweaks/enhance.ts +21 -6
  76. package/src/tweaks/feedback.ts +133 -34
  77. package/src/tweaks/fold.ts +22 -10
  78. package/src/tweaks/heavy.ts +461 -137
  79. package/src/tweaks/icons.ts +12 -5
  80. package/src/tweaks/image-store.ts +131 -43
  81. package/src/tweaks/lazy.ts +36 -19
  82. package/src/tweaks/markup.ts +255 -92
  83. package/src/tweaks/panel.ts +1262 -438
  84. package/src/tweaks/presets.ts +85 -0
  85. package/src/tweaks/schema.ts +213 -51
  86. package/src/tweaks/shadow-root.ts +178 -0
  87. package/src/tweaks/shared.ts +802 -190
  88. package/src/tweaks/standalone.ts +120 -30
  89. package/src/tweaks/types.ts +644 -143
  90. package/src/tweaks/undo.ts +104 -0
  91. package/src/tweaks.css +556 -550
  92. package/src/wide-gamut.ts +473 -125
  93. package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
  94. package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
  95. package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
  96. package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
  97. package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
  98. package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
  99. package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
  100. package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
  101. package/dist/tweaks/clip-WW7JYO4X.js +0 -2
  102. package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
  103. package/dist/tweaks/color-YMMFURP6.js +0 -2
  104. package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
  105. package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
  106. package/dist/tweaks/gradient-JER7PW4D.js +0 -2
  107. package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
  108. package/dist/tweaks/image-H6QSLGO3.js +0 -4
  109. package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
  110. package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
  111. package/dist/tweaks/interval-TEUHQGZN.js +0 -2
  112. package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
  113. package/dist/tweaks/markup-NIBJD6E2.js +0 -2
  114. package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
  115. package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
  116. package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
  117. package/dist/tweaks/motion-FQPUBKKM.js +0 -2
  118. package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
  119. package/dist/tweaks/plot-2HBNBZSG.js +0 -3
  120. package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
  121. package/dist/tweaks/point-VV6XV3I6.js +0 -2
  122. package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
  123. package/dist/tweaks/shadow-5E4E55I2.js +0 -2
  124. package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
  125. package/dist/tweaks/sides-P3STEVYY.js +0 -2
  126. package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
  127. package/dist/tweaks/spring-HUVSXNVN.js +0 -2
  128. package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
  129. package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
  130. package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
  131. /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
  132. /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
@@ -2,84 +2,194 @@
2
2
  // linked; unlinked, the row opens in place and four fields sit in a line under it, in CSS order, each under its glyph.
3
3
  // The value is the four numbers in CSS order plus the shorthand `css`, built only from the numbers and the unit —
4
4
  // never a host string. Lazy. ──
5
- import { el, txt, btn, icon, registerControl, stepPrecision } from "../shared.js";
5
+ import { el, txt, btn, icon, registerControl, stepPrecision, words } from "../shared.js";
6
6
  import { numField, decimalStep } from "../heavy.js";
7
7
  import type { OnChange, NumField } from "../shared.js";
8
8
  import type { Meta } from "../schema.js";
9
9
  import type { Control, SidesValue } from "../types.js";
10
10
 
11
- const ICON_SPLIT = icon('<path d="M3 9V7a4 4 0 0 1 4-4h2M15 3h2a4 4 0 0 1 4 4v2M21 15v2a4 4 0 0 1-4 4h-2M9 21H7a4 4 0 0 1-4-4v-2"/>'); // original: the four corner arcs of a rounded box, apart
12
- const ICON_CORNER = icon('<path d="M3 13V8a5 5 0 0 1 5-5h5"/>', "", 1.5, 16); // original: the top-left quarter of a rounded box, turned per corner in CSS
13
- const ICON_EDGE = icon('<path d="M3 10V8a5 5 0 0 1 5-5a5 5 0 0 1 5 5v2"/>', "", 1.5, 16); // original: the top of that box, two of its corners joined, turned per side
11
+ // original: the four corner arcs of a rounded box, apart
12
+ const ICON_SPLIT = icon(
13
+ '<path d="M3 9V7a4 4 0 0 1 4-4h2M15 3h2a4 4 0 0 1 4 4v2M21 15v2a4 4 0 0 1-4 4h-2M9 21H7a4 4 0 0 1-4-4v-2"/>',
14
+ );
15
+ // original: the top-left quarter of a rounded box, turned per corner in CSS
16
+ const ICON_CORNER = icon('<path d="M3 13V8a5 5 0 0 1 5-5h5"/>', "", 1.5, 16);
17
+ // original: the top of that box, two of its corners joined, turned per side
18
+ const ICON_EDGE = icon('<path d="M3 10V8a5 5 0 0 1 5-5a5 5 0 0 1 5 5v2"/>', "", 1.5, 16);
14
19
  const LEN_RE = /^(-?\d*\.?\d+)([a-z%]*)$/i;
15
20
  const num = (v: unknown) => (v != null && v !== "" && Number.isFinite(+v) ? +v : null);
16
- const expand = (n: number[]) => (n.length === 1 ? [n[0], n[0], n[0], n[0]] : n.length === 2 ? [n[0], n[1], n[0], n[1]] : n.length === 3 ? [n[0], n[1], n[2], n[1]] : n.slice(0, 4));
21
+ const expand = (n: number[]) =>
22
+ n.length === 1
23
+ ? [n[0], n[0], n[0], n[0]]
24
+ : n.length === 2
25
+ ? [n[0], n[1], n[0], n[1]]
26
+ : n.length === 3
27
+ ? [n[0], n[1], n[2], n[1]]
28
+ : n.slice(0, 4);
17
29
  // A shorthand string as one to four [number, unit] pairs, or null for anything else: calc(), var(), five lengths, junk.
18
- const lengths = (s: string) => { const m = s.trim().split(/\s+/).map((t) => LEN_RE.exec(t)); return m.length <= 4 && m.every(Boolean) ? (m as RegExpExecArray[]).map((x) => [+x[1], x[2].toLowerCase()] as const) : null; };
30
+ const lengths = (s: string) => {
31
+ const m = s
32
+ .trim()
33
+ .split(/\s+/)
34
+ .map((t) => LEN_RE.exec(t));
35
+ return m.length <= 4 && m.every(Boolean)
36
+ ? (m as RegExpExecArray[]).map((x) => [+x[1], x[2].toLowerCase()] as const)
37
+ : null;
38
+ };
19
39
  // Any form to four numbers, or null; an object fills in from `cur`, so `{ top: 4 }` moves one side.
20
40
  const read = (v: unknown, cur: number[]): number[] | null => {
21
41
  if (typeof v === "number") return Number.isFinite(v) ? [v, v, v, v] : null;
22
- if (typeof v === "string") { const n = lengths(v); return n ? expand(n.map((x) => x[0])) : null; }
23
- if (Array.isArray(v)) { const n = v.map(num); return n.length >= 1 && n.length <= 4 && n.every((x) => x !== null) ? expand(n as number[]) : null; }
24
- if (v && typeof v === "object") { const o = v as Record<string, unknown>, out = cur.slice(); let any = false; (["top", "right", "bottom", "left"] as const).forEach((k, i) => { const x = num(o[k]); if (x !== null) { out[i] = x; any = true; } }); return any ? out : null; }
42
+ if (typeof v === "string") {
43
+ const n = lengths(v);
44
+ return n ? expand(n.map((x) => x[0])) : null;
45
+ }
46
+ if (Array.isArray(v)) {
47
+ const n = v.map(num);
48
+ return n.length >= 1 && n.length <= 4 && n.every((x) => x !== null) ? expand(n as number[]) : null;
49
+ }
50
+ if (v && typeof v === "object") {
51
+ const o = v as Record<string, unknown>,
52
+ out = cur.slice();
53
+ let any = false;
54
+ (["top", "right", "bottom", "left"] as const).forEach((k, i) => {
55
+ const x = num(o[k]);
56
+ if (x !== null) {
57
+ out[i] = x;
58
+ any = true;
59
+ }
60
+ });
61
+ return any ? out : null;
62
+ }
25
63
  return null;
26
64
  };
27
65
 
28
- // The unit a shorthand string says, when its sides agree: "1rem 2rem" is rem, and so is "0 1rem 2". A zero doesn't vote,
29
- // as 0 is 0 in any unit. Mixed units can't be kept, since the control edits four numbers in one unit, so they warn and
30
- // read as px, as every string did before; "" when the string names no unit.
66
+ // The unit a shorthand string says, when its sides agree: "1rem 2rem" is rem, and so is "0 1rem 2". A zero doesn't
67
+ // vote, as 0 is 0 in any unit. Mixed units can't be kept, since the control edits four numbers in one unit, so they
68
+ // warn and read as px, as every string did before; "" when the string names no unit.
31
69
  const saidUnit = (v: unknown, name: string, sides: string[]) => {
32
- const n = typeof v === "string" && lengths(v); if (!n) return "";
33
- const said = expand(n.map((_, i) => i)).map((i, s) => [sides[s].toLowerCase(), n[i][0] ? n[i][1] : ""]).filter(([, u]) => u);
70
+ const n = typeof v === "string" && lengths(v);
71
+ if (!n) return "";
72
+ const said = expand(n.map((_, i) => i))
73
+ .map((i, s) => [sides[s].toLowerCase(), n[i][0] ? n[i][1] : ""])
74
+ .filter(([, u]) => u);
34
75
  if (new Set(said.map(([, u]) => u)).size < 2) return said.length ? said[0][1] : "";
35
- console.warn(`[tweaks] sides "${name}": "${v}" mixes units (${said.map((s) => s.join(" ")).join(", ")}) and the control edits one, so all four read as px; write one unit, or set unit`);
76
+ console.warn(
77
+ `[tweaks] sides "${name}": "${v}" mixes units (${said.map((s) => s.join(" ")).join(", ")}) and the control edits one, so all four read as px; write one unit, or set unit`,
78
+ );
36
79
  return "";
37
80
  };
38
81
 
39
82
  function createSides(meta: Meta, onChange: OnChange): Control {
40
- const corners = !!meta.corners, names = corners ? ["Top-left", "Top-right", "Bottom-right", "Bottom-left"] : ["Top", "Right", "Bottom", "Left"];
41
- const name = meta.label || meta.key, init = meta.value ?? meta;
42
- const unit = (typeof meta.unit === "string" && meta.unit.trim()) || saidUnit(init, name, names) || "px"; // an explicit unit wins over the string's
83
+ const corners = !!meta.corners,
84
+ names = corners ? ["Top-left", "Top-right", "Bottom-right", "Bottom-left"] : ["Top", "Right", "Bottom", "Left"];
85
+ // topLeft …; `names` stays the glyph's data-at
86
+ const tr = words(),
87
+ shown = names.map((n) =>
88
+ tr(
89
+ n.toLowerCase().replace(/-(\w)/, (_, c) => c.toUpperCase()),
90
+ n,
91
+ ),
92
+ );
93
+ const name = meta.label || meta.key,
94
+ init = meta.value ?? meta;
95
+ // an explicit unit wins over the string's
96
+ const unit = (typeof meta.unit === "string" && meta.unit.trim()) || saidUnit(init, name, names) || "px";
43
97
  let v = read(init, [0, 0, 0, 0]);
44
- // No step: 1, or finer when a side has decimals, so "0.5rem 1.25rem" keeps its quarter rem (decimalStep, as the number's).
45
- const spec = { step: meta.step ?? Math.min(1, ...(v || []).map(decimalStep)), min: meta.min ?? (corners ? 0 : undefined), max: meta.max, unit }; // a radius can't go negative; a margin can
46
- const fmt = (n: number) => String(+n.toFixed(Math.max(2, stepPrecision(+spec.step)))); // the readout and css at the step's decimals, so 0.125rem isn't 0.13rem
47
- // A default it can't read still starts at 0, but says so: a padding that quietly lost its value looks like the page's bug.
98
+ // No step: 1, or finer when a side has decimals, so "0.5rem 1.25rem" keeps its quarter rem (decimalStep, as the
99
+ // number's).
100
+ // a radius can't go negative; a margin can
101
+ const spec = {
102
+ step: meta.step ?? Math.min(1, ...(v || []).map(decimalStep)),
103
+ min: meta.min ?? (corners ? 0 : undefined),
104
+ max: meta.max,
105
+ unit,
106
+ };
107
+ // the readout and css at the step's decimals, so 0.125rem isn't 0.13rem
108
+ const fmt = (n: number) => String(+n.toFixed(Math.max(2, stepPrecision(+spec.step))));
109
+ // A default it can't read still starts at 0, but says so: a padding that quietly lost its value looks like the page's
110
+ // bug.
48
111
  // An object naming no side stays quiet, as markup hands its whole dataset over when there's no data-value.
49
- if (!v && meta.value != null && (typeof meta.value !== "object" || Array.isArray(meta.value))) console.warn(`[tweaks] sides "${name}": can't read ${JSON.stringify(meta.value)?.slice(0, 60)}, so it starts at 0; it takes one to four numbers or plain lengths, like "8px 16px"`);
112
+ if (!v && meta.value != null && (typeof meta.value !== "object" || Array.isArray(meta.value)))
113
+ console.warn(
114
+ `[tweaks] sides "${name}": can't read ${JSON.stringify(meta.value)?.slice(0, 60)}, so it starts at 0; it takes one to four numbers or plain lengths, like "8px 16px"`,
115
+ );
50
116
  v = v || [0, 0, 0, 0];
51
117
  let linked = v.every((x) => x === v[0]);
52
- const root = el("div", "tw-sides");
53
- const all = numField({ label: meta.label, value: v[0], row: true, ...spec }, (n) => { v = [n, n, n, n]; update(); emit(); });
54
- // One toggle, moved to whichever block is showing; last in both, so it sits on the same pixels either way. It says what
55
- // a click opens — each corner or side on its own — and is pressed while they are.
56
- const split = corners ? "Independent corners" : "Independent sides";
57
- const link = btn("tw-sides-link", ICON_SPLIT); link.setAttribute("aria-label", split); link.title = split; link.addEventListener("click", () => toggle());
118
+ const root = el("div", "twk-sides");
119
+ const all = numField({ label: meta.label, value: v[0], row: true, ...spec }, (n) => {
120
+ v = [n, n, n, n];
121
+ update();
122
+ emit();
123
+ });
124
+ // One toggle, moved to whichever block is showing; last in both, so it sits on the same pixels either way. It says
125
+ // what a click opens — each corner or side on its own — and is pressed while they are.
126
+ const split = corners ? tr("splitCorners", "Independent corners") : tr("splitSides", "Independent sides");
127
+ const link = btn("twk-sides-link", ICON_SPLIT);
128
+ link.setAttribute("aria-label", split);
129
+ link.title = split;
130
+ link.addEventListener("click", () => toggle());
58
131
  // The head has the row's geometry, so opening moves nothing: the readout takes the field's place, with the unit once.
59
- const multi = el("div", "tw-sides-multi"), head = el("div", "tw-sides-head"), sum = el("span", "tw-sides-sum"), nums = document.createTextNode("");
60
- sum.append(nums, txt("span", "tw-sides-unit", unit)); head.append(txt("span", "tw-row-label", meta.label), sum);
61
- const fields = el("div", "tw-fields");
62
- const flds: NumField[] = names.map((n, i) => numField({ label: n, value: v[i], ...spec }, (x) => { v[i] = x; update(); emit(); }));
132
+ const multi = el("div", "twk-sides-multi"),
133
+ head = el("div", "twk-sides-head"),
134
+ sum = el("span", "twk-sides-sum"),
135
+ nums = document.createTextNode("");
136
+ sum.append(nums, txt("span", "twk-sides-unit", unit));
137
+ head.append(txt("span", "twk-row-label", meta.label), sum);
138
+ const fields = el("div", "twk-fields");
139
+ const flds: NumField[] = shown.map((n, i) =>
140
+ numField({ label: n, value: v[i], ...spec }, (x) => {
141
+ v[i] = x;
142
+ update();
143
+ emit();
144
+ }),
145
+ );
63
146
  // The input and the tooltip carry the full name; on screen each field wears its glyph, one drawing turned per place.
64
- flds.forEach((f, i) => { const lab = f.el.querySelector<HTMLElement>(".tw-field-label")!; lab.title = names[i]; lab.classList.add("tw-sides-glyph"); lab.dataset.at = names[i].toLowerCase(); lab.innerHTML = corners ? ICON_CORNER : ICON_EDGE; fields.append(f.el); });
65
- multi.append(head, fields); root.append(all.el, multi);
147
+ flds.forEach((f, i) => {
148
+ const lab = f.el.querySelector<HTMLElement>(".twk-field-label")!;
149
+ lab.title = shown[i];
150
+ lab.classList.add("twk-sides-glyph");
151
+ lab.dataset.at = names[i].toLowerCase();
152
+ lab.innerHTML = corners ? ICON_CORNER : ICON_EDGE;
153
+ fields.append(f.el);
154
+ });
155
+ multi.append(head, fields);
156
+ root.append(all.el, multi);
66
157
  // As the fields fitted them.
67
158
  v = flds.map((f) => f.get());
68
159
 
69
- const css = () => { const [t, r, b, l] = v.map(fmt), u = (s: string) => (s === "0" ? "0" : s + unit); return (t === b && r === l ? (t === r ? [t] : [t, r]) : r === l ? [t, r, b] : [t, r, b, l]).map(u).join(" "); };
160
+ const css = () => {
161
+ const [t, r, b, l] = v.map(fmt),
162
+ u = (s: string) => (s === "0" ? "0" : s + unit);
163
+ return (t === b && r === l ? (t === r ? [t] : [t, r]) : r === l ? [t, r, b] : [t, r, b, l]).map(u).join(" ");
164
+ };
70
165
  const value = (): SidesValue => ({ top: v[0], right: v[1], bottom: v[2], left: v[3], css: css() });
71
166
  const emit = () => onChange(value());
72
167
  const update = () => {
73
- root.classList.toggle("is-linked", linked); link.setAttribute("aria-pressed", String(!linked));
168
+ root.classList.toggle("is-linked", linked);
169
+ link.setAttribute("aria-pressed", String(!linked));
74
170
  (linked ? all.el : head).append(link);
75
- all.set(v[0]); flds.forEach((f, i) => f.set(v[i]));
171
+ all.set(v[0]);
172
+ flds.forEach((f, i) => f.set(v[i]));
76
173
  nums.data = v.map(fmt).join(" ");
77
174
  };
78
- const toggle = () => { linked = !linked; if (linked) v = [v[0], v[0], v[0], v[0]]; update(); emit(); link.focus(); };
175
+ const toggle = () => {
176
+ linked = !linked;
177
+ if (linked) v = [v[0], v[0], v[0], v[0]];
178
+ update();
179
+ emit();
180
+ link.focus();
181
+ };
79
182
  update();
80
183
 
81
184
  // Set/restore: equal sides re-link, unequal ones unlink, so a restored "8 16 8 16" opens the fields it needs.
82
- const set = (x: unknown) => { const n = read(x, v); if (!n) return; flds.forEach((f, i) => f.set(n[i])); v = flds.map((f) => f.get()); linked = v.every((s) => s === v[0]); update(); };
185
+ const set = (x: unknown) => {
186
+ const n = read(x, v);
187
+ if (!n) return;
188
+ flds.forEach((f, i) => f.set(n[i]));
189
+ v = flds.map((f) => f.get());
190
+ linked = v.every((s) => s === v[0]);
191
+ update();
192
+ };
83
193
  return { el: root, set, get: value, range: all.range };
84
194
  }
85
195
 
@@ -1,145 +1,305 @@
1
1
  // ── Spring config — physics (stiffness/damping/mass) or a perceptual time (duration/bounce) mode, over one settle-curve preview. Lazy. ──
2
- import { el, txt, onReady, onLive, clamp, createSegmented, registerControl } from "../shared.js";
3
- import { dragGesture, numField, cssVar, accentColor, springCurve, springFromTime, boxFrac, notice, SPRING_DUR } from "../heavy.js";
2
+ import { el, txt, onReady, onLive, clamp, createSegmented, registerControl, words } from "../shared.js";
3
+ import {
4
+ dragGesture,
5
+ numField,
6
+ cssVar,
7
+ accentColor,
8
+ springCurve,
9
+ springFromTime,
10
+ boxFrac,
11
+ notice,
12
+ SPRING_DUR,
13
+ } from "../heavy.js";
4
14
  import type { OnChange, NumField } from "../shared.js";
5
15
  import type { Meta } from "../schema.js";
6
16
  import type { Control } from "../types.js";
7
17
 
8
18
  type SpringMode = "time" | "physics";
9
19
  /** The physics triple every emitted value carries. */
10
- interface Phys { stiffness: number; damping: number; mass: number }
20
+ interface Phys {
21
+ stiffness: number;
22
+ damping: number;
23
+ mass: number;
24
+ }
11
25
  type PhysKey = keyof Phys;
12
26
  /** The perceptual pair the time mode edits. */
13
- interface SpringTime { visualDuration: number; bounce: number }
27
+ interface SpringTime {
28
+ visualDuration: number;
29
+ bounce: number;
30
+ }
14
31
  /** What set() accepts: any subset of the physics, the time pair and an explicit mode. */
15
32
  type SpringValue = Partial<Phys & SpringTime & { mode: SpringMode }>;
16
33
 
17
34
  function createSpring(meta: Meta, onChange: OnChange): Control {
35
+ const tr = words();
18
36
  // The fields' floors, non-finite to the defaults: a stiffness or mass of 0 would divide the settle curve to NaN.
19
37
  const cl = (v: unknown, lo: number, def: number) => (Number.isFinite(+v) ? Math.max(lo, +v) : def);
20
- const clampS = (o: { stiffness?: unknown; damping?: unknown; mass?: unknown }): Phys => ({ stiffness: cl(o.stiffness, 1, 300), damping: cl(o.damping, 0.5, 26), mass: cl(o.mass, 0.1, 1) });
38
+ const clampS = (o: { stiffness?: unknown; damping?: unknown; mass?: unknown }): Phys => ({
39
+ stiffness: cl(o.stiffness, 1, 300),
40
+ damping: cl(o.damping, 0.5, 26),
41
+ mass: cl(o.mass, 0.1, 1),
42
+ });
21
43
  const has = (x: unknown) => Number.isFinite(+x);
22
44
 
23
- // Time mode edits visualDuration + bounce (Motion's spring shorthand), mapped onto the physics the preview and emit share
24
- // exactly as Motion resolves them (springFromTime): stiffness = (2π / (visualDuration × 1.2))², ζ from bounce floored at
25
- // 0.05, a negative bounce overdamped. Not through the physics pad's clamps, which would cut a short spring's stiffness.
45
+ // Time mode edits visualDuration + bounce (Motion's spring shorthand), mapped onto the physics the preview and emit
46
+ // share exactly as Motion resolves them (springFromTime): stiffness = (2π / (visualDuration × 1.2))², ζ from bounce
47
+ // floored at
48
+ // 0.05, a negative bounce overdamped. Not through the physics pad's clamps, which would cut a short spring's
49
+ // stiffness.
26
50
  // An authored duration past the editor's range is clamped, and the console says so once.
27
51
  const [DUR_MIN, DUR_MAX] = SPRING_DUR;
28
52
  let said = false;
29
53
  const clampDur = (v: unknown) => {
30
- const n = has(v) ? +v : 0.5, c = clamp(n, DUR_MIN, DUR_MAX);
31
- if (c !== n && !said) { said = true; notice("springDur", [meta.key, n, c]); }
54
+ const n = has(v) ? +v : 0.5,
55
+ c = clamp(n, DUR_MIN, DUR_MAX);
56
+ if (c !== n && !said) {
57
+ said = true;
58
+ notice("springDur", [meta.key, n, c]);
59
+ }
32
60
  return c;
33
61
  };
34
62
  const clampBounce = (v: unknown) => clamp(has(v) ? +v : 0.2, -1, 1); // Motion takes a negative bounce: overdamped
35
63
  // Ten significant figures: the trig round trip otherwise hands back 300.00000000000006 for a stiffness of 300.
36
64
  const sig = (x: number) => +x.toPrecision(10);
37
- const timeToPhysics = (t: SpringTime): Phys => { const p = springFromTime(t.visualDuration, t.bounce); return { stiffness: sig(p.stiffness), damping: sig(p.damping), mass: p.mass }; };
65
+ const timeToPhysics = (t: SpringTime): Phys => {
66
+ const p = springFromTime(t.visualDuration, t.bounce);
67
+ return { stiffness: sig(p.stiffness), damping: sig(p.damping), mass: p.mass };
68
+ };
38
69
 
39
- // Top-level props (the inline shorthand) or the `value:{…}` form, value winning; the mode is explicit or inferred, time winning.
70
+ // Top-level props (the inline shorthand) or the `value:{…}` form, value winning; the mode is explicit or inferred,
71
+ // time winning.
40
72
  const explicit = (m: unknown): SpringMode | null => (m === "time" || m === "physics" ? m : null);
41
- const init = { stiffness: meta.stiffness, damping: meta.damping, mass: meta.mass, visualDuration: meta.visualDuration, bounce: meta.bounce, ...(meta.value || {}) };
42
- let mode: SpringMode = explicit(init.mode) ?? explicit(meta.mode) ?? ((has(init.visualDuration) || has(init.bounce)) ? "time" : "physics");
73
+ const init = {
74
+ stiffness: meta.stiffness,
75
+ damping: meta.damping,
76
+ mass: meta.mass,
77
+ visualDuration: meta.visualDuration,
78
+ bounce: meta.bounce,
79
+ ...(meta.value || {}),
80
+ };
81
+ let mode: SpringMode =
82
+ explicit(init.mode) ?? explicit(meta.mode) ?? (has(init.visualDuration) || has(init.bounce) ? "time" : "physics");
43
83
  const time: SpringTime = { visualDuration: clampDur(init.visualDuration), bounce: clampBounce(init.bounce) };
44
84
  let phys = clampS({ stiffness: init.stiffness, damping: init.damping, mass: init.mass });
45
85
 
46
- const root = el("div", "tw-spring");
47
- const viz = el("div", "tw-spring-viz");
48
- const canvas = document.createElement("canvas"); canvas.className = "tw-spring-canvas"; viz.append(canvas);
49
- const physFields = el("div", "tw-fields");
50
- const timeFields = el("div", "tw-fields");
86
+ const root = el("div", "twk-spring");
87
+ const viz = el("div", "twk-spring-viz");
88
+ const canvas = document.createElement("canvas");
89
+ canvas.className = "twk-spring-canvas";
90
+ viz.append(canvas);
91
+ const physFields = el("div", "twk-fields");
92
+ const timeFields = el("div", "twk-fields");
51
93
  const ctx = canvas.getContext("2d");
52
94
 
53
95
  // The preview and the emitted value both read this, so they can never disagree.
54
96
  const resolved = () => (mode === "time" ? timeToPhysics(time) : clampS(phys));
55
97
  const draw = () => {
56
98
  // Layout size: getBoundingClientRect includes the pop's entrance scale.
57
- const w = Math.max(1, viz.clientWidth), h = Math.max(1, viz.clientHeight); if (w < 2) return;
58
- const dpr = window.devicePixelRatio || 1; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
99
+ const w = Math.max(1, viz.clientWidth),
100
+ h = Math.max(1, viz.clientHeight);
101
+ if (w < 2) return;
102
+ const dpr = window.devicePixelRatio || 1;
103
+ canvas.width = w * dpr;
104
+ canvas.height = h * dpr;
105
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
59
106
  ctx.clearRect(0, 0, w, h);
60
107
  const s = resolved();
61
108
  // Sample ~one point per device pixel so fast oscillations stay smooth, not angular.
62
109
  const pts = springCurve(s.stiffness, s.damping, s.mass, Math.max(160, Math.round(w * dpr)));
63
- const peak = Math.max(1.05, ...pts), pad = 8; // the bezier editor's PAD, so the two previews inset alike
110
+ const peak = Math.max(1.05, ...pts),
111
+ pad = 8; // the bezier editor's PAD, so the two previews inset alike
64
112
  const X = (i: number) => pad + (i / (pts.length - 1)) * (w - 2 * pad);
65
113
  const Y = (v: number) => h - pad - (v / peak) * (h - 2 * pad);
66
- ctx.strokeStyle = cssVar(root, "--tw-surface-active") || "rgba(255,255,255,0.1)"; ctx.lineWidth = 1;
67
- ctx.beginPath(); ctx.moveTo(pad, Y(1)); ctx.lineTo(w - pad, Y(1)); ctx.stroke(); // target (=1)
68
- ctx.strokeStyle = accentColor(root); ctx.lineWidth = 1.5; ctx.lineJoin = ctx.lineCap = "round";
69
- ctx.beginPath(); pts.forEach((v, i) => (i ? ctx.lineTo(X(i), Y(v)) : ctx.moveTo(X(i), Y(v)))); ctx.stroke();
114
+ ctx.strokeStyle = cssVar(root, "--twk-surface-active") || "rgba(255,255,255,0.1)";
115
+ ctx.lineWidth = 1;
116
+ ctx.beginPath();
117
+ ctx.moveTo(pad, Y(1));
118
+ ctx.lineTo(w - pad, Y(1));
119
+ ctx.stroke(); // target (=1)
120
+ ctx.strokeStyle = accentColor(root);
121
+ ctx.lineWidth = 1.5;
122
+ ctx.lineJoin = ctx.lineCap = "round";
123
+ ctx.beginPath();
124
+ pts.forEach((v, i) => (i ? ctx.lineTo(X(i), Y(v)) : ctx.moveTo(X(i), Y(v))));
125
+ ctx.stroke();
70
126
  };
71
- // The resolved {k,d,m} plus, in time mode, visualDuration/bounce, from which set() infers the mode back from persistence.
72
- const resolvedValue = (): Phys & Partial<SpringTime> => (mode === "time" ? { ...resolved(), visualDuration: time.visualDuration, bounce: time.bounce } : resolved());
127
+ // The resolved {k,d,m} plus, in time mode, visualDuration/bounce, from which set() infers the mode back from
128
+ // persistence.
129
+ const resolvedValue = (): Phys & Partial<SpringTime> =>
130
+ mode === "time" ? { ...resolved(), visualDuration: time.visualDuration, bounce: time.bounce } : resolved();
73
131
  const emit = () => onChange(resolvedValue());
74
132
 
75
133
  const flds: Partial<Record<PhysKey, NumField>> = {};
76
- ([["stiffness", "Stiffness", 1], ["damping", "Damping", 0.5], ["mass", "Mass", 0.1]] as const).forEach(([key, lab, step]) => {
77
- flds[key] = numField({ label: lab, value: phys[key], step, min: step }, (v) => { phys[key] = v; draw(); emit(); });
134
+ (
135
+ [
136
+ ["stiffness", "Stiffness", 1],
137
+ ["damping", "Damping", 0.5],
138
+ ["mass", "Mass", 0.1],
139
+ ] as const
140
+ ).forEach(([key, lab, step]) => {
141
+ flds[key] = numField({ label: tr(key, lab), value: phys[key], step, min: step }, (v) => {
142
+ phys[key] = v;
143
+ draw();
144
+ emit();
145
+ });
78
146
  physFields.append(flds[key].el);
79
147
  });
80
148
  // The field shows ms; the value stays Motion's visualDuration in seconds.
81
- const durFld = numField({ label: "Duration", value: time.visualDuration * 1000, step: 10, min: DUR_MIN * 1000, max: DUR_MAX * 1000, unit: "ms" }, (v) => { time.visualDuration = clampDur(v / 1000); draw(); emit(); });
82
- const bounceFld = numField({ label: "Bounce", value: time.bounce * 100, step: 5, min: -100, max: 100, unit: "%" }, (v) => { time.bounce = clampBounce(v / 100); draw(); emit(); }); // shown as a percentage; the value stays Motion's −1–1
149
+ const durFld = numField(
150
+ {
151
+ label: tr("duration", "Duration"),
152
+ value: time.visualDuration * 1000,
153
+ step: 10,
154
+ min: DUR_MIN * 1000,
155
+ max: DUR_MAX * 1000,
156
+ unit: "ms",
157
+ },
158
+ (v) => {
159
+ time.visualDuration = clampDur(v / 1000);
160
+ draw();
161
+ emit();
162
+ },
163
+ );
164
+ // shown as a percentage; the value stays Motion's −1–1
165
+ const bounceFld = numField(
166
+ { label: tr("bounce", "Bounce"), value: time.bounce * 100, step: 5, min: -100, max: 100, unit: "%" },
167
+ (v) => {
168
+ time.bounce = clampBounce(v / 100);
169
+ draw();
170
+ emit();
171
+ },
172
+ );
83
173
  timeFields.append(durFld.el, bounceFld.el);
84
174
 
85
- const showMode = () => { timeFields.style.display = mode === "time" ? "" : "none"; physFields.style.display = mode === "physics" ? "" : "none"; };
86
- const switchMode = (m: unknown) => { if ((m !== "time" && m !== "physics") || m === mode) return; mode = m; showMode(); draw(); emit(); };
87
- const modeToggle = createSegmented([{ value: "time", label: "Time" }, { value: "physics", label: "Physics" }], mode, switchMode, "Spring mode");
88
- const modeRow = el("div", "tw-row");
89
- modeRow.append(txt("span", "tw-row-label", "Mode"), modeToggle.el);
175
+ const showMode = () => {
176
+ timeFields.style.display = mode === "time" ? "" : "none";
177
+ physFields.style.display = mode === "physics" ? "" : "none";
178
+ };
179
+ const switchMode = (m: unknown) => {
180
+ if ((m !== "time" && m !== "physics") || m === mode) return;
181
+ mode = m;
182
+ showMode();
183
+ draw();
184
+ emit();
185
+ };
186
+ const modeToggle = createSegmented(
187
+ [
188
+ { value: "time", label: tr("time", "Time") },
189
+ { value: "physics", label: tr("physics", "Physics") },
190
+ ],
191
+ mode,
192
+ switchMode,
193
+ tr("springMode", "Spring mode"),
194
+ );
195
+ const modeRow = el("div", "twk-row");
196
+ modeRow.append(txt("span", "twk-row-label", tr("mode", "Mode")), modeToggle.el);
90
197
  // A caption, so two springs in one panel can be told apart and the filter matches shown text.
91
- if (meta.label) root.append(txt("div", "tw-spring-label", meta.label));
198
+ if (meta.label) root.append(txt("div", "twk-spring-label", meta.label));
92
199
  root.append(viz, modeRow, physFields, timeFields);
93
200
 
94
201
  // Drag the preview to tune by feel: horizontal sets stiffness or duration, vertical sets damping or bounce.
95
- const ST_MIN = 1, ST_MAX = 500, DA_MIN = 1, DA_MAX = 40, DRAG_DUR_MIN = 0.1, DRAG_DUR_MAX = 1; // the feel-tuning span; the fields reach further
202
+ // the feel-tuning span; the fields reach further
203
+ const ST_MIN = 1,
204
+ ST_MAX = 500,
205
+ DA_MIN = 1,
206
+ DA_MAX = 40,
207
+ DRAG_DUR_MIN = 0.1,
208
+ DRAG_DUR_MAX = 1;
96
209
  const fromPointer = (e: PointerEvent) => {
97
210
  const [px, py] = boxFrac(e, viz);
98
211
  // Set the field (it snaps and clamps), then read back, so readout, value and preview agree.
99
212
  if (mode === "time") {
100
- durFld.set((DRAG_DUR_MIN + px * (DRAG_DUR_MAX - DRAG_DUR_MIN)) * 1000); time.visualDuration = durFld.get() / 1000;
101
- bounceFld.set((1 - 2 * py) * 100); time.bounce = bounceFld.get() / 100; // top = bounce 100%, the middle 0, the bottom −100% (overdamped)
213
+ durFld.set((DRAG_DUR_MIN + px * (DRAG_DUR_MAX - DRAG_DUR_MIN)) * 1000);
214
+ time.visualDuration = durFld.get() / 1000;
215
+ // top = bounce 100%, the middle 0, the bottom −100% (overdamped)
216
+ bounceFld.set((1 - 2 * py) * 100);
217
+ time.bounce = bounceFld.get() / 100;
102
218
  } else {
103
- flds.stiffness.set(ST_MIN + px * (ST_MAX - ST_MIN)); phys.stiffness = flds.stiffness.get();
104
- flds.damping.set(DA_MIN + py * (DA_MAX - DA_MIN)); phys.damping = flds.damping.get(); // top = low damping
219
+ flds.stiffness.set(ST_MIN + px * (ST_MAX - ST_MIN));
220
+ phys.stiffness = flds.stiffness.get();
221
+ flds.damping.set(DA_MIN + py * (DA_MAX - DA_MIN));
222
+ phys.damping = flds.damping.get(); // top = low damping
105
223
  }
106
- draw(); emit();
224
+ draw();
225
+ emit();
107
226
  };
108
227
  dragGesture(viz, {
109
- onDown: (e) => { e.preventDefault(); viz.classList.add("is-dragging"); fromPointer(e); },
228
+ onDown: (e) => {
229
+ e.preventDefault();
230
+ viz.classList.add("is-dragging");
231
+ fromPointer(e);
232
+ },
110
233
  onMove: fromPointer,
111
234
  onEnd: () => viz.classList.remove("is-dragging"),
112
235
  });
113
236
  showMode();
114
237
  onReady(draw);
115
- // The canvas reads theme tokens at draw time: redraw on resize, on a tab page revealing it (tw-reflow; it built at
116
- // 0×0 while hidden), on an OS scheme flip and on a host re-theme — CSS re-themes the SVG controls, a canvas keeps a stale color.
117
- onLive(canvas, [[window, "resize"], [window, "tw-reflow"], [matchMedia("(prefers-color-scheme: dark)"), "change"], [window, "tw-retheme"]], draw);
118
- // A host flipping [data-tw-scheme] re-themes through CSS alone; the canvas needs a redraw. Let go once mounted and removed.
119
- let schemeObs: MutationObserver | null = null, wasMounted = false;
238
+ // The canvas reads theme tokens at draw time: redraw on resize, on a tab page revealing it (twk-reflow; it built at
239
+ // 0×0 while hidden), on an OS scheme flip and on a host re-theme — CSS re-themes the SVG controls, a canvas keeps a
240
+ // stale color.
241
+ onLive(
242
+ canvas,
243
+ [
244
+ [window, "resize"],
245
+ [window, "twk-reflow"],
246
+ [matchMedia("(prefers-color-scheme: dark)"), "change"],
247
+ [window, "twk-retheme"],
248
+ ],
249
+ draw,
250
+ );
251
+ // A host flipping [data-twk-scheme] re-themes through CSS alone; the canvas needs a redraw. Let go once mounted and
252
+ // removed.
253
+ // The redraw waits a frame, for a shadow panel's own observer to mirror the pin first.
254
+ let schemeObs: MutationObserver | null = null,
255
+ wasMounted = false;
120
256
  if (typeof MutationObserver === "function") {
121
257
  schemeObs = new MutationObserver(() => {
122
- if (canvas.isConnected) { wasMounted = true; draw(); }
123
- else if (wasMounted) { schemeObs.disconnect(); schemeObs = null; }
258
+ if (canvas.isConnected) {
259
+ wasMounted = true;
260
+ requestAnimationFrame(draw);
261
+ } else if (wasMounted) {
262
+ schemeObs.disconnect();
263
+ schemeObs = null;
264
+ }
265
+ });
266
+ schemeObs.observe(document.documentElement, {
267
+ subtree: true,
268
+ attributes: true,
269
+ attributeFilter: ["data-twk-scheme"],
124
270
  });
125
- schemeObs.observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ["data-tw-scheme"] });
126
271
  }
127
272
  return {
128
273
  el: root,
129
- destroy: () => { if (schemeObs) { schemeObs.disconnect(); schemeObs = null; } },
274
+ destroy: () => {
275
+ if (schemeObs) {
276
+ schemeObs.disconnect();
277
+ schemeObs = null;
278
+ }
279
+ },
130
280
  // Silent set/restore: infer the mode from the keys, update caches and fields, redraw without emitting.
131
281
  set: (v: SpringValue) => {
132
282
  if (!v || typeof v !== "object") return;
133
- const ex = explicit(v.mode), hasTime = has(v.visualDuration) || has(v.bounce), hasPhys = has(v.stiffness) || has(v.damping) || has(v.mass);
283
+ const ex = explicit(v.mode),
284
+ hasTime = has(v.visualDuration) || has(v.bounce),
285
+ hasPhys = has(v.stiffness) || has(v.damping) || has(v.mass);
134
286
  if (!hasTime && !hasPhys && !ex) return;
135
287
  mode = ex ?? (hasTime ? "time" : "physics"); // an explicit mode wins; else time, the authoring mode
136
- if (hasTime) { if (has(v.visualDuration)) time.visualDuration = clampDur(v.visualDuration); if (has(v.bounce)) time.bounce = clampBounce(v.bounce); }
137
- // Restore the physics cache even alongside time keys (reset's default carries both), so a later toggle to Physics shows the intended spring.
288
+ if (hasTime) {
289
+ if (has(v.visualDuration)) time.visualDuration = clampDur(v.visualDuration);
290
+ if (has(v.bounce)) time.bounce = clampBounce(v.bounce);
291
+ }
292
+ // Restore the physics cache even alongside time keys (reset's default carries both), so a later toggle to Physics
293
+ // shows the intended spring.
138
294
  if (hasPhys) phys = clampS({ ...phys, ...v });
139
295
  modeToggle.set(mode);
140
- durFld.set(time.visualDuration * 1000); bounceFld.set(time.bounce * 100);
141
- flds.stiffness.set(phys.stiffness); flds.damping.set(phys.damping); flds.mass.set(phys.mass);
142
- showMode(); draw();
296
+ durFld.set(time.visualDuration * 1000);
297
+ bounceFld.set(time.bounce * 100);
298
+ flds.stiffness.set(phys.stiffness);
299
+ flds.damping.set(phys.damping);
300
+ flds.mass.set(phys.mass);
301
+ showMode();
302
+ draw();
143
303
  },
144
304
  get: () => resolvedValue(),
145
305
  };