Skip to content

Commit effe953

Browse files
committed
doc
1 parent d8fea8d commit effe953

1 file changed

Lines changed: 96 additions & 218 deletions

File tree

CHANGELOG.md

Lines changed: 96 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,86 @@
1-
### Unreleased
1+
### 0.56.0 _2026-03-10_
22

33
#### Added
44

5-
- **Function-style aliases for collection / arithmetic operators**
6-
the lowercase `\operatorname{...}` forms common in Desmos-style and
7-
procedural notations now parse to their existing capitalized
8-
counterparts:
9-
- `\operatorname{mod}(a, b)``Mod`
10-
- `\operatorname{var}(L)``Variance`
11-
- `\operatorname{shuffle}(L)``Shuffle`
12-
- `\operatorname{random}()``Random`
13-
- `\operatorname{repeat}(x)``Repeat`
14-
- `\operatorname{join}(L1, L2, ...)``Join`
15-
No semantic change — the operators themselves were already
16-
registered, just under their capitalized names.
17-
18-
- **`Distance(p1, p2)`** — Euclidean distance between two points
19-
represented as tuples. Accepts any positive dimension; mismatched
20-
dimensions return a typed error. LaTeX trigger
21-
`\operatorname{distance}(p1, p2)`.
22-
23-
- **Geometric primitive heads**`Triangle`, `Sphere`, `Segment`
24-
registered as opaque typed function heads (signature only, no
25-
evaluator). Parsed but preserved structurally; consumers branch on
26-
the operator name. LaTeX triggers `\operatorname{triangle}`,
27-
`\operatorname{sphere}`, `\operatorname{segment}`. Same shape as the
28-
existing `["To", a, b]` pattern.
29-
30-
- **`To` library entry**`\to` already parsed to `["To", a, b]`,
31-
but the head wasn't in the standard operator set, so rows containing
32-
it were classified as `unsupported-operator`. The library entry
33-
declares the head's signature (no evaluator) so the classification
34-
reflects reality: this is a known typed action node.
35-
36-
- **`GeometricVector` head**`\operatorname{vector}(p1, p2)` (Desmos's
37-
geometric form: a directed segment between two points) parses to a
38-
new opaque typed head. Distinct from the existing `Vector` operator
39-
for column-vector construction (`(number+) -> vector` signature), so
40-
aliasing the LaTeX trigger to it would have either constrained
41-
Desmos's input shape or forced a signature widening. New head, no
42-
evaluator — same shape as `Triangle`/`Sphere`/`Segment`.
43-
44-
- **First-class color values** — colors are now typed values with their
45-
own primitive type (`color`) and per-colorspace constructor heads,
46-
rather than anonymous tuples.
47-
48-
- **Constructor heads** (one per colorspace): `Rgb`, `Hsv`, `Hsl`,
49-
`Oklab`, `Oklch`. Each takes 3 components plus an optional 4th
50-
alpha argument. Components are interpreted per the colorspace's
51-
own conventions — `Rgb` channels 0–1 sRGB (no parse-time clamp),
52-
HSV/HSL hue in degrees with saturation/value/lightness 0–1,
53-
Oklab/Oklch with their standard ranges. The operator name
54-
preserves the colorspace through evaluation.
55-
5+
- **First-class color values** — colors are now typed values with a dedicated
6+
`color` primitive type and per-colorspace constructor heads, rather than
7+
anonymous tuples.
8+
- **Constructor heads**: `Rgb`, `Hsv`, `Hsl`, `Oklab`, `Oklch`. Each takes 3
9+
components plus an optional alpha. Channels follow each colorspace's own
10+
conventions (RGB: 0–1 sRGB; HSV/HSL: hue in degrees, S/V/L 0–1; Oklab/Oklch:
11+
standard ranges).
5612
- **LaTeX**: `\operatorname{rgb}(...)`, `\operatorname{hsv}(...)`,
5713
`\operatorname{hsl}(...)`, `\operatorname{oklab}(...)`,
58-
`\operatorname{oklch}(...)`. 1:1 with the heads, both directions.
59-
60-
- **Conversions**: `AsRgb`, `AsHsv`, `AsHsl`, `AsOklab`, `AsOklch`
61-
convert any color to the named space (identity if already there).
62-
Oklab↔Oklch routes directly via polar/cartesian conversion;
63-
sRGB-based spaces go through RGB.
64-
65-
- **`ColorDelta(a, b)`** — perceptual color difference (ΔE_OK,
66-
Euclidean distance in OKLab). Wide-gamut inputs are not clipped
67-
before measurement.
68-
69-
- **`color` primitive type** — added to `PrimitiveType`. Subtype of
70-
`value`. Used in the signatures of all color constructors,
71-
conversions, `Color`, `ColorMix`, `ContrastingColor`, and the
72-
color-input slots of `ColorDelta`/`ColorContrast`/`ColorToString`/
73-
`ColorToColorspace`/`ColorFromColorspace`.
74-
75-
- Driven by the Graph Paper team's roadmap for parsing Desmos-style
76-
color formulas (`\operatorname{rgb}`, `\operatorname{hsv}`,
77-
`\operatorname{oklab}` were the most common unsupported color
78-
operators in their corpus).
79-
80-
- **JavaScript compile-target support for color values** — all five
81-
color constructor heads, the five `As*` converters, `ColorDelta`,
82-
and `Distance` compile to runtime `_SYS.*` calls. At runtime a color
83-
is a 3- or 4-element `[L, C, H]` (or `[L, C, H, alpha]`) array in
84-
canonical OKLCh — the same shape produced by `Color()`, `ColorMix`,
85-
`Colormap`, etc. (Mirrors the GPU target's design: color values are
86-
`vec3` OKLCh in shader code.) `AsOklch` compiles as a no-op pass-
87-
through. The compile-target representation is identical across JS,
88-
GLSL, and WGSL, so values move between contexts without conversion.
14+
`\operatorname{oklch}(...)`, parsing and serialization both directions.
15+
- **Conversions**: `AsRgb`, `AsHsv`, `AsHsl`, `AsOklab`, `AsOklch` convert any
16+
color to the named space (identity if already there).
17+
- **`ColorDelta(a, b)`** — perceptual color difference (ΔE_OK, Euclidean
18+
distance in OKLab). Wide-gamut inputs are not clipped before measurement.
19+
20+
- **JavaScript compile-target support for color values** — all color
21+
constructors, the `As*` converters, `ColorDelta`, and `Distance` are
22+
supported. At runtime a color is a 3- or 4-element OKLCh array
23+
(`[L, C, H]` or `[L, C, H, alpha]`), matching the GPU target's `vec3`/`vec4`
24+
representation, so values move between JS, GLSL, and WGSL without conversion.
25+
26+
- **`Distance(p1, p2)`** — Euclidean distance between two points represented as
27+
tuples. Accepts any positive dimension; mismatched dimensions return a typed
28+
error. LaTeX trigger `\operatorname{distance}(p1, p2)`.
29+
30+
- **Geometric primitive heads**`Triangle`, `Sphere`, `Segment`, and
31+
`GeometricVector` are now recognized as typed function heads (no evaluator,
32+
preserved structurally for downstream consumers). LaTeX triggers
33+
`\operatorname{triangle}`, `\operatorname{sphere}`, `\operatorname{segment}`,
34+
`\operatorname{vector}(p1, p2)`. `GeometricVector` is distinct from the
35+
existing `Vector` (column-vector construction).
36+
37+
- **`To` head registered**`\to` already parsed to `["To", a, b]` but was
38+
classified as `unsupported-operator`; it is now a known typed head.
39+
40+
- **Function-style aliases** — lowercase `\operatorname{...}` forms common in
41+
Desmos-style notation now parse to their existing capitalized operators:
42+
`\operatorname{mod}``Mod`, `\operatorname{var}``Variance`,
43+
`\operatorname{shuffle}``Shuffle`, `\operatorname{random}``Random`,
44+
`\operatorname{repeat}``Repeat`, `\operatorname{join}``Join`.
45+
46+
- **`ce.latexOptions`** — new mutable, engine-wide bag of LaTeX parse/serialize
47+
options (e.g. `decimalSeparator`, `digitGroupSeparator`). Available as a
48+
constructor option and as a read/write property:
49+
```ts
50+
const ce = new ComputeEngine({ latexOptions: { decimalSeparator: '{,}' } });
51+
// or post-construction:
52+
ce.latexOptions = { decimalSeparator: '{,}' };
53+
```
54+
These options are merged into every `ce.parse()` and `expr.toLatex()` call.
55+
Precedence (most-specific wins): `LatexSyntax` instance defaults <
56+
`ce.latexOptions` < per-call options. Previously, options like
57+
`decimalSeparator` could only be changed per call post-construction (and
58+
`expr.latex` could not be customized at all).
8959

