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
@@ -1,5 +1,5 @@
1
1
  // ── Plot — graph y=f(x) with a safe expression evaluator (compileExpr). Lazy.
2
- import { el, txt, clamp, onReady, onLive, quietFocus, registerControl } from "../shared.js";
2
+ import { el, txt, clamp, onReady, onLive, quietFocus, registerControl, words } from "../shared.js";
3
3
  import { svgEl } from "../heavy.js";
4
4
  import type { OnChange, Built } from "../shared.js";
5
5
  import type { Meta } from "../schema.js";
@@ -13,105 +13,224 @@ type Tok = { t: "num"; v: number } | { t: "id" | "op"; v: string } | { t: "lp" |
13
13
  // ── A safe expression evaluator: recursive descent, not eval/Function, so a typed formula does only arithmetic and
14
14
  // whitelisted Math calls. Null-prototype tables, so "constructor" can't parse as an identifier. ──
15
15
  const PLOT_FUNCS: Record<string, (...args: number[]) => number> = Object.assign(Object.create(null), {
16
- sin: Math.sin, cos: Math.cos, tan: Math.tan, asin: Math.asin, acos: Math.acos,
17
- atan: Math.atan, atan2: Math.atan2, sinh: Math.sinh, cosh: Math.cosh, tanh: Math.tanh,
18
- sqrt: Math.sqrt, cbrt: Math.cbrt, abs: Math.abs, exp: Math.exp, log: Math.log, ln: Math.log,
19
- log10: Math.log10, log2: Math.log2, floor: Math.floor, ceil: Math.ceil, round: Math.round,
20
- sign: Math.sign, trunc: Math.trunc, min: Math.min, max: Math.max, pow: Math.pow, hypot: Math.hypot,
21
- mod: (a: number, b: number) => a % b, clamp: (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi),
16
+ sin: Math.sin,
17
+ cos: Math.cos,
18
+ tan: Math.tan,
19
+ asin: Math.asin,
20
+ acos: Math.acos,
21
+ atan: Math.atan,
22
+ atan2: Math.atan2,
23
+ sinh: Math.sinh,
24
+ cosh: Math.cosh,
25
+ tanh: Math.tanh,
26
+ sqrt: Math.sqrt,
27
+ cbrt: Math.cbrt,
28
+ abs: Math.abs,
29
+ exp: Math.exp,
30
+ log: Math.log,
31
+ ln: Math.log,
32
+ log10: Math.log10,
33
+ log2: Math.log2,
34
+ floor: Math.floor,
35
+ ceil: Math.ceil,
36
+ round: Math.round,
37
+ sign: Math.sign,
38
+ trunc: Math.trunc,
39
+ min: Math.min,
40
+ max: Math.max,
41
+ pow: Math.pow,
42
+ hypot: Math.hypot,
43
+ mod: (a: number, b: number) => a % b,
44
+ clamp: (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi),
45
+ });
46
+ // called empty these return a number (Infinity, 0), so a slip drew nothing and flagged nothing
47
+ const PLOT_VARIADIC = ["min", "max", "hypot"];
48
+ const PLOT_CONSTS: Record<string, number> = Object.assign(Object.create(null), {
49
+ pi: Math.PI,
50
+ e: Math.E,
51
+ tau: Math.PI * 2,
52
+ phi: (1 + Math.sqrt(5)) / 2,
22
53
  });
23
- const PLOT_VARIADIC = ["min", "max", "hypot"]; // called empty these return a number (Infinity, 0), so a slip drew nothing and flagged nothing
24
- const PLOT_CONSTS: Record<string, number> = Object.assign(Object.create(null), { pi: Math.PI, e: Math.E, tau: Math.PI * 2, phi: (1 + Math.sqrt(5)) / 2 });
25
54
  /** The compiled expression, or null on any parse error. */
26
55
  export function compileExpr(src: unknown): Expr | null {
27
- const r = parseExpr(src); return typeof r === "function" ? r : null;
56
+ const r = parseExpr(src);
57
+ return typeof r === "function" ? r : null;
28
58
  }
29
59
  /** The compiled expression, or why it doesn't parse; the reason names the fix where there is one ("write sin(…)"). */
30
60
  export function parseExpr(src: unknown): Expr | string {
31
61
  if (typeof src !== "string" || !src.trim()) return "type an expression in x";
32
- if (src.length > 512) return "too long: 512 characters at most"; // also bounds recursion depth, so no RangeError mid-parse
33
- const s = src, toks: Tok[] = [];
34
- // `5.` reads as 5, as `.5` reads as 0.5. An e is an exponent only with a digit right after it (and its sign): `2e+1` is
35
- // 20, while `2e+x` is the constant, 2·e + x, as `2e` alone is 2·e. Scientific notation never has a name for its power.
36
- const numRe = /(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y, idRe = /[A-Za-z_]\w*/y;
62
+ // also bounds recursion depth, so no RangeError mid-parse
63
+ if (src.length > 512) return "too long: 512 characters at most";
64
+ const s = src,
65
+ toks: Tok[] = [];
66
+ // `5.` reads as 5, as `.5` reads as 0.5. An e is an exponent only with a digit right after it (and its sign): `2e+1`
67
+ // is 20, while `2e+x` is the constant, 2·e + x, as `2e` alone is 2·e. Scientific notation never has a name for its
68
+ // power.
69
+ const numRe = /(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y,
70
+ idRe = /[A-Za-z_]\w*/y;
37
71
  let i = 0;
38
72
  while (i < s.length) {
39
73
  const ch = s[i];
40
- if (ch === " " || ch === "\t" || ch === "\n") { i++; continue; }
74
+ if (ch === " " || ch === "\t" || ch === "\n") {
75
+ i++;
76
+ continue;
77
+ }
41
78
  if ((ch >= "0" && ch <= "9") || (ch === "." && s[i + 1] >= "0" && s[i + 1] <= "9")) {
42
- numRe.lastIndex = i; const m = numRe.exec(s); if (!m || m.index !== i) return `unexpected "${ch}"`;
43
- toks.push({ t: "num", v: parseFloat(m[0]) }); i = numRe.lastIndex; continue;
79
+ numRe.lastIndex = i;
80
+ const m = numRe.exec(s);
81
+ if (!m || m.index !== i) return `unexpected "${ch}"`;
82
+ toks.push({ t: "num", v: parseFloat(m[0]) });
83
+ i = numRe.lastIndex;
84
+ continue;
44
85
  }
45
86
  if ((ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch === "_") {
46
- idRe.lastIndex = i; const m = idRe.exec(s); toks.push({ t: "id", v: m[0] }); i = idRe.lastIndex; continue;
87
+ idRe.lastIndex = i;
88
+ const m = idRe.exec(s);
89
+ toks.push({ t: "id", v: m[0] });
90
+ i = idRe.lastIndex;
91
+ continue;
92
+ }
93
+ if (ch === "*" && s[i + 1] === "*") {
94
+ toks.push({ t: "op", v: "^" });
95
+ i += 2;
96
+ continue;
97
+ }
98
+ if ("+-*/%^".includes(ch)) {
99
+ toks.push({ t: "op", v: ch });
100
+ i++;
101
+ continue;
102
+ }
103
+ if (ch === "(") {
104
+ toks.push({ t: "lp" });
105
+ i++;
106
+ continue;
107
+ }
108
+ if (ch === ")") {
109
+ toks.push({ t: "rp" });
110
+ i++;
111
+ continue;
112
+ }
113
+ if (ch === ",") {
114
+ toks.push({ t: "comma" });
115
+ i++;
116
+ continue;
47
117
  }
48
- if (ch === "*" && s[i + 1] === "*") { toks.push({ t: "op", v: "^" }); i += 2; continue; }
49
- if ("+-*/%^".includes(ch)) { toks.push({ t: "op", v: ch }); i++; continue; }
50
- if (ch === "(") { toks.push({ t: "lp" }); i++; continue; }
51
- if (ch === ")") { toks.push({ t: "rp" }); i++; continue; }
52
- if (ch === ",") { toks.push({ t: "comma" }); i++; continue; }
53
118
  return `unexpected "${ch}"`;
54
119
  }
55
- let p = 0, why = "";
56
- const fail = (m: string): null => { why = why || m; return null; }; // the innermost reason wins; every caller unwinds on null
120
+ let p = 0,
121
+ why = "";
122
+ // the innermost reason wins; every caller unwinds on null
123
+ const fail = (m: string): null => {
124
+ why = why || m;
125
+ return null;
126
+ };
57
127
  const peek = () => toks[p];
58
- const eat = (t?: Tok["t"]) => { const tok = toks[p]; if (!tok || (t && tok.t !== t)) return null; p++; return tok; };
128
+ const eat = (t?: Tok["t"]) => {
129
+ const tok = toks[p];
130
+ if (!tok || (t && tok.t !== t)) return null;
131
+ p++;
132
+ return tok;
133
+ };
59
134
  const add = (): Expr | null => {
60
- let a = mul(); if (!a) return null;
135
+ let a = mul();
136
+ if (!a) return null;
61
137
  while (peek() && peek().t === "op" && (peek().v === "+" || peek().v === "-")) {
62
- const op = eat("op").v, b = mul(); if (!b) return null; const aa = a;
138
+ const op = eat("op").v,
139
+ b = mul();
140
+ if (!b) return null;
141
+ const aa = a;
63
142
  a = op === "+" ? (x) => aa(x) + b(x) : (x) => aa(x) - b(x);
64
143
  }
65
144
  return a;
66
145
  };
67
146
  const mul = (): Expr | null => {
68
- let a = unary(); if (!a) return null;
147
+ let a = unary();
148
+ if (!a) return null;
69
149
  for (let t = peek(); t; t = peek()) {
70
- // `2x`, `2pi`, `3(x+1)`, `pi x`, `3x(x)`, `(x)(x)`, `sin(x)cos(x)`: a number, x, a constant or ")" directly before
71
- // a name or "(" multiplies, as on paper. x and the constants are never functions, so `x(2)` is 2x; `xy` stays one
72
- // (unknown) name, and a number after an operand (`2 3`, `(x)2`) asks for an operator. It binds as `*` does: `2x^2` is 2·x².
73
- const prev = toks[p - 1].t, implied = (prev === "num" || prev === "id" || prev === "rp") && (t.t === "id" || t.t === "lp");
150
+ // `2x`, `2pi`, `3(x+1)`, `pi x`, `3x(x)`, `(x)(x)`, `sin(x)cos(x)`: a number, x, a constant or ")" directly
151
+ // before a name or "(" multiplies, as on paper. x and the constants are never functions, so `x(2)` is 2x; `xy`
152
+ // stays one (unknown) name, and a number after an operand (`2 3`, `(x)2`) asks for an operator. It binds as `*`
153
+ // does: `2x^2` is 2·x².
154
+ const prev = toks[p - 1].t,
155
+ implied = (prev === "num" || prev === "id" || prev === "rp") && (t.t === "id" || t.t === "lp");
74
156
  if (!implied && !(t.t === "op" && (t.v === "*" || t.v === "/" || t.v === "%"))) break;
75
- const op = implied ? "*" : eat("op").v, b = unary(); if (!b) return null; const aa = a;
157
+ const op = implied ? "*" : eat("op").v,
158
+ b = unary();
159
+ if (!b) return null;
160
+ const aa = a;
76
161
  a = op === "*" ? (x) => aa(x) * b(x) : op === "/" ? (x) => aa(x) / b(x) : (x) => aa(x) % b(x);
77
162
  }
78
163
  return a;
79
164
  };
80
165
  const unary = (): Expr | null => {
81
166
  if (peek() && peek().t === "op" && (peek().v === "-" || peek().v === "+")) {
82
- const op = eat("op").v, a = unary(); if (!a) return null;
167
+ const op = eat("op").v,
168
+ a = unary();
169
+ if (!a) return null;
83
170
  return op === "-" ? (x) => -a(x) : a;
84
171
  }
85
172
  return pow();
86
173
  };
87
174
  const pow = (): Expr | null => {
88
- const a = atom(); if (!a) return null;
89
- if (peek() && peek().t === "op" && peek().v === "^") { eat("op"); const b = unary(); if (!b) return null; return (x) => Math.pow(a(x), b(x)); }
175
+ const a = atom();
176
+ if (!a) return null;
177
+ if (peek() && peek().t === "op" && peek().v === "^") {
178
+ eat("op");
179
+ const b = unary();
180
+ if (!b) return null;
181
+ return (x) => Math.pow(a(x), b(x));
182
+ }
90
183
  return a;
91
184
  };
92
185
  const atom = (): Expr | null => {
93
- const tok = peek(); if (!tok) return null;
94
- if (tok.t === "num") { eat("num"); return () => tok.v; }
95
- if (tok.t === "lp") { eat("lp"); const e = add(); if (!e || !eat("rp")) return null; return e; }
186
+ const tok = peek();
187
+ if (!tok) return null;
188
+ if (tok.t === "num") {
189
+ eat("num");
190
+ return () => tok.v;
191
+ }
192
+ if (tok.t === "lp") {
193
+ eat("lp");
194
+ const e = add();
195
+ if (!e || !eat("rp")) return null;
196
+ return e;
197
+ }
96
198
  if (tok.t === "id") {
97
- eat("id"); const name = tok.v.toLowerCase(), fn = PLOT_FUNCS[name];
98
- if (peek() && peek().t === "lp" && (fn || (name !== "x" && !(name in PLOT_CONSTS)))) { // `x(2)`: a product, in mul()
199
+ eat("id");
200
+ const name = tok.v.toLowerCase(),
201
+ fn = PLOT_FUNCS[name];
202
+ // `x(2)`: a product, in mul()
203
+ if (peek() && peek().t === "lp" && (fn || (name !== "x" && !(name in PLOT_CONSTS)))) {
99
204
  if (!fn) return fail(`unknown function "${tok.v}"`);
100
- eat("lp"); const argv: Expr[] = [];
101
- if (peek() && peek().t !== "rp") { do { const a = add(); if (!a) return null; argv.push(a); } while (eat("comma")); }
205
+ eat("lp");
206
+ const argv: Expr[] = [];
207
+ if (peek() && peek().t !== "rp") {
208
+ do {
209
+ const a = add();
210
+ if (!a) return null;
211
+ argv.push(a);
212
+ } while (eat("comma"));
213
+ }
102
214
  if (!eat("rp")) return null;
103
215
  if (!argv.length && PLOT_VARIADIC.includes(name)) return fail(`${name}() needs at least one value`);
104
216
  return (x) => fn(...argv.map((a) => a(x)));
105
217
  }
106
218
  if (fn) {
107
- // `sin x`, `cos 2x`: a one-argument function takes a bare x, constant or number-times-name, as a calculator does.
108
- // Anything longer (`sin x^2`, `sin -x`, `sin 2 cos x`) reads two ways, so it asks for the parentheses instead of guessing.
109
- const a = fn.length === 1 ? bare() : null, n = peek();
110
- if (!a || (n && (n.t === "op" ? n.v === "^" : n.t !== "rp" && n.t !== "comma"))) return fail(`write ${name}(…)`);
219
+ // `sin x`, `cos 2x`: a one-argument function takes a bare x, constant or number-times-name, as a calculator
220
+ // does.
221
+ // Anything longer (`sin x^2`, `sin -x`, `sin 2 cos x`) reads two ways, so it asks for the parentheses instead
222
+ // of guessing.
223
+ const a = fn.length === 1 ? bare() : null,
224
+ n = peek();
225
+ if (!a || (n && (n.t === "op" ? n.v === "^" : n.t !== "rp" && n.t !== "comma")))
226
+ return fail(`write ${name}(…)`);
111
227
  return (x) => fn(a(x));
112
228
  }
113
229
  if (name === "x") return (x) => x;
114
- if (name in PLOT_CONSTS) { const c = PLOT_CONSTS[name]; return () => c; }
230
+ if (name in PLOT_CONSTS) {
231
+ const c = PLOT_CONSTS[name];
232
+ return () => c;
233
+ }
115
234
  return fail(`unknown name "${tok.v}"`); // an identifier that isn't x, a constant, or a known function
116
235
  }
117
236
  return null;
@@ -119,102 +238,203 @@ export function parseExpr(src: unknown): Expr | string {
119
238
  const named = (t: Tok) => t && t.t === "id" && !PLOT_FUNCS[t.v.toLowerCase()];
120
239
  const bare = (): Expr | null => {
121
240
  const t = peek();
122
- if (t && t.t === "num") { eat("num"); if (!named(peek())) return () => t.v; const b = atom(); return b && ((x) => t.v * b(x)); }
241
+ if (t && t.t === "num") {
242
+ eat("num");
243
+ if (!named(peek())) return () => t.v;
244
+ const b = atom();
245
+ return b && ((x) => t.v * b(x));
246
+ }
123
247
  return named(t) ? atom() : null;
124
248
  };
125
- const tree = add(), left = toks[p];
249
+ const tree = add(),
250
+ left = toks[p];
126
251
  if (tree && !left) return tree;
127
252
  if (why) return why;
128
253
  // A parse that stopped short, or tokens left over: say where.
129
- const last = toks[p - 1], unclosed = toks.slice(0, p).reduce((d, t) => d + (t.t === "lp" ? 1 : t.t === "rp" ? -1 : 0), 0);
254
+ const last = toks[p - 1],
255
+ unclosed = toks.slice(0, p).reduce((d, t) => d + (t.t === "lp" ? 1 : t.t === "rp" ? -1 : 0), 0);
130
256
  if (!tree && last && last.t === "op") return `"${last.v}" needs a value on each side`;
131
257
  if (!left) return unclosed > 0 ? 'missing ")"' : "not a valid expression";
132
- return left.t === "num" ? `missing an operator before ${left.v}`
133
- : left.t === "rp" ? (last && last.t === "lp" ? "nothing inside the parentheses" : 'unmatched ")"')
134
- : left.t === "comma" ? '"," outside a function\'s parentheses'
135
- : left.t === "op" ? `"${left.v}" needs a value on each side` : "not a valid expression";
258
+ return left.t === "num"
259
+ ? `missing an operator before ${left.v}`
260
+ : left.t === "rp"
261
+ ? last && last.t === "lp"
262
+ ? "nothing inside the parentheses"
263
+ : 'unmatched ")"'
264
+ : left.t === "comma"
265
+ ? '"," outside a function\'s parentheses'
266
+ : left.t === "op"
267
+ ? `"${left.v}" needs a value on each side`
268
+ : "not a valid expression";
136
269
  }
137
270
 
138
271
  // ── Plot — y = f(x) across a range with an editable expression, or a fixed `fn`. Y auto-ranges unless yMin/yMax are
139
272
  // given; the line breaks across asymptotes. ──
140
273
  function createPlot(meta: Meta, onChange: OnChange): Control {
141
- // Non-finite bounds take the defaults, an inverted pair swaps, an equal pair pads out (÷0 drew nothing); a degenerate y-range falls back to auto.
142
- let xMin = Number(meta.xMin ?? meta.min), xMax = Number(meta.xMax ?? meta.max); // min/max are the verbose form's aliases
274
+ const tr = words();
275
+ // Non-finite bounds take the defaults, an inverted pair swaps, an equal pair pads out (÷0 drew nothing); a degenerate
276
+ // y-range falls back to auto.
277
+ // min/max are the verbose form's aliases
278
+ let xMin = Number(meta.xMin ?? meta.min),
279
+ xMax = Number(meta.xMax ?? meta.max);
143
280
  if (!Number.isFinite(xMin)) xMin = -10;
144
281
  if (!Number.isFinite(xMax)) xMax = 10;
145
- if (xMax < xMin) { const t = xMin; xMin = xMax; xMax = t; }
146
- if (xMax === xMin) { xMin -= 1; xMax += 1; }
147
- const samples = Math.max(2, Math.min(4096, (meta.samples | 0) || 256)); // capped: 1e8 samples per keystroke froze the tab
282
+ if (xMax < xMin) {
283
+ const t = xMin;
284
+ xMin = xMax;
285
+ xMax = t;
286
+ }
287
+ if (xMax === xMin) {
288
+ xMin -= 1;
289
+ xMax += 1;
290
+ }
291
+ // capped: 1e8 samples per keystroke froze the tab
292
+ const samples = Math.max(2, Math.min(4096, meta.samples | 0 || 256));
148
293
  const fixedY = Number.isFinite(meta.yMin) && Number.isFinite(meta.yMax) && +meta.yMax > +meta.yMin;
149
294
  const fn = typeof meta.fn === "function" ? meta.fn : null;
150
295
  const editable = meta.editable !== false && !fn;
151
296
  let expr = meta.expr != null ? String(meta.expr) : fn ? "" : "sin(x)"; // a sine to start from
152
297
  let compiled = fn || parseExpr(expr); // a string is the reason it doesn't parse
153
298
 
154
- const root = el("div", "tw-plot");
155
- if (meta.label) root.append(txt("div", "tw-plot-label", meta.label));
156
- const graph = el("div", "tw-plot-graph");
157
- const svg = svgEl("svg", "tw-plot-svg"); svg.setAttribute("preserveAspectRatio", "none");
158
- const axisX = svgEl("line", "tw-plot-axis"), axisY = svgEl("line", "tw-plot-axis"), curve = svgEl("path", "tw-plot-curve");
159
- svg.append(axisX, axisY, curve); graph.append(svg); root.append(graph);
299
+ const root = el("div", "twk-plot");
300
+ if (meta.label) root.append(txt("div", "twk-plot-label", meta.label));
301
+ const graph = el("div", "twk-plot-graph");
302
+ const svg = svgEl("svg", "twk-plot-svg");
303
+ svg.setAttribute("preserveAspectRatio", "none");
304
+ const axisX = svgEl("line", "twk-plot-axis"),
305
+ axisY = svgEl("line", "twk-plot-axis"),
306
+ curve = svgEl("path", "twk-plot-curve");
307
+ svg.append(axisX, axisY, curve);
308
+ graph.append(svg);
309
+ root.append(graph);
160
310
 
161
311
  let input: HTMLInputElement | null = null;
162
312
  if (editable) {
163
- const field = el("div", "tw-plot-field");
164
- input = el("input", "tw-plot-input"); input.type = "text"; input.value = expr; input.spellcheck = false;
165
- input.autocapitalize = "off"; quietFocus(input); /* click-to-edit stays ringless; Tab rings the field */ input.setAttribute("aria-label", `${meta.label || "Plot"} — expression in x`); input.setAttribute("aria-invalid", "false");
166
- field.append(txt("span", "tw-plot-fx", "y ="), input); root.append(field); // txt, not el(…, html): el's third arg is innerHTML
313
+ const field = el("div", "twk-plot-field");
314
+ input = el("input", "twk-plot-input");
315
+ input.type = "text";
316
+ input.value = expr;
317
+ input.spellcheck = false;
318
+ input.autocapitalize = "off";
319
+ quietFocus(input);
320
+ /* click-to-edit stays ringless; Tab rings the field */ input.setAttribute(
321
+ "aria-label",
322
+ tr("expression", "{label} — expression in x", { label: meta.label || tr("plot", "Plot") }),
323
+ );
324
+ input.setAttribute("aria-invalid", "false");
325
+ // txt, not el(…, html): el's third arg is innerHTML
326
+ field.append(txt("span", "twk-plot-fx", "y ="), input);
327
+ root.append(field);
167
328
  }
168
329
 
169
330
  const PAD = 6;
170
331
  const draw = () => {
171
332
  // Flag invalid before the size guard, so a hidden tab page still reports a bad expression.
172
- const f = typeof compiled === "function" ? compiled : null, ok = !!f;
333
+ const f = typeof compiled === "function" ? compiled : null,
334
+ ok = !!f;
173
335
  root.classList.toggle("is-invalid", editable && !ok);
174
- if (input) { input.setAttribute("aria-invalid", ok ? "false" : "true"); input.title = ok ? "" : String(compiled); } // the title says why, and names the fix
175
- const r = graph.getBoundingClientRect(), W = r.width, H = r.height; if (W < 2) return;
336
+ // the title says why, and names the fix
337
+ if (input) {
338
+ input.setAttribute("aria-invalid", ok ? "false" : "true");
339
+ input.title = ok ? "" : String(compiled);
340
+ }
341
+ const r = graph.getBoundingClientRect(),
342
+ W = r.width,
343
+ H = r.height;
344
+ if (W < 2) return;
176
345
  svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
177
- const ys: number[] = []; let yLo = Infinity, yHi = -Infinity;
178
- if (ok) for (let k = 0; k < samples; k++) {
179
- const x = xMin + (xMax - xMin) * (k / (samples - 1));
180
- let y: number; try { y = f(x); } catch { y = NaN; }
181
- ys.push(y);
182
- if (Number.isFinite(y)) { if (y < yLo) yLo = y; if (y > yHi) yHi = y; }
183
- }
184
- let lo = meta.yMin, hi = meta.yMax;
346
+ const ys: number[] = [];
347
+ let yLo = Infinity,
348
+ yHi = -Infinity;
349
+ if (ok)
350
+ for (let k = 0; k < samples; k++) {
351
+ const x = xMin + (xMax - xMin) * (k / (samples - 1));
352
+ let y: number;
353
+ try {
354
+ y = f(x);
355
+ } catch {
356
+ y = NaN;
357
+ }
358
+ ys.push(y);
359
+ if (Number.isFinite(y)) {
360
+ if (y < yLo) yLo = y;
361
+ if (y > yHi) yHi = y;
362
+ }
363
+ }
364
+ let lo = meta.yMin,
365
+ hi = meta.yMax;
185
366
  if (!fixedY) {
186
- if (!Number.isFinite(yLo) || yLo === yHi) { const c = Number.isFinite(yLo) ? yLo : 0; lo = c - 1; hi = c + 1; }
187
- else { const pad = (yHi - yLo) * 0.12; lo = yLo - pad; hi = yHi + pad; }
367
+ if (!Number.isFinite(yLo) || yLo === yHi) {
368
+ const c = Number.isFinite(yLo) ? yLo : 0;
369
+ lo = c - 1;
370
+ hi = c + 1;
371
+ } else {
372
+ const pad = (yHi - yLo) * 0.12;
373
+ lo = yLo - pad;
374
+ hi = yHi + pad;
375
+ }
188
376
  }
189
377
  const xPx = (x: number) => PAD + ((x - xMin) / (xMax - xMin)) * (W - 2 * PAD);
190
- const yPx = (y: number) => (H - PAD) - ((y - lo) / (hi - lo)) * (H - 2 * PAD);
191
- const axis = (ln: Built<SVGLineElement>, ax: number, ay: number, bx: number, by: number, show: boolean) => { ln.style.display = show ? "" : "none"; if (show) { ln.setAttribute("x1", ax); ln.setAttribute("y1", ay); ln.setAttribute("x2", bx); ln.setAttribute("y2", by); } };
378
+ const yPx = (y: number) => H - PAD - ((y - lo) / (hi - lo)) * (H - 2 * PAD);
379
+ const axis = (ln: Built<SVGLineElement>, ax: number, ay: number, bx: number, by: number, show: boolean) => {
380
+ ln.style.display = show ? "" : "none";
381
+ if (show) {
382
+ ln.setAttribute("x1", ax);
383
+ ln.setAttribute("y1", ay);
384
+ ln.setAttribute("x2", bx);
385
+ ln.setAttribute("y2", by);
386
+ }
387
+ };
192
388
  axis(axisX, PAD, yPx(0), W - PAD, yPx(0), lo <= 0 && hi >= 0);
193
389
  axis(axisY, xPx(0), PAD, xPx(0), H - PAD, xMin <= 0 && xMax >= 0);
194
- let d = "", pen = false;
195
- if (ok) for (let k = 0; k < samples; k++) {
196
- const y = ys[k];
197
- if (!Number.isFinite(y)) { pen = false; continue; }
198
- const px = xPx(xMin + (xMax - xMin) * (k / (samples - 1))), py = clamp(yPx(y), -1e4, 1e4);
199
- d += (pen ? "L" : "M") + px.toFixed(1) + "," + py.toFixed(1) + " "; pen = true;
200
- }
390
+ let d = "",
391
+ pen = false;
392
+ if (ok)
393
+ for (let k = 0; k < samples; k++) {
394
+ const y = ys[k];
395
+ if (!Number.isFinite(y)) {
396
+ pen = false;
397
+ continue;
398
+ }
399
+ const px = xPx(xMin + (xMax - xMin) * (k / (samples - 1))),
400
+ py = clamp(yPx(y), -1e4, 1e4);
401
+ d += (pen ? "L" : "M") + px.toFixed(1) + "," + py.toFixed(1) + " ";
402
+ pen = true;
403
+ }
201
404
  curve.setAttribute("d", d);
202
405
  };
203
406
 
204
- if (editable) input.addEventListener("input", () => {
205
- expr = input.value; compiled = parseExpr(expr); // a reason string on a parse error → draw flags is-invalid
206
- draw(); onChange(expr);
207
- });
407
+ if (editable)
408
+ input.addEventListener("input", () => {
409
+ expr = input.value;
410
+ compiled = parseExpr(expr); // a reason string on a parse error → draw flags is-invalid
411
+ draw();
412
+ onChange(expr);
413
+ });
208
414
 
209
415
  onReady(draw);
210
- onLive(root, [[window, "resize"], [window, "tw-reflow"]], draw); // tw-reflow: a tab page revealing this re-measures it
416
+ // twk-reflow: a tab page revealing this re-measures it
417
+ onLive(
418
+ root,
419
+ [
420
+ [window, "resize"],
421
+ [window, "twk-reflow"],
422
+ ],
423
+ draw,
424
+ );
211
425
 
212
426
  return {
213
427
  el: root,
214
428
  get: () => expr,
215
- set: (v: unknown) => { if (v == null) return; expr = String(v); if (input) input.value = expr; if (!fn) compiled = parseExpr(expr); draw(); }, // an invalid expression flags, as typing does
429
+ // an invalid expression flags, as typing does
430
+ set: (v: unknown) => {
431
+ if (v == null) return;
432
+ expr = String(v);
433
+ if (input) input.value = expr;
434
+ if (!fn) compiled = parseExpr(expr);
435
+ draw();
436
+ },
216
437
  };
217
438
  }
218
439
 
219
440
  registerControl("plot", createPlot);
220
-