9060
#### Changed
9161

92-
- **`Color('...')`** now returns an `Oklch` head instead of a 0–1 sRGB
93-
`Tuple`. Oklch is the canonical wide-gamut representation; downstream
94-
code can convert via `AsRgb` etc. The string parser still accepts the
95-
same set of CSS-style inputs.
96-
97-
- **`ColorMix`** now returns an `Oklch` head regardless of input form
98-
(was: 0–1 sRGB `Tuple`). When both inputs are typed color heads, the
99-
mix happens in OKLCh directly without an sRGB pinch, preserving
100-
out-of-gamut chroma. Hue interpolation takes the shortest path around
101-
the wheel; mixing with an achromatic endpoint carries the other
62+
- **`Color('...')`** now returns an `Oklch` head instead of a 0–1 sRGB `Tuple`.
63+
The string parser still accepts the same set of CSS-style inputs.
64+
- **`ColorMix`** now returns an `Oklch` head and mixes in OKLCh directly,
65+
preserving out-of-gamut chroma. Hue interpolation takes the shortest path
66+
around the wheel; mixing with an achromatic endpoint carries the other
10267
endpoint's hue (matches CSS Color 4 `color-mix`).
103-
104-
- **`ContrastingColor`** now returns an `Rgb` head (was: 0–1 sRGB
105-
`Tuple`). White and black are sRGB by definition, so `Rgb(1, 1, 1)`
106-
and `Rgb(0, 0, 0)` are the natural representations.
107-
108-
- **`Colormap`** now returns `Oklch` heads — either a `List(Oklch, ...)`
109-
for full-palette / N-color resampling, or a single `Oklch` for
110-
position-sampling.
111-
112-
- **`ColorToString`** with `'oklch'` format now serializes typed color
113-
inputs without an sRGB round-trip; out-of-gamut chroma serializes
114-
losslessly. `'hex'`/`'rgb'`/`'hsl'` paths unchanged (sRGB is the
115-
destination, so clipping is correct).
116-
68+
- **`ContrastingColor`** now returns an `Rgb` head (was: 0–1 sRGB `Tuple`).
69+
- **`Colormap`** now returns `Oklch` heads — either a `List(Oklch, ...)` or a
70+
single `Oklch` for position-sampling.
71+
- **`ColorToString`** with `'oklch'` format serializes typed color inputs
72+
without an sRGB round-trip; out-of-gamut chroma serializes losslessly.
73+
`'hex'`/`'rgb'`/`'hsl'` paths are unchanged.
11774
- **Color-consuming signatures tightened**`(any, any)`
11875
`(color | string | tuple, color | string | tuple)` for `ColorDelta`,
11976
`ColorContrast`, `ColorMix`, `ContrastingColor`, `ColorToString`,
120-
`ColorToColorspace`. The `As*` converters tightened further to
121-
`(color) -> color` since they only accept typed color heads.
77+
`ColorToColorspace`. The `As*` converters take `(color) -> color`.
12278

12379
#### Migration notes
12480

125-
Code that consumed `Color('...')`'s tuple output, or the tuple output
126-
of `ColorMix` / `ContrastingColor` / `Colormap`, now sees a typed color
127-
head. To reconstruct the previous shape, wrap with the appropriate
128-
converter:
81+
Code that consumed the tuple output of `Color('...')`, `ColorMix`,
82+
`ContrastingColor`, or `Colormap` now sees a typed color head. To get the
83+
previous 0–1 sRGB shape, wrap with `AsRgb`:
12984

13085
```ts
13186
// Before: const tuple = ce.expr(['Color', "'red'"]).evaluate(); // [r, g, b] in 0-1
@@ -134,122 +89,45 @@ const rgb = ce.expr(['AsRgb', ['Color', "'red'"]]).evaluate();
13489
// rgb is ['Rgb', r, g, b] with channels 0-1
13590
```
13691

137-
**RGB channel convention**: `Rgb` head components are **0–1 sRGB** across
138-
all layers (engine, JS compile, GPU compile). This is a uniform convention
139-
chosen for shader-pipeline interoperability and for round-trip simplicity
140-
(`Rgb(r, g, b) → AsRgb → [r, g, b]` matches the input). Consumers parsing
141-
Desmos-style formulas (which use 0–255) should normalize at the
142-
adapter/importer layer — `Rgb(255, 0, 0)` in Desmos source maps to
143-
`Rgb(1, 0, 0)` in CE.
144-
145-
- **`ce.latexOptions`** — new mutable, engine-wide bag of LaTeX
146-
parse/serialize options (e.g. `decimalSeparator`, `digitGroupSeparator`).
147-
Available as a constructor option and as a read/write property:
148-
```ts
149-
const ce = new ComputeEngine({ latexOptions: { decimalSeparator: '{,}' } });
150-
// or post-construction:
151-
ce.latexOptions = { decimalSeparator: '{,}' };
152-
```
153-
These are merged into every `ce.parse()` and `expr.toLatex()` call.
154-
Precedence (most-specific wins): LatexSyntax instance defaults <
155-
`ce.latexOptions` < per-call options. Previously, the only way to change
156-
options like `decimalSeparator` post-construction was per call (and the
157-
`expr.latex` getter could not be customized at all).
92+
`Rgb` head components are **0–1 sRGB** across all layers (engine, JS compile,
93+
GPU compile).
15894

15995
#### Fixed
16096

16197
- **Super-linear parse time on deeply-nested parametric expressions**
162-
`ce.parse()` could exhibit exponential blowup on inputs like nested
163-
rotation matrices `\left(\cos(\theta)\cdot S+\sin(\theta)\right)`
164-
(depth 6 took ~44s; depth 7 would take minutes). Two underlying causes:
165-
166-
- The `cachedValue()` helper that backs type / sign caches on
167-
`BoxedFunction` had its early-return guard commented out, so every
168-
`.type` access recomputed the type by recursing into all operand
169-
types. With cache disabled, an expression of nesting depth `d`
170-
triggered O(2^d) type recomputations during canonicalization.
171-
172-
The guard had been disabled because the original form was buggy:
173-
174-
```js
175-
if (v.generation === undefined || v.generation === generation) {
176-
if (v.value === null) v.value = fn();
177-
return v.value;
178-
}
179-
```
98+
`ce.parse()` could exhibit exponential blowup on inputs like nested rotation
99+
matrices `\left(\cos(\theta)\cdot S+\sin(\theta)\right)` (depth 6 took ~44s).
100+
Two underlying causes were addressed: the type/sign cache on `BoxedFunction`
101+
was effectively disabled (causing every `.type` access to recurse through all
102+
operands), and `parseEnclosure` was speculatively trying matchfix definitions
103+
whose close-delimiter token wasn't even present in the input. Parse time on
104+
the affected inputs is now linear.
105+
106+
- **`ce.parse()` ignored the injected `LatexSyntax` instance's
107+
`decimalSeparator`**`ce.parse()` hardcoded `decimalSeparator: '.'`,
108+
silently overriding any value configured on a `LatexSyntax` passed via the
109+
constructor's `latexSyntax` option. The injected instance's configured
110+
separator now takes effect end-to-end.
180111

181-
On a fresh cache `v.generation` is `undefined`, so the first call
182-
took the early-return branch, computed `v.value`, and returned —
183-
but never updated `v.generation` from `undefined`. Every later
184-
call, regardless of the requested generation, also matched
185-
`=== undefined` and returned the same stale value. The cache
186-
effectively never invalidated. Disabling it fixed staleness at the
187-
cost of all caching.
188-
189-
Re-enabled with a corrected guard that compares against the
190-
requested generation directly, so first-compute updates
191-
`v.generation` and subsequent calls invalidate correctly when the
192-
engine generation advances:
193-
194-
```js
195-
if (v.generation === generation && v.value !== null) return v.value;
196-
v.generation = generation;
197-
v.value = fn();
198-
return v.value;
199-
```
112+
- **`expr.toMathJson({ metadata: ['latex'] })` was silently dropped** — passing
113+
a metadata array of specific fields (e.g. `['latex']` or `['wikidata']`) was
114+
ignored; only `metadata: 'all'` worked. The array form now correctly
115+
populates the requested fields.
200116

201-
- `parseEnclosure` tried every matchfix definition that matched the
202-
open delimiter, parsing the body twice for each (once with the
203-
boundary, once without). For invalid inputs containing many `.`
204-
tokens (e.g. Desmos's `p.x` field-access syntax), the
205-
`EvaluateAt` matchfix (`.` … `|`) was attempted on every `.` even
206-
though `|` was nowhere in the input, and each speculative body
207-
parse contained more nested `.` triggers. Two changes address this:
208-
a pre-check (using a `closeTokens` set pre-computed at dictionary
209-
indexing time) skips matchfix defs whose close trigger can't appear
210-
ahead in the token stream; and the `EvaluateAt` def is no longer
211-
tried on bare `.` (the canonical form is `\left.expr\right|_{x=0}`,
212-
which the pre-check still admits via the `\left` prefix), so a `.`
213-
without a `\left`/`\mathopen`/etc. prefix never speculates as
214-
`EvaluateAt`.
215-
216-
Combined, deeply-nested expressions and large invalid inputs (the
217-
Desmos corpus's worst row was 1006 chars and previously hung
218-
indefinitely) now parse in milliseconds. On the Desmos corpus
219-
benchmark (2,092 probes at 5s budget), parse-timeouts dropped from
220-
62 to 3 — a 95% reduction.
221-
222-
- **`ce.parse()` ignored the injected LatexSyntax instance's
223-
`decimalSeparator`**`ce.parse()` hardcoded `decimalSeparator: '.'`,
224-
silently overriding any value configured on the `LatexSyntax` passed
225-
via the constructor's `latexSyntax` option. The hardcoded default has
226-
been removed; the injected instance's configured separator now takes
227-
effect end-to-end.
228-
229-
- **`expr.toMathJson({ metadata: ['latex'] })` was silently dropped** —
230-
passing a metadata array containing only specific fields (e.g.
231-
`['latex']` or `['wikidata']`) was ignored because the option fell
232-
through and the final spread overrode it with the empty default. Only
233-
`metadata: ['all']` worked. The array case now correctly populates
234-
the requested metadata fields.
235-
236-
- **`expr.toMathJson({ shorthands: ['all'] })` disabled all shorthands** —
237-
the `'all'` branch correctly expanded the defaults to the full kind
238-
list, but a following unconditional `if (Array.isArray(...))`
239-
overwrote it back to the literal `['all']`, which matches no actual
240-
shorthand kind. So passing `['all']` had the opposite effect of what
241-
was intended. The string form `'all'` and explicit lists like
242-
`['function']` were unaffected.
117+
- **`expr.toMathJson({ shorthands: ['all'] })` disabled all shorthands** — the
118+
`['all']` array form had the opposite of its intended effect. The string form
119+
`'all'` and explicit lists like `['function']` were unaffected.
243120

244121
### 0.55.6 _2026-03-08_
245122

246123
#### Fixed
247124

248-
- **LaTeX parsing: `\lim` with postfix operators** — `\lim_{x\to 0}\left(x\right)^x`
249-
now correctly parses as `Limit(x^x)` instead of `Power(Limit(x), x)`. The
250-
`\lim` parser was using `parseArguments('implicit')` which stripped the
251-
delimiters and left the `^x` unconsumed; it now uses `parseExpression` so
252-
postfix operators are included in the limit body.
125+
- **LaTeX parsing: `\lim` with postfix operators**
126+
`\lim_{x\to 0}\left(x\right)^x` now correctly parses as `Limit(x^x)` instead
127+
of `Power(Limit(x), x)`. The `\lim` parser was using
128+
`parseArguments('implicit')` which stripped the delimiters and left the `^x`
129+
unconsumed; it now uses `parseExpression` so postfix operators are included in
130+
the limit body.
253131

254132
- **LaTeX parsing: style, size, and color switch commands**`\displaystyle`,
255133
`\textstyle`, `\scriptstyle`, `\scriptscriptstyle`, `\tiny`..`\Huge` (10 size

0 commit comments

Comments
 (0)