// ===========================================================================
//  JMathAnim DSL Quick Reference
//
//  Build:   typst compile main.typ ../jmathanim-dsl-cheatsheet.pdf
//  Watch:   typst watch main.typ
// ===========================================================================

#import "lib/cheatsheet.typ": *

#show: cheatsheet.with(
  title: "JMathAnim DSL Quick Reference",
  subtitle: "Groovy scripting DSL",
  version: "1.5.0",
)

#note[
  #lead[Note:]
  - All parameter keys are case-insensitive. `style:` accepts either a Map, a
    String (style name) or a List mixing both.
  - Parameters that expect coordinates admit both `Coordinates` objects (`Vec`,
    `Point`, etc.) or a list like `[3,2]`.
  - *Angles are always in radians.* There is no `degrees:` parameter: write
    `angle: 90 * DEGREES`.
  - The same idea always has the same name, and the older spellings keep working
    as aliases: `from:`/`to:` for origin and destination (`origin:`, `destiny:`,
    `dst:` are aliases), and `gap:`/`gaps:` are interchangeable wherever a gap is
    accepted (`gaps:` also takes the `[h, v]` pair).
  - An `r` before a string takes its content literally, ideal for LaTeX:
    `r"$\pi$"`. The same prefix before a bracket, `r["$a$", "$b$"]`, makes every
    string inside raw, nested lists and maps included. It needs two elements or
    more, so a one element list is written `[r"$\pi$"]`.
]

= Basics

== `shape(...)`

#tbl((29%, 51%, 20%),
  [Type], [Required params], [Optional params],
  [`circle`], [-], [`segments`],
  [`square`], [-], [-],
  [`rectangle`], [`from:[x,y]`, `to:[x,y]` or `rect`], [-],
  [`triangle`], [-], [-],
  [`polygon`], [`points:[[x,y],...]`], [-],
  [`polyline`], [`points:[[x,y],...]`], [-],
  [`segment`], [`from:[x,y]`, `to:[x,y]`], [`numpoints`],
  [`arc`], [`angle` (radians)], [`segments`],
  [`regularPolygon`], [`sides`], [-],
  [`regularInscribedPolygon`], [`sides`], [-],
  [`annulus`], [`minRadius`, `maxRadius`], [-],
  [`logo`], [`commands` (String)], [-],
  [`svgpath`], [`path` (String of SVG path commands, or any code from
    `toSVGPath()`/`toStyledPath()`, compressed or not)], [-],
)

#lead[Common params (every object builder):] `style`, `parts`, `layer`,
`visible`, `fixedCamera`, `transform`, `stack`, `label`, `methods`, `addToScene`

`fixedCamera: true` draws the object with the fixed camera, so it stays where it
is while the scene camera moves. `methods:` is a Map `name -> closure` that
becomes real methods of that object and of no other (inside the body, `delegate`
is the object itself):
`methods: [at: { c, r -> delegate[('a'..'h')[c - 1] + r] }]`.

#lead[Path params (any type):] `subShape: [a, b]` keeps only that portion of the
path (parameters from 0 to 1 along the whole path); `smooth: true` curves every
segment from the slopes of the neighbouring points, giving a polygon or polyline
a spline-like look (a number sets the tension, 0 to 1, default `0.7`). Both are
applied right after building the shape, so styles, transforms and labels land on
the result.

```groovy
def p = shape(type: "polyline", points: [[0,.25],[-1,.25],[0,-.25],[1,.25]],
              smooth: .7)
def h = shape(type: "circle", subShape: [0.2, 0.76])
```

```groovy
def s = shape(type: "circle", segments: 6,
              style: [color: "blue", thickness: 2],
              transform: [scale: 1.5, shift: [1, 0]])

def r = shape(type: "segment", from: [0,0], to: [1,1])
def p = shape(type: "polygon", points: [[0,0],[1,0],[0.5,1]])
def b = shape(type: "rectangle", rect: [-2,-1, 2,1])
// rect: also "screen", "unit", a number, or a Rect
```

`rect` accepts the same values as any other rectangular domain in the DSL: a
`Rect`, `[xmin,ymin,xmax,ymax]`, a number `n` (centered square), or a preset name
(`unit`, `centered`, `screen`, `trig`, `trigCentered`). It cannot be combined
with `from`/`to`.

#lead[Inline labels (`label:`):] attach one or more labels to the shape, with the
same options as the standalone `label(...)` DSL (`t` is an alias for `location`):

```groovy
shape(type: "square", label: "x")               // LaTeX label at t=0.5
shape(type: "square",
      label: [text: "x", t: 0.3, upside: true,
              style: [drawColor: "blue"], transform: [rotate: PI/2]])
shape(type: "square", label: [object: Shape.circle().scale(0.1), t: 0.3])
shape(type: "square", label: [[text: "A", t: 0], [text: "B", t: 0.5]])
shape(type: "regularpolygon", sides: 5,
      label: ["a", "b", "c", "d", "e"])         // one per side: 0, 1, 2...
shape(type: "regularpolygon", sides: 3,        // corner-anchored labels:
      label: [[text: "A", vertex: 0], [text: "B", vertex: 1],
              [text: "C", vertex: 2]])
```

`vertex: n` (alternative to `t`) anchors the label at path vertex \#n, placed on
the outward corner bisector (ideal for polygon vertices). Indices wrap
circularly (`-1` = last vertex) and the label is never rotated along the path.

When you pass *several* labels and a spec has no position of its own (`t`, `side`
or `vertex`), it is placed at the midpoint of the side matching its index, so the
list goes around the shape instead of piling up at the same spot.

`side: n` (alternative to `t`) places the label at the midpoint of side \#n,
giving every side the same weight regardless of its length; indices wrap
circularly. The same parametrization is available for `t` with
`normalized: false`, so on a 4-sided shape `t: 0.5/4` is the midpoint of the
first side whatever its dimensions:

```groovy
shape(type: "rectangle", from: [0,0], to: [3,1],
      label: [[text: "a", side: 0], [text: "b", side: 1]])
shape(type: "rectangle", from: [0,0], to: [3,1],
      label: [text: "a", t: 0.5/4, normalized: false])   // same as side: 0
```

Inline labels are *internal objects* of the shape: they update, draw and animate
(fadeIn/fadeOut, etc.) together with it, and are never added to the scene on
their own. Retrieve them with `sq.getInternalObject("label0")` (`"label1"`, ...)
or name them yourself with a `key:` entry:

```groovy
def sq = shape(type: "square", label: [text: "x", key: "north"])
def lbl = sq.getInternalObject("north")
```

For a label managed independently of its path (added/removed from the scene by
itself), use the standalone `label(...)` DSL instead.

#lead[Reading it back.] The path is reachable point by point, and the shape
answers as a curve of its own:

```groovy
sq[2]; sq[1..3]; sq.size()      // JMPathPoint(s); a negative index counts back
sq.getPoint(2)                  // the same one, as a Point
sq[2].v; sq[2].vEnter; sq[2].vExit   // its position and its two control points
sq(0.5)                         // the Vec at 50% of the length of the path
sq.getVecAt(0.5)                // the same reading, in the Bezier parameter
sq.getSubShape(0.2, 0.6)        // that portion of the path, as a new Shape
sq.getPath()                    // the JMPath: getLength(), getArea(),
                                // getCentroid(), isConvex(), getSlopeAt(t, true)
sq.toSVGPath()                  // the path as a String, to paste into svgpath
```

== `point(...)`

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`at`], [`[x,y]` / `[x,y,z]` / Coords], [`[0,0]`], [Position],
  [`style`], [Map], [-], [Style (e.g. `dotStyle`, `color`, `thickness`)],
  [`label`], [String / Map / List], [-], [Inline label(s), as in `shape(...)`],
  [`layer` / `visible` / `transform` / `stack`], [-], [-], [Common props],
  [`addToScene`], [boolean], [`false`], [Add to scene immediately],
)

```groovy
def p = point(at: [1, 0], style: [color: "blue", dotStyle: "cross",
                                  thickness: 4])
def a = point(at: [1, 0], label: "$A$")
```

#lead[Reading it back.] `p.vec` is the vector it holds and `p.x`, `p.y`, `p.z`
its components; `A.to(B)` is the vector from one point to another, and
`p.getDotShape()` the dot that gets drawn.

== `line(...)` / `ray(...)`

Infinite line and ray, as plain shapes: they redraw themselves to span the whole
visible math view, so they look infinite at any zoom. For the constructible
variants, which follow their defining points, use `ctLine(...)` / `ctRay(...)`.

#tbl(W3,
  [Key], [Type], [Description],
  [`a`], [`[x,y]` / Coords], [*required*, point the line passes through],
  [`b`], [`[x,y]` / Coords], [Second point; excludes `dir`],
  [`dir`], [`Vec` / `[x,y]` / line], [Direction instead of a second point: a
    vector, any object that has one (a segment, another line), or a pair of
    points such as `[P1, P2]`],
)

```groovy
def l = line(a: A, b: B)
def r = ray(a: [0, 0], dir: [1, 1])
def m = line(a: A, dir: segment1, style: [color: "blue"])
```

== `stack:` map

Exactly one of `to`, `screen`, or `point` must be present.

#tbl(W3,
  [Key], [Type], [Description],
  [`to`], [Boxable], [Destination object],
  [`screen`], [String], [`CENTER` `LEFT` `RIGHT` `UPPER` `LOWER` `UPPER_LEFT`
    `UPPER_RIGHT` `LOWER_LEFT` `LOWER_RIGHT`],
  [`point`], [`[x,y]`], [Fixed point destination],
  [`originAnchor`], [String], [AnchorType of moving object (alias
    `anchorStart`)],
  [`destinyAnchor`], [String], [AnchorType of destination (alias `anchorEnd`)],
  [`gaps`], [double / `[h,v]`], [Absolute gap],
  [`relativeGaps`], [double / `[h,v]`], [Relative gap (mutually exclusive with
    `gaps`)],
  [`ignoreInternals`], [boolean], [Stack by the object alone, ignoring its
    labels and marks (alias `ignoreInternal`)],
)

```groovy
stack: [to: otherShape, originAnchor: "left", destinyAnchor: "right", gaps: 0.1]
stack: [screen: "upper_left"]
stack: [point: [1, 2], destinyAnchor: "center"]
```

= AnchorType values (common reference)

`CENTER` `LEFT` `RIGHT` `UPPER` `LOWER`
`LEFT_AND_ALIGNED_UPPER` `LEFT_AND_ALIGNED_LOWER`
`RIGHT_AND_ALIGNED_UPPER` `RIGHT_AND_ALIGNED_LOWER`
`LOWER_AND_ALIGNED_LEFT` `LOWER_AND_ALIGNED_RIGHT`
`UPPER_AND_ALIGNED_LEFT` `UPPER_AND_ALIGNED_RIGHT`
`BASELINE` `BASELINE_LEFT` `BASELINE_RIGHT`
`DIAG1` `DIAG2` `DIAG3` `DIAG4` `ZTOP` `ZBOTTOM`

#tip[
  All keyword String values support *prefix matching* (case-insensitive). E.g.
  `"cen"` matches `CENTER` in `AnchorType`.
]

The three `BASELINE` anchors take the height from the text baseline instead of
from the bounding box, so texts with and without descenders ("aqua" vs "blue")
line up as they read. Only LaTeX objects compiled with JLaTeXMath know their
baseline; every other object falls back to its lower side.

`AlignType` has the matching pair `BASELINE` (both baselines at the same height)
and `BASELINE_TO_LOWER` (this baseline on the lower side of the other object, so
a text can sit on any shape or on the math view).

```groovy
def a = latex(text: "aqua", anchor: "baseline")
def b = latex(text: "blue", transform: [align: [a, "baseline"]])
b.alignBaselineWith(a)              // the same, without the transform map
group(obj: [a, b]).alignBaselines() // every text of a group at once
animAlign(obj: b, to: a, type: "baseline", runtime: 1)
b.stack(to: a, destinyAnchor: "baseline_right", gaps: [.3, 0])
a.trimBoxToBaseline()               // baseline becomes the lower side of the
                                    // box, so labels, stack and layouts align
```

`BASELINE_LEFT` and `BASELINE_RIGHT` are the reverse of each other, so stacking
with either puts the texts side by side sharing the line. Being a horizontal pair
they take the *horizontal* gap, like `RIGHT_AND_ALIGNED_LOWER`, while plain
`BASELINE` takes the vertical one.

== `transform:` map

#tbl(W3,
  [Key], [Type], [Description],
  [`scale`], [double / `[sx,sy]`], [Uniform or non-uniform scale],
  [`shift`], [`[x,y]` / Coords], [Translation],
  [`rotate`], [double / `[center, angle]`], [Rotation (radians), optionally
    around a point],
  [`center`], [`true`], [Center the object on screen],
  [`affine`], [AffineJTransform / List / Map], [An `affineTransform(...)` applied
    as is (see `affineTransform(...)`)],
)

The entries are applied *in the order they are written*, so
`[scale: 2, affine: tr, center: true]` scales, then applies the affine map, and
centers last. Reordering the keys reorders the operations.

The whole `transform:` param also accepts an `AffineJTransform` instead of a map,
as a shorthand for `transform: [affine: tr]`.

`affine` also takes a flat list of coordinates: 3 of them, `[A,B,C]`, give the
affine map sending the canonical frame `[0,0],[1,0],[0,1]` into them; 4,
`[A,B,C,D]`, the direct similarity mapping `A,B` into `C,D`; 6,
`[A,B,C,D,E,F]`, the general affine map sending `A,B,C` into `D,E,F`. A list of
transforms (or of param maps) is applied in order.

```groovy
transform: [scale: 2, shift: [1, 0], rotate: 45*DEGREES]
transform: [rotate: [[0,0], PI/3]]   // rotate around [0,0]
transform: [affine: tr]              // tr = affineTransform(...)
transform: tr                        // shorthand for the line above
transform: [affine: [A, B, C]]               // canonical frame -> A,B,C
transform: [affine: [A, B, C, D]]            // similarity A,B -> C,D
transform: [affine: [A, B, C, D, E, F]]      // affine A,B,C -> D,E,F
```

= Reading objects

Every builder gives back the object it made, and the object is meant to be read:
the pieces it draws, the numbers it follows, the points it was built on. What
each one offers is listed in its own entry; the shortcuts below work on all of
them.

#tbl((36%, 64%),
  [Expression], [What it gives],
  [`obj.center`, `obj.width`], [Any `getXxx()` reads as a property, without the
    `get` and the parentheses: `getCenter()`, `getWidth()`, `getHeight()`. When
    the setter exists it is also written that way: `obj.width = 2` scales the
    object, `obj.center = [0,0]` moves it],
  [`obj.boundingBox`], [The `Rect` around it, with `center`, `upperLeft`,
    `left`, `lower`... and `getFromAnchor(AnchorType.UPPER)`],
  [`sq.area`, `sq.length`], [Area enclosed and length of the path of any shape;
    the length is the perimeter when it is closed. The `Rect` of a bounding box
    answers both too],
  [`obj.getInternalObject("label0")`], [An internal object (an inline label, a
    mark) by its key; `getInternalObjectKeys()` lists them],
  [`obj["knob"]`, `obj["pan*"]`], [Named pieces of a composite object, one or a
    group of them (see #link(<parts-map>)[`parts:` map]); `obj.parts()` lists
    the names],
  [`g[0]`, `g[-1]`, `g[0..2]`], [Children of a group or a compound: by index,
    counting from the end, or a range, which gives a List],
  [`eq[3]`], [Glyph 3 of a multi-shape object (a formula, an SVG import)],
  [`sq[2]`], [The `JMPathPoint` number 2 of a shape],
  [`tri[0]`], [Vertex 0 of a constructible polygon, polyline or triangle],
  [`obj.each { }`, `obj.collect { }`], [Groups, multi-shape objects and color
    scales are iterable],
  [`sq(0.5)`], [The `Vec` at 50% of the length of any path],
  [`curve(t)`, `cp(level)`, `cs(t)`], [Point of a parametric curve at its own
    parameter, contour line at a level, color of a scale. Each one takes a
    number or a `scalar(...)`],
  [`ct.mathObject`], [The `MathObject` a constructible draws, the one holding
    its geometry],
  [`eq as Shape`, `g as List`], [Every subpath merged into one `Shape`, the
    children as a plain List],
  [`obj + vec`, `obj * 2`], [A transformed copy; the original is left alone],
  [`scene << obj`, `g << obj`], [Add to the scene, or to a group],
)

#tip[
  In the editor, write the dot and press `Ctrl+Shift+G`: the method search lists
  what the object accepts, found by name or by description, and writes the chosen
  one at the caret. It knows the type of the variable, lists its own methods before
  the inherited ones, and offers the short property form whenever the getter has a
  setter.
]

#tip[
  What is read this way is the value of the moment and not a live link:
  `def w = obj.width` is a number, and it does not follow the object afterwards.
  Read it inside an `always(...)` block to keep it up to date, or build it as a
  #link(<scalar>)[`scalar(...)`] (`distance`, `radius`, `angle`, `area`), which
  is recomputed on its own and can be built upon.
]

= Colors & Styles

== `style:` map

#tbl(W3,
  [Key], [Type], [Values / Notes],
  [`name`], [String], [Named style template (loaded first)],
  [`color`], [String], [Color name or hex],
  [`drawColor`], [String], [Stroke color],
  [`fillColor`], [String], [Fill color],
  [`thickness`], [double], [Stroke width],
  [`thicknessTracksZoom`], [boolean], [Camera zoom changes apparent thickness
    (default `false`)],
  [`thicknessTracksScale`], [boolean], [Scaling the object scales its thickness
    (default `true`)],
  [`drawAlpha`], [double], [Stroke opacity `[0,1]`],
  [`fillAlpha`], [double], [Fill opacity `[0,1]`],
  [`alpha`], [double], [Both opacities at once],
  [`visible`], [boolean], [Not a style: applied to the object owning the block,
    ignored on named style templates],
  [`layer`], [int], [Rendering layer],
  [`lineCap`], [String], [`SQUARE` `ROUND` `BUTT`],
  [`lineJoin`], [String], [`BEVEL` `ROUND` `MITER`],
  [`strokeType`], [String], [`OUTSIDE` `INSIDE` `CENTERED`],
  [`dotStyle`], [String], [`CIRCLE` `CROSS` `PLUS` `RING` `TRIANGLE_UP_HOLLOW`
    `TRIANGLE_UP_FILLED` `TRIANGLE_DOWN_FILLED` `TRIANGLE_DOWN_HOLLOW`],
  [`dashStyle`], [String], [`SOLID` `DASHED` `DOTTED` `DASHDOTTED`],
  [`renderEffects`], [Map], [Per-object render effects, see below],
)

```groovy
style: [name: "myTemplate", color: "red", thickness: 2, dashStyle: "dashed"]
style: "myTemplate"   // shorthand: loads named style only
```

#lead[`renderEffects:` sub-map.] Effects the renderer applies to that one object.
Every spatial value is in math units, not pixels, so they follow the zoom.

#tbl(W3,
  [Key], [Type], [Description],
  [`blur`], [double], [Gaussian blur radius],
  [`shadow`], [boolean], [Draw the shadow. A positive `shadowKernel` already
    enables it; an explicit `shadow: false` still wins],
  [`shadowKernel`], [double], [Blur radius of the shadow],
  [`shadowOffsetX` / `shadowOffsetY`], [double], [Offset of the shadow],
  [`shadowColor`], [String / JMColor], [Color of the shadow],
  [`shadowAlpha`], [double], [Opacity of the shadow],
  [`clip`], [Shape / `false`], [Clip the object to that shape, or drop the
    clipping],
)

```groovy
apply(obj: sq, style: [renderEffects: [blur: .02,
                                       shadowKernel: .05,
                                       shadowOffsetX: .02, shadowOffsetY: -.02,
                                       shadowColor: "black", shadowAlpha: .6,
                                       clip: myRectangle]])
```

== `parts:` map <parts-map>

Styles the individual pieces of a composite object by name. Accepted by every
object builder, by `apply(...)` and by `animStyle(...)`. Each value is exactly
what `style:` accepts: a Map, a named-style String, or a List of both.

#tbl((34%, 66%),
  [Selector], [Matches],
  [`knob`], [One part, by the name it was registered under (case-insensitive)],
  [`"min,max"` or `["min","max"]`], [Several at once],
  [`"lab*"`], [Glob on the name (`*` and `?`)],
  [`"*"`], [Every part],
  [`"xAxis/ticks"`], [Descends into the parts of a part],
)

Selectors are applied by specificity, not in writing order: `"*"` first, then
globs, then exact names, so the general case is the base and the specific
selectors correct it. A selector matching nothing is reported in the log with the
list of available names.

Part names of the built-in composites:

#tbl((22%, 48%, 30%),
  [Object], [Parts], [Collective names],
  [`slider`], [`knob`, `value`, `name`, `min`, `max`], [`labels`, `ends`],
  [`axes`], [`xAxis`, `yAxis`, `xTicks`, `yTicks`, `xLegends`, `yLegends`,
    `xLabel`, `yLabel`], [`axis`, `ticks`, `legends`, `labels`],
  [`shape`, `point`, `funcGraph`, `line`, `ray`, `parametricCurve`, `trail`,
    `locus`, `combine`, `pointOnGraph`], [`label0`, `label1`... (inline labels), or the
    `key:` of the label spec], [-],
  [`group`, `compound`], [The name of every node / named
    child], [-],
)

```groovy
slider(value: a, from: 0, to: 1, style: [color: "gray"],
       parts: [knob : [color: "red", dotStyle: "circle", thickness: 40],
               labels: [color: "gray"],
               "min,max": [drawAlpha: 0.5]])

axes(parts: ["*": [color: "gray"], legends: [color: "white"]])

apply(obj: sl, parts: [knob: [color: "green"]])       // after creation
animStyle(obj: sl, parts: [knob: [color: "green"]])   // animated
sl.parts(knob: [color: "green"])                      // Groovy shortcut
sl["knob"].thickness(40)                              // one piece by name
sl.parts()                                            // list available names
```

#lead[Reaching pieces with `obj[...]`.] The same selectors work in a subscript,
which returns the piece when it matches exactly one and a `MathObjectGroup` with
all of them when it matches several, so nothing is silently dropped:

```groovy
balance["beam"]                                  // the piece
balance["beam", "panL", "panR"]                  // a group with the three
balance["beam,panL"]                             // the same, as one String
balance["pan*"].color("red")                     // a group: the glob matches 2
balance.parts("panL", "panR")                    // method-call form
apply(obj: balance["pan*"], style: [thickness: 4])
```

The group registers the pieces under their own names, so
`balance["beam","panL"]["panL"]` works and it accepts a `parts:` map of its own.
It is a _logical_ group: the pieces stay inside the composite object, which is
still the one drawing them, so use it for bulk operations and not to add them to
the scene. A selector matching nothing fails with the list of available names.

== `createStyle(...)`

```groovy
createStyle(name: "myStyle", style: [color: "red", thickness: 2])
```

#tbl((20%, 16%, 64%),
  [Param], [Required], [Description],
  [`name`], [yes], [Style name to register],
  [`style`], [yes], [Map of style properties (same keys as `style:` map)],
)

== `linearGradient(...)`

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`from`], [`[x,y]` / Point], [`[0,0]`], [Start point],
  [`to`], [`[x,y]` / Point], [`[1,0]`], [End point],
  [`relative`], [boolean], [`true`], [Coords relative to shape],
  [`cycle`], [String], [`"no_cycle"`], [`no_cycle` `reflect` `repeat`],
  [`colorScale`], [ColorScale / List], [-], [Every stop at once: a `ColorScale`,
    a preset `["viridis", 0, 1]`, or explicit `[[color, value], ...]`],
  [`0.0:`, `0.5:`, ...], [String / JMColor], [-], [Color stops, applied on top of
    `colorScale`],
)

```groovy
def lg = linearGradient(from: [0,0], to: [1,0],
                        relative: true,
                        0.0: "blue", 0.5: "red", 1.0: "#00FF00")
def lg2 = linearGradient(colorScale: colorScale(name: "viridis"))
def lg3 = linearGradient(colorScale: ["magma", 0, 1], 1.0: "white")
```

== `radialGradient(...)`

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`center`], [`[x,y]` / Point], [`[0.5,0.5]`], [Gradient center],
  [`radius`], [double], [`0.5`], [Gradient radius],
  [`focusAngle`], [double], [`0`], [Focus point angle (radians)],
  [`focusDistance`], [double], [`0`], [Focus distance from center],
  [`relative`], [boolean], [`true`], [Coords relative to shape],
  [`cycle`], [String], [`"no_cycle"`], [`no_cycle` `reflect` `repeat`],
  [`colorScale`], [ColorScale / List], [-], [Every stop at once, as in
    `linearGradient(...)`],
  [`0.0:`, `1.0:`, ...], [String / JMColor], [-], [Color stops, applied on top of
    `colorScale`],
)

```groovy
def rg = radialGradient(center: [0.5,0.5], radius: 0.5,
                        0.0: "yellow", 1.0: "red")
```

== `colorScale(...)`

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`name`], [String], [-], [Preset: `viridis` `magma` `inferno` `plasma` `turbo`
    `rainbow` `grayscale` `blueRed`],
  [`from` / `to`], [double], [`0` / `1`], [Range spanned by the preset or by
    `colors`],
  [`colors`], [List], [-], [Colors spread over `[from, to]`, or
    `[position, color]` pairs],
  [`0.0:`, `1.0:`, ...], [String / JMColor], [-], [Explicit stops (applied last,
    override the rest)],
)

```groovy
def cs = colorScale(name: "viridis", from: 0, to: 1)
def cs = colorScale(colors: ["violet", "red", "brown"])      // stops 0, 0.5, 1
def cs = colorScale(colors: [[0, "violet"], [0.5, "black"]]) // explicit places
def cs = colorScale(0.0: "blue", 0.5: "white", 1.0: "red")
def cs = colorScale((-2): "blue", 0: "white", 2: "red")  // negative keys: in ()
```

#lead[Reading it back.] `cs(t)`, or `cs.getColorAt(t)`, is the color at that
position, with `t` a number or a `scalar(...)`. The stops themselves are
`cs.getColors()` (position -> `JMColor`, in order), and the scale is iterable:
`cs.each { t, color -> ... }`, `cs.collect { t, color -> ... }`, `cs.size()`.

= Other Objects

== `axes(...)`

#tbl(W3w,
  [Key], [Type], [Description],
  [`type`], [String], [`cartesian` (default), `numberLine` (only the x axis),
    `numberLineY` (only the y axis). The range of the hidden axis is ignored],
  [`xAxisVisible` / `yAxisVisible`], [boolean], [Draw that axis with its arrow
    head, label and ticks; overrides `type`],
  [`xRange` / `yRange`], [`[from, to]`], [Primary tick range (or `range:` for
    both)],
  [`xStep` / `yStep`], [double], [Primary tick step (default `1`)],
  [`secondary`], [Map], [Shorthand: sets both `secondaryX` and `secondaryY`],
  [`secondaryX` / `secondaryY`], [Map `{from,to,step,maxWidth}`],
    [Secondary ticks],
  [`ticksX` / `ticksY`], [List], [Individual ticks (Number or Map
    `{at, label, type, maxWidth, scale, style, markStyle, showLabel}`)],
  [`style`], [Map], [Global axes style],
  [`axisStyle`], [Map], [Style of both axis lines at once],
  [`xStyle` / `yStyle`], [Map], [Individual axis line style],
  [`xTickStyle` / `yTickStyle`], [Map], [Tick mark style],
  [`xLegendStyle` / `yLegendStyle`], [Map], [Tick label style],
  [`parts`], [Map], [Style of each piece by name: `xAxis`, `yAxis`, `xTicks`,
    `yTicks`, `xLegends`, `yLegends`, `xLabel`, `yLabel`, plus `axis`, `ticks`,
    `legends`, `labels` (see #link(<parts-map>)[`parts:` map])],
  [`labelScale`], [double], [Scale factor for all tick labels (X and Y)],
  [`xLabelScale` / `yLabelScale`], [double], [Scale factor for the labels of one
    axis],
  [`xScale` / `yScale`], [Map], [Data to scene mapping: `{origin, unit}` or
    `{log, origin, unit}`],
  [`logTicksX` / `logTicksY`], [Map `{from, to, minor}`], [Labelled ticks at
    powers of the scale base; `minor` adds unlabelled marks in between],
  [`axisRange`], [`[from, to]`], [Shorthand: draws both axes as segments],
  [`xAxisRange` / `yAxisRange`], [`[from, to]`], [Draws that axis as a segment
    instead of an infinite line],
  [`arrows`], [boolean], [Arrow head at the positive end of each axis],
  [`xAxisLabel` / `yAxisLabel`], [String], [Latex name drawn at the end of the
    axis],
  [`format`], [String], [Number pattern for automatic labels, e.g. `"#.##"`],
  [`xFormat` / `yFormat`], [String], [Number pattern for one axis, overrides
    `format`],
)

`tick.type`: `PRIMARY` (default) or `SECONDARY`. `tick.scale` scales that single
label; it multiplies with `labelScale`.

```groovy
def ax = axes(xRange: [-3, 3], yRange: [-2, 2], xStep: 1, yStep: 1,
              secondaryX: [step: 0.5],
              labelScale: 0.8,
              ticksX: [[at: Math.PI, label: r"\pi", scale: 1.5]])

// Bounded axes with arrow heads and names
def ax2 = axes(xRange: [-3, 3], yRange: [-2, 2],
               axisRange: [-3, 3], arrows: true,
               xAxisLabel: r"$x$", yAxisLabel: r"$y$")

// A plain number line
def nl = axes(type: "numberLine", range: [-5, 5], xStep: 1, arrows: true)

// Logarithmic x-axis: 10^0 .. 10^3, one unit per decade
def ax3 = axes(yRange: [-2, 2],
               xScale: [log: 10, unit: 1],
               logTicksX: [from: 0, to: 3, minor: true])
```

`xScale`/`yScale` decouple the values an axis shows from the scene coordinates
where they are drawn. Use `ax.toWorld(x, y)` to place an object at a given pair
of axis values, and `ax.toData(x, y)` for the inverse.

#lead[Reading it back.]

```groovy
ax.toWorld(1, 2); ax.toData(0.5, 0)       // axis values <-> scene coordinates
ax["xAxis"]; ax["xTicks"]; ax["labels"]   // the pieces, by the names of parts:
ax.getXAxisLabel()                        // the LatexMathObject at the end of the axis
ax.getXScale(); ax.getYScale()            // the AxisScale, when one was set
```

== `grid(...)`

Cartesian grid. Added to the scene by default (`addToScene: false` to opt out).

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`center`], [`[x,y]` / Coords], [`[0,0]`], [Grid center],
  [`steps`], [double / `[sx,sy]`], [`[1,1]`], [Primary grid step],
  [`divisions`], [int / `[dx,dy]`], [`[1,1]`], [Secondary subdivisions],
  [`primaryStyle`], [Map], [-], [Style for primary lines],
  [`secondaryStyle`], [Map], [-], [Style for secondary lines],
)

```groovy
def g = grid(steps: 1, divisions: 4,
             primaryStyle: [color: "gray"],
             secondaryStyle: [color: "lightgray"])
```

== `polarGrid(...)`

Polar grid. Added to the scene by default (`addToScene: false` to opt out).

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`center`], [`[x,y]` / Coords], [`[0,0]`], [Grid center],
  [`radiusStep`], [double], [`1`], [Distance between primary circles],
  [`angularDivisions`], [int], [`6`], [Primary radial rays over the full circle],
  [`divisions`], [int / `[dr,da]`], [`[1,1]`], [Secondary radial / angular
    subdivisions],
  [`primaryStyle`], [Map], [-], [Style for primary lines],
  [`secondaryStyle`], [Map], [-], [Style for secondary lines],
)

```groovy
def pg = polarGrid(radiusStep: 1, angularDivisions: 12, divisions: [2, 2])
```

#lead[Reading it back.] Both grids hand over the lines they draw, so one of them
can be styled or animated on its own:

```groovy
g.getHorizontalPrimaryLines(); g.getVerticalSecondaryLines()   // cartesian
pg.getPrimaryCircles(); pg.getRadialPrimaryLines()             // polar
g.getPrimaryGridStyle(); g.getSecondaryGridStyle()             // the two styles
```

== `board(...)`

Rectangle divided into cells (the uncoloured chessboard), returned as a compound.
The outer border is a single closed rectangle named `border`; the inner lines are
segments named `v1`..`v<columns-1>` (vertical, left to right) and
`h1`..`h<rows-1>` (horizontal, bottom to top), so `b["v3"]` and `parts:` reach
them.

#tbl(W4w,
  [Key], [Type], [Default], [Description],
  [`width`], [double], [`1`], [Width of the board],
  [`height`], [double], [`width`], [Height of the board],
  [`center`], [`[x,y]` / Coords], [`[0,0]`], [Center of the board],
  [`rect`], [Rect / `[xmin,ymin,xmax,ymax]` / double / name], [-], [Whole region
    instead of `width`/`height`/`center`],
  [`columns`], [int], [`8`], [Number of columns (alias `cols`)],
  [`rows`], [int], [`columns`], [Number of rows],
  [`divisions`], [int / `[cols,rows]`], [-], [Shorthand for both;
    `columns`/`rows` win],
)

```groovy
def b = board(width: 4, columns: 8)                      // chessboard
def b2 = board(width: 6, height: 3, columns: 4, rows: 2,
               style: [thickness: 2],
               parts: ["border": [thickness: 5], "h*": [color: "gray"]])
```

== `funcGraph(...)`

#tbl(W4w,
  [Key], [Type], [Default], [Description],
  [`func`], [Closure / DoubleUnaryOperator / DoubleBinaryOperator],
    [*required*], [The function to plot],
  [`xRange` (`range`)], [`[from, to]`], [-], [Domain; if omitted the graph is
    *dynamic* (follows the camera)],
  [`numPoints`], [int], [-], [Sample points (only with an explicit range)],
  [`dynamic`], [boolean], [-], [Force dynamic range even with `xRange`],
  [`style` / `label`], [-], [-], [Style and inline label(s)],
)

```groovy
def fg = funcGraph(func: { x -> Math.sin(x) }, xRange: [-3, 3],
                   style: [color: "blue"])
def fg2 = funcGraph(func: Math.&cos)   // dynamic: follows the camera view
```

#lead[Reading it back.]

```groovy
fg.getFunctionValue(1.2)   // f(1.2)
fg.getSlope(1.2, 1)        // slope at that x, 1 forwards and -1 backwards
fg.getAreaShape(0, 1)      // the region under the curve, as a Shape
fg.addX(0.7)               // forces a node there, and returns its JMPathPoint
fg(0.5)                    // as any path: the Vec at 50% of its length
```

== `pointOnGraph(...)`

An updateable point whose y follows f(x) of a function graph: shifting it
horizontally slides it along the curve.

#tbl(W3,
  [Key], [Type], [Description],
  [`x`], [double], [*required*, x coordinate along the graph],
  [`graph`], [FunctionGraph], [*required*, the graph the point lies on (aliases
    `funcGraph`, `on`)],
)

```groovy
def fg = funcGraph(func: { x -> Math.sin(x) }, addToScene: true)
def p  = pointOnGraph(x: 1, graph: fg, label: r"$P$",
                      style: [color: "red", dotStyle: "circle", thickness: 4],
                      addToScene: true)
```

`p.getFunctionGraph()` is the graph it lies on, and `p.getSlopePointLeft()` /
`p.getSlopePointRight()` two points giving the tangent there.

== `trail(...)`

An updateable Shape that appends the position of a marker object on every frame,
drawing the path it describes. It only grows while it is in the scene, so it is
normally built with `addToScene: true`; the marker itself is not added.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`marker`], [Boxable], [*required*], [Object to follow, by its center: a
    MathObject or a path point such as `sq[0]` (aliases `obj`, `point`)],
  [`pen`], [String], [`down`], [`down` records from the start; `up` waits until
    `trail.lowerPen()`],
)

```groovy
def P = point(at: [1, 0], style: [thickness: 30, drawColor: "red"],
              addToScene: true)
def t = trail(marker: P, style: [drawColor: "tomato", thickness: 6],
              layer: 1, addToScene: true)
animRotate(obj: P, center: [0, 0], angle: 2 * PI, runtime: 5)
```

A trail starts where the marker was when it was built and records every move
since, so `t.startHere()` right before the animation you want recorded discards
the rest and restarts at the current position.

`t.getMarker()` is the object it follows, and `t.toShape()` an independent
`Shape` with what it has drawn so far.

== `locus(...)`

The curve a point describes when a parameter it depends on is swept over a
range. It is the immediate counterpart of `trail`: a trail records where its
marker has been, one point per frame, so it only exists once the movement has
been played, while a locus is computed in full the moment it is built, and
recomputed whenever the construction changes, without advancing a frame.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`trace`], [Boxable], [*required*], [Object whose position is traced, by its
    center. It has to depend on the parameter (aliases `obj`, `point`,
    `marker`)],
  [`param`], [Parametric], [*required*], [The value to sweep: a `scalar(...)` or
    anything else with an animatable value (aliases `driver`, `of`)],
  [`range`], [`[from, to]`], [`[0, 1]`], [Interval the parameter is swept over
    (aliases `trange`, `prange`). Also as `from:`/`to:`],
  [`numPoints`], [int], [`100`], [Samples taken along the range],
  [`maxJump`], [double], [none], [Distance above which two consecutive samples
    count as a jump, and the stroke is cut instead of crossing the scene],
  [`closed`], [boolean], [auto], [Closes the curve. By default it is decided
    from the samples: closed when the last one falls on the first],
  [`smooth`], [boolean], [`true`], [`false` joins the samples with straight
    segments],
  [`live`], [boolean], [`true`], [`false` freezes the curve, so it stops
    following the construction],
)

```groovy
def r = scalar(1)
def C = ctCircle(center: [0, 0], radius: r, addToScene: true)
def L = ctLine(a: [-3, 0.5], b: [3, 0.5], addToScene: true)
def P = ctIntersectionPoint(a: C, b: L, addToScene: true)
locus(trace: P, param: r, range: [0.5, 3], style: [drawColor: "gold"],
      addToScene: true)
animScalar(obj: r, from: 1, to: 3, runtime: 4)   //P runs along the locus
```

Sweeping the parameter is what draws the curve, so animating it does *not*
recompute the locus: what does is a change in any other input of the
construction. The parameter is put back where it was, and the construction
recomputed with it, before the call returns.

The objects between the parameter and the traced point are driven by
recomputing them, not by updating them, so they have to be constructible ones:
a link whose position comes from an `always` updater does not follow the
parameter while the locus is sampled. Points where the construction is
undefined, an intersection that does not exist, are dropped and cut the stroke.

They are looked for first among what the traced point declares reading, and,
failing that, among what the scene knows to depend on the parameter. The second
case is the usual one for a point taken from the geometry a construction
writes, `ctPoint(at: circle.mathObject[0])`: there the driven objects have to
be in the scene, and the curve is resolved on the first frame.

#lead[Reading it back.]

```groovy
lo.getTraced(); lo.getDriver()      // the traced object, and the swept parameter
lo.getRangeFrom(); lo.getRangeTo(); lo.getNumPoints()
lo.recompute()                      // rebuild it right now
lo.getChain()                       // what stands between the parameter and the point
```

== `parametricCurve(...)`

#tbl(W3w,
  [Key], [Type], [Description],
  [`x`, `y`], [Closure / operator / number], [Cartesian components (`z`
    optional)],
  [`r`, `theta`], [Closure / operator / number], [Polar components (switches to
    polar mode)],
  [`function`], [Closure `{ t -> [x, y] }`], [Whole point at once (also
    `[x,y,z]`, a `Vec`, or `[r, theta]` in polar). Evaluated once per point, so
    both components share any expensive computation. Excludes
    `x`/`y`/`z`/`r`/`theta`],
  [`trange` (`range`)], [`[tmin, tmax]`], [*required* parameter domain],
  [`numPoints`], [int], [Sample points],
  [`coordinates`], [String], [`cartesian` (default) or `polar`],
  [`tpoints`], [Number / List], [Extra parameter values to force as nodes],
  [`style` / `label`], [-], [Style and inline label(s)],
)

```groovy
def c = parametricCurve(x: { t -> Math.cos(t) }, y: { t -> Math.sin(t) },
                        trange: [0, 2*PI])
def rose = parametricCurve(r: { t -> Math.cos(3*t) }, theta: { t -> t },
                           trange: [0, PI])
def sp = parametricCurve(trange: [0, 10], function: { t ->
    def h = Math.exp(-0.1*t)     // computed once, used by both components
    [h*Math.cos(t), h*Math.sin(t)]
})
```

#lead[Reading it back.] Calling the curve reads it at *its own* parameter, the
one of `trange`, and not at a fraction of the length:

```groovy
c(1.2); c.getFunctionValue(1.2)   // the point at t = 1.2; a number or a scalar
c.getTangentVector(1.2, 1)        // tangent there, 1 forwards and -1 backwards
c.getParametrizedVecAt(0.5)       // the other reading: 50% of the length
```

== `contourPlot(...)`

#tbl(W3w,
  [Key], [Type], [Description],
  [`function`], [BiFunction], [`(x,y) -> value` *required*],
  [`levels`], [List or Map `{min,max,step}`], [Contour levels *required*],
  [`rect`], [`[xmin,ymin,xmax,ymax]` / number / preset], [Domain (default:
    camera view). Alias `rect`],
  [`cols` / `rows`], [int], [Grid resolution (default `100`)],
  [`colorScale`], [ColorScale], [Color scale instance],
  [`style`], [Map], [Applied to all contour shapes],
)

```groovy
def cp = contourPlot(
    function: (x, y) -> x*x + y*y,
    levels: [min: 0.5, max: 3.0, step: 0.5],
    cols: 150, rows: 150,
    colorScale: colorScale(0.0: "blue", 1.0: "red"))
```

#lead[Reading it back.] `cp(1.5)`, or `cp.getContourAt(1.5)`, is a new
independent `Shape` with the contour at that level, computed on the same grid.
The level does not have to be one of `levels`, and the components it may have
come joined by invisible segments. The contours already drawn are the children:
`cp[i]`, `cp.size()`, `cp.each { }`.

== `densityPlot(...)`

Pixel heat-map of a scalar field. Added to the scene by default
(`addToScene: false` to opt out).

#tbl(W4w,
  [Key], [Type], [Default], [Description],
  [`function`], [Closure `(x,y)` or `(x,y,t)` / operator], [*required*],
    [Scalar field (`t` is animatable)],
  [`rect`], [`[xmin,ymin,xmax,ymax]` / number / preset], [camera view],
    [Domain (`unit` `centered` `screen` `trig` `trigCentered`). Alias `rect`],
  [`coordinates`], [String], [`cartesian`], [`cartesian` or `polar` (field gets
    `r, theta`)],
  [`colorScale`], [ColorScale], [viridis], [Color scale],
  [`parallel`], [boolean], [`true`], [Parallel row evaluation],
  [`style`], [Map], [-], [Mainly for opacity (`alpha`)],
)

```groovy
def dp = densityPlot(function: { x, y -> Math.sin(x) * Math.cos(y) },
                     rect: "centered")
```

#lead[Reading it back.] `dp.getColorScale()` is the scale it paints with,
`dp.getFunction()` the field it evaluates and `dp.getImage()` the bitmap it
renders.

== `vectorField(...)`

Grid of arrows sampling a planar field. Added to the scene by default.

#tbl(W4w,
  [Key], [Type], [Default], [Description],
  [`function`], [Closure `(x,y)`/`(x,y,t)` → `Vec` or `[vx,vy]`], [*required*],
    [The field (`t` animatable)],
  [`rect`], [`[xmin,ymin,xmax,ymax]` / number / preset], [camera view],
    [Domain (alias `rect`)],
  [`cols` / `rows`], [int], [`15`], [Grid resolution],
  [`arrows`], [String], [`simple`], [`simple` (stroke-only) or `classic` (filled
    head)],
  [`fill`], [boolean], [-], [Filled arrows (default: true for `classic`)],
  [`normalize`], [boolean], [`false`], [Same length for every arrow (direction
    only)],
  [`scaleFactor`], [double], [`0.9`], [Fraction of a cell for the longest arrow],
  [`lengthScale`], [double], [-], [Fixed world length per unit magnitude
    (disables auto-scale)],
  [`autoScale`], [boolean], [`true`], [Auto-scale longest arrow to a cell],
  [`colorScale`], [ColorScale], [-], [Color arrows by magnitude],
  [`colorByMagnitude`], [boolean], [`true`], [Auto viridis coloring by
    magnitude],
  [`parallel`], [boolean], [`true`], [Parallel row evaluation],
  [`style`], [Map], [-], [Applied to every arrow],
)

```groovy
def vf = vectorField(function: { x, y -> Vec.to(-y, x) },
                     rect: [-3, -3, 3, 3], cols: 20, rows: 20,
                     arrows: "classic")
```

#lead[Reading it back.] It is a multi-shape object, one arrow per sample, so
`vf[i]`, `vf.size()` and `vf.each { }` reach them one by one.
`vf.getMaxMagnitude()` is the largest magnitude found, the one the auto-scale
works from.

== `voronoi(...)`

Voronoi diagram of a point cloud, one region (a Shape) per seed, in the same
order as `points`. Seeds are registered as dependencies: moving or animating one
rebuilds the diagram.

#tbl(W4w,
  [Key], [Type], [Default], [Description],
  [`points`], [List of `[x,y]` / Coordinates], [*required*], [Seed points],
  [`rect`], [`[xmin,ymin,xmax,ymax]` / number / Rect / preset], [camera view],
    [Clips the unbounded (hull) regions],
  [`style`], [Map], [-], [Applied to every region],
  [`addToScene`], [boolean], [`false`], [Add on creation],
)

```groovy
def seeds = (1..30).collect { Point.random() }
def v = voronoi(points: seeds, style: [drawColor: "gold", fillAlpha: 0.3])
play.showCreation(3, v)
```

A `transform:` is applied to the region shapes, so it is discarded on the next
rebuild. Transform the seeds instead when the diagram must stay live.

#lead[Reading it back.]

```groovy
v[i]; v.size(); v.each { }        // the regions, in the order of points
v.getRegion(seed)                 // the region of one seed
v.getRegions()                    // seed -> region
v.getDelaunay().getTriangles()    // the triangulation behind the diagram
v.getSeparatingSegment(p1, p2)    // the edge between two seeds, as a Shape
```

== `delaunay(...)`

Delaunay triangulation of a point cloud, as a `MultiShapeObject`. It is a
snapshot: moving the points afterwards does not rebuild it.

#tbl(W4w,
  [Key], [Type], [Default], [Description],
  [`points`], [List of `[x,y]` / Coordinates], [*required*], [At least 3 points],
  [`type`], [String], [`triangles`], [`triangles`, `edges` (deduplicated) or
    `circumcircles`],
  [`style`], [Map], [-], [Applied to every shape],
  [`addToScene`], [boolean], [`false`], [Add on creation],
)

```groovy
def d = delaunay(points: seeds, type: "edges", style: [thickness: 3],
                 addToScene: true)
```

Being a `MultiShapeObject`, `d[i]`, `d.size()` and `d.each { }` reach the
triangles (or the edges, or the circumcircles) one by one.

== `image(...)`

Imports a bitmap image (a `JMImage`) or an SVG one (a `MultiShapeObject`, one
`Shape` per SVG element). Files are looked up in `resources/images`; the `!`
prefix takes an absolute path.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`file`], [String], [*required*], [File name (aliases `filename`, `src`)],
  [`type`], [String], [by extension], [`bitmap` or `svg`, forcing the importer],
  [`preserveRatio`], [boolean], [`false`], [Keep the aspect ratio on resize
    (bitmap only)],
  [`adjustTo`], [`[A, B]`], [-], [Places the lower left / lower right corners on
    A and B],
  [`style` / `layer` / `visible` / `transform` / `stack`], [-], [-],
    [Common props],
  [`addToScene`], [boolean], [`false`], [Add to scene immediately],
)

Sizing goes through the transform map, as with any other object.

```groovy
def img = image(file: "euler.jpg", transform: [height: 2, center: true])
def svg = image(file: "knuth.svg", transform: [height: 2], addToScene: true)
image(file: "photo.jpg", adjustTo: [[-1,-1], [1,-1]])  // corners on two points
```

`label:` is not accepted (images are not label-locatable): stack a
`latex(...)`/`text(...)` object to the image instead.

An SVG import is a multi-shape object, so `svg[0]`, `svg.size()` and
`svg.each { }` reach its elements; a bitmap is a single object.

== `slider(...)`

Visual read-out of a `scalar(...)` inside a range. Everything is placed on every
frame relative to the track, so moving or stacking the slider drags the whole
thing and the marker keeps following the value.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`value`], [Scalar / Parametric / number], [*required*], [The value shown; a
    plain number gives a fixed slider],
  [`from` / `to`], [double], [`0` / `1`], [Ends of the range],
  [`length`], [double], [`2`], [Length of the track],
  [`orientation`], [String], [`horizontal`], [`horizontal` or `vertical`],
  [`at`], [`[x,y]`], [`[0,0]`], [Start of the track (`transform`/`stack` also
    work)],
  [`label`], [String], [-], [Name of the magnitude, at the start of the track],
  [`format`], [String], [`"0.00"`], [Number format of the read-out and the end
    labels],
  [`showValue` / `showEnds`], [boolean], [`true`], [Read-out next to the marker /
    labels of both ends],
  [`gap`], [double], [`0.1`], [Distance from the labels to the track and the
    marker],
  [`labelScale`], [double], [`0.5`], [Scale of every label],
  [`style`], [Map], [-], [Style of the track],
  [`parts`], [Map], [-], [Style of each piece by name: `knob`, `value`, `name`,
    `min`, `max`, plus `labels` and `ends` (see #link(<parts-map>)[`parts:` map])],
  [`knobStyle` / `labelStyle`], [Map], [-], [Old spelling of
    `parts:[knob: ...]` / `parts:[labels: ...]`],
)

```groovy
def a = scalar(0)
slider(value: a, from: 0, to: 1, label: r"$a$",
       stack: [screen: "lower_right", gaps: 0.2], addToScene: true)
animScalar(obj: a, to: 1, runtime: 4)      // the marker moves on its own
```

Returns the track `Shape`. Its pieces are reachable by name: `sl["knob"]`
(`"value"`, `"name"`, `"min"`, `"max"`), and styled with `parts:`.

== `combine(...)`

Boolean operations between shapes. The operands are never modified: a new `Shape`
is built, with the style of the first operand unless `style:` says otherwise.
Three or more operands are folded from left to right.

#tbl(W3w,
  [Key], [Type], [Description],
  [`type`], [String], [*required*: `union`, `intersect`, `subtract` (aliases
    `substract`, `difference`)],
  [`obj`], [List], [Two or more Shapes; excludes
    `from`/`with`],
  [`from` / `with`], [Shape / Shape or List], [Pair form, instead of `obj`],
  [`style` / `layer` / `visible` / `transform` / `stack` / `label` /
    `addToScene`], [-], [Common props, applied to the result],
)

```groovy
def r = combine(type: "intersect", obj: [A, B, C], style: "solidRed",
                addToScene: true)
def d = combine(type: "subtract", from: A, with: B)
```

Artifacts are possible when several operations are chained on complex paths, a
known limitation of the underlying JavaFX algorithm.

== `scalar(...)` <scalar>

A number, not drawn and not a `MathObject`. Being `Parametric`, it is the target
of `animScalar(...)`, so updaters, links and the `t` of a field can follow a
value that moves over time. It is also the number a construction is built on: a
radius, an angle, a ratio.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`value`], [double], [`0`], [The number itself (alias `at`)],
  [`expression`], [String], [-], [Formula in mXparser syntax over the names of
    `vars` (aliases `expr`, `formula`)],
  [`vars`], [Map], [`[:]`], [What each name of the formula stands for (aliases
    `variables`, `args`)],
  [`of`], [Parametric], [-], [Another animatable value to follow],
  [`distance`], [`[A, B]`], [-], [Distance between 2 points],
  [`radius`], [Circle], [-], [Radius of a circle, an arc or a sector],
  [`angle`], [`[A, O, B]`], [-], [Angle at the vertex, counterclockwise from A to
    B, in $[0, 2 pi)$],
  [`area`], [`ctPolygon`], [-], [Area it encloses, always positive],
)

The forms are exclusive. Every one but `value` builds a *derived* scalar:
recomputed whenever any of the objects it reads changes, so whatever is built on
it follows, and read-only, since its value comes from its own definition.

Each name of `vars` becomes a variable of the formula: another scalar is read
live and declared as an input, a plain number is a constant. The formula is
mXparser's own syntax, so `sin`, `ln`, `pi`, `e` and the rest are available as
they come, and it is checked when the scalar is built: a name the formula uses
and `vars` does not give raises there instead of quietly evaluating to `NaN` for
the whole animation.

```groovy
def t = scalar(0)                        // shorthand: scalar(value: 0)
always(obj: dot, dependsOn: t) { it.moveTo(Math.cos(t.value),
                                           Math.sin(t.value)) }
animScalar(obj: t, from: 0, to: 2*PI, runtime: 3)

def r = scalar(expression: "1 + cos(t)", vars: [t: t])   //follows t
def C = ctCircle(center: [0, 0], radius: r, addToScene: true)
def d = scalar(distance: [A, B])
```

Read and write a literal one with `t.value`; `t.add(delta)` and
`t.lerp(target, alpha)` are also available.

== `arrow(...)`

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`from`], [`[x,y]` / Coords], [*required*], [Start point],
  [`to`], [`[x,y]` / Coords], [*required*], [End point],
  [`head`], [int / String], [`ARROW1`], [End arrowhead],
  [`tail`], [int / String], [`NONE_BUTT`], [Start arrowhead],
  [`curvature`], [double], [-], [Arrow curvature],
  [`arrowThickness`], [double], [-], [Arrowhead thickness],
  [`startScale` / `endScale`], [double], [-], [Arrowhead scale],
  [`label`], [String / Map], [-], [Text label],
  [`lengthLabel`], [String / Map], [-], [Length label],
  [`vecLabel`], [String / Map], [-], [Vector label],
  [`style`], [Map], [-], [Style],
)

#lead[Arrow types:] `NONE_BUTT`(0) `ARROW1`(1) `ARROW2`(2) `ARROW3`(3)
`SQUARE`(4) `BULLET`(5)

```groovy
def ar = arrow(from: [0,0], to: [1,1], head: "ARROW2", tail: "NONE_BUTT",
               label: [text: r"v", scale: 0.8, gap: 0.05])
```

#lead[Label sub-map keys:] `text`, `format`, `t` (location 0-1), `color`,
`drawColor`, `fillColor`, `thickness`, `scale`, `gap`, `upside`, `anchor`,
`rotation` (`FIXED` `ROTATE` `SMART`), `style` (sub-map), `transform` (sub-map)

== `connector(...)`

Same params as `arrow(...)` plus:

#tbl(W3w,
  [Key], [Type], [Description],
  [`from`], [MathObject], [*required* (source object)],
  [`to`], [MathObject], [*required* (destination object)],
  [`type`], [String], [Body of the connector: `straight` (default) or `stepped`],
  [`tail`], [int / String], [Arrowhead at the start (default `NONE_BUTT`), as in
    `arrow(...)`],
  [`anchorStart` / `anchorEnd`], [AnchorType], [Fixed exit and entry sides,
    instead of letting `connectionType` choose (aliases `originAnchor`,
    `destinyAnchor`)],
  [`stepDirection`], [String], [For `stepped`: `AUTO` (default), `HVH`
    (horizontal, vertical, horizontal) or `VHV`],
  [`connectionType`], [String], [Exit strategy: `auto` (default), `fixed`,
    `boxed`, `circular`, `inscribed`, `ellipse`, `center`. Works for stepped too
    (e.g. `center` routes between the object centers). Must be set (not `auto`)
    for the scale params below to apply],
  [`startScaleBBox`], [double], [Multiplicative bbox start scale (default `1`;
    grows with the object; ignored when `connectionType` is `auto` or `center`)],
  [`endScaleBBox`], [double], [Multiplicative bbox end scale (default `1`;
    ignored when `connectionType` is `auto` or `center`)],
  [`gaps`], [number / `[h,v]`], [Fixed clearance added to each object's bbox
    before computing the exit (a constant distance, unlike the scale). Works with
    any `connectionType`, incl. `center` (then exits a fixed distance from the
    center)],
)

```groovy
def con = connector(from: shapeA, to: shapeB, head: 2,
                    curvature: 0.3, label: "f")
```

== `delimiter(...)`

#lead[Standard form:]

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`from`], [`[x,y]` / Coords], [*required*], [Start point],
  [`to`], [`[x,y]` / Coords], [*required*], [End point],
  [`type`], [String], [`BRACE`], [`BRACE` `PARENTHESIS` `BRACKET` `INVISIBLE`
    `LENGTH_ARROW` `LENGTH_BRACKET`],
  [`gap`], [double], [`0.1`], [Distance from anchor to delimiter body],
)

#lead[Stacked form:]

#tbl(W3,
  [Key], [Type], [Description],
  [`stackedTo`], [MathObject], [*required* (object to stack to)],
  [`anchor`], [String], [`UPPER`(def) `LOWER` `LEFT` `RIGHT`],
  [`type`], [String], [See above],
  [`gap`], [double], [Distance from object],
)

#lead[Common optional params:]

#tbl((25%, 75%),
  [Key], [Description],
  [`delimiterScale`], [Scale of the delimiter shape],
  [`amplitudeScale`], [Scale of the delimiter amplitude],
  [`labelGap`], [Distance from label to delimiter shape],
  [`label`], [String, Map `{text, color, scale, gap, rotation, style, transform}`
    or `[object: mathObject]`],
  [`lengthLabel`], [String or Map
    `{format, color, scale, gap, rotation, style, transform}`],
  [`rotation`], [`FIXED` `ROTATE` `SMART`],
  [`style`], [Style map],
)

```groovy
def d = delimiter(from: [0,0], to: [2,0], type: "BRACE", gap: 0.15,
                  label: [text: r"$L$", scale: 0.8, gap: 0.05])

def d = delimiter(stackedTo: myShape, anchor: "UPPER", type: "BRACKET",
                  lengthLabel: [format: "0.00", color: "blue"])
```

#lead[Reading connectors and delimiters back.] Both keep their ends, their body
and their label:

```groovy
ar.getStart(); ar.getEnd()        // the two ends of an arrow or a connector
ar.getMathObject()                // the body it draws, a Shape
ar.getLabel(); ar.getLabelTip()   // the label object, and the tippable holding it
ar.getTypeA(); ar.getTypeB()      // the two arrow heads
d.getA(); d.getB()                // the two ends of a delimiter
d.getMathObject()                 // everything it draws, as one group
```

== `label(...)`

Attaches a tippable object to a path. Exactly one *mode* is required: `text` (a
`LabelTip`), `type` (an arrowhead / equal-mark `TippableObject`), or `object`
(any MathObject as the tip).

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`path`], [Pathable], [*required*], [Path object to attach to],
  [`text`], [String], [-], [_Mode:_ LaTeX label text],
  [`type`], [String], [-], [_Mode:_ `arrow1`…`arrow3`, `square`, `bullet`, or
    `equal1`…`equal4`. Arrow names take a `<` prefix (head points backwards) and
    an `xN` suffix (N chevrons), e.g. `<arrow2x3`],
  [`object`], [MathObject], [-], [_Mode:_ arbitrary object as the tip],
  [`location` (`t`)], [double], [`0.5`\*], [Position along path `[0,1]`
    (\*path-dependent default)],
  [`normalized`], [boolean], [`true`], [`t` measured along arc length; `false`
    weights every side equally (alias `parametrized`)],
  [`vertex`], [int], [-], [Anchor at vertex \#n (outward corner bisector), wraps
    circularly],
  [`side`], [int], [-], [Midpoint of side \#n, sides equally weighted, wraps
    circularly],
  [`upside`], [boolean], [-], [Place above path],
  [`gap`], [double], [-], [Distance to shape],
  [`gapRelative` (`gapIsRelative`)], [boolean], [`true`], [Gap relative to size],
  [`rotation`], [String], [-], [`FIXED` `ROTATE` `SMART`],
  [`rotateDirection`], [double], [-], [Angle (radians) the tip is turned away
    from the tangent: `PI/2` places it perpendicular to the path],
  [`slopeDirection`], [String / boolean], [-], [`POSITIVE` `NEGATIVE`],
  [`anchor`], [AnchorType], [-], [Anchor of the tip; on a *point* label it
    selects the side of the point the label goes to (`left`, `upper`, `diag1`…)],
  [`scale`], [double / `[sx,sy]`], [-], [Scale the tip],
  [`rotate`], [double], [-], [Rotate the tip (radians)],
  [`style`], [Map / String], [-], [Style properties],
  [`color` / `drawColor` / `fillColor` / `thickness`], [String / double], [-],
    [Flat style shortcuts],
)

```groovy
def lt = label(path: myShape, location: 0.5, text: r"$A$",
               upside: true, gap: 0.1, rotation: "rotate")
def ah = label(path: seg, type: "arrow2", t: 1)
def em = label(path: seg, type: "equal2")   // equal-length marks
```

#tip[
  `delimiter(...)` accepts a nested `labelTip(...)` block for its own label; that
  is a different, delimiter-scoped helper, not this top-level command.
]

= Text & LaTeX

== `latex(...)`

#tbl(W3,
  [Key], [Type], [Description],
  [`text`], [String], [*required* (LaTeX expression)],
  [`anchor`], [String], [Anchor side used to position the object],
  [`style`], [Map / String], [Style properties or named style],
  [`latexStyle`], [LatexStyle / String], [Per-glyph coloring style (see
    `latexStyle(...)`) or a registered name],
  [`colorToIndices`], [String / List], [Per-glyph coloring by index (see below)],
  [`colorToTags`], [List], [Per-region coloring by `\jmtag` name (see below).
    Alias: `colorToTag`],
  [`format`], [String / List], [Number format of the `{#0}`..`{#9}` arguments:
    one pattern for all of them, or one per argument],
  [`layer` / `visible` / `transform`], [-], [Common props],
)

```groovy
def eq = latex(text: r"$e^{i\pi}+1=0$")
def q  = latex(text: "$x^2+y^2$", colorToIndices: [["blue", 0, 1],
                                                   ["red", 3, 4]])
def r  = latex(text: r"$\jmtag{lhs}{x^2}=\jmtag{rhs}{y}$",
               colorToTags: [["blue", "lhs"], ["red", "rhs"]])
```

`colorToIndices` forms: `"blue"` (all glyphs), `["blue", 1, 2]` (glyphs 1 & 2),
`[["blue", 1, 2], ["red", 3, 5]]` (several groups); indices accept ranges like
`0..5`.

`colorToTags` forms: `["blue", "region1"]` (the glyphs of that region),
`["blue", "r1", "r2"]` (both regions), `[["blue", "lhs"], ["red", "rhs"]]`
(several groups). The names are the ones written in the `\jmtag{name}{...}`
macros of the formula.

#lead[Reading it back.] A formula is a multi-shape object whose children are its
glyphs, in the order jlatexmath laid them out:

```groovy
eq[0]; eq[-1]; eq[2..5]; eq.size()   // glyph(s), and how many there are
eq.each { it.fillColor("gold") }     // iterable, like any multi-shape object
eq as List; eq as Shape              // the glyphs as a List / merged into one path
eq.getShapesOfTag("lhs")             // the glyphs of a \jmtag region...
eq.getIndicesOfTag("lhs")            // ...or their indices
eq.getShapesOfArg(0)                 // the glyphs a {#0} argument produced
eq.getShapesWith(token)              // the ones matching a LatexToken
eq.getLatexParser().get(3)           // the LatexToken of glyph 3
eq.getText()                         // the compiled source
eq.getBaselinePoint()                // where its baseline sits
```

#lead[Numbered arguments.] `{#0}` to `{#9}` in the text stand for a `Scalar` the
formula owns, and it recompiles itself whenever one of them changes.
`eq.getArg(n)` is that very scalar, so it is what an animation writes on, and
`format:` decides how it is printed:

```groovy
def eq = latex(text: r"$t = {#0}$", format: "0.00")
animScalar(obj: eq.getArg(0), from: 0, to: 1, runtime: 3)
```

== `text(...)`

Plain (non-LaTeX) text.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`text`], [String], [*required*], [The text to display],
  [`font`], [String], [-], [Font family, e.g. `"Serif"`],
  [`fontSize` (`height`, `size`)], [double], [`0.5`], [Text height in math
    units],
  [`bold` / `italic` / `outline`], [boolean], [-], [Font flags],
  [`format`], [String / List], [`0.00`], [Number format of the `{#0}`..`{#9}`
    arguments: one pattern for all of them, or one per argument],
  [`style`], [Map / String], [-], [Style properties],
)

```groovy
def t = text(text: "Hello", font: "Serif", fontSize: 0.8,
             style: [color: "blue"])
```

#lead[Reading it back.] A `TextMathObject` is one object and not a collection of
glyphs, so it takes no subscript. It does carry the same numbered arguments as a
formula: `{#0}` to `{#9}` in the text, `t.getArg(0)` to reach one and `format:`
to print it.

== `latexStyle(...)`

Declaratively colors the glyphs of a LaTeX formula with ordered rules (later
rules override earlier ones). See the _Coloring Formulas_ chapter for the full
token model.

#tbl(W3,
  [Key], [Type], [Description],
  [`rules`], [List of Maps], [*required* (coloring rules, see below)],
  [`apply`], [LatexMathObject / List], [Apply the built style to these
    formula(s)],
  [`name`], [String], [Register the style under this name for reuse],
)

Each rule has a *paint* part (`color` for both draw and fill, `drawColor` /
`fillColor` for each one separately, or a full `style` map/name) and optional
*conditions*: `char` (exact CHAR), `match` / `differ` (token spec on the glyph),
`prev` / `next` (neighbour token specs). A token spec is a String (its LaTeX
name) or a Map (`type`, `string`, `subscript`, `depth`, ...).

```groovy
latexStyle(apply: formula, name: "rowColMatrix", rules: [
    [char: "X", color: "steelblue"],
    [match: [subscript: true], color: "gold"],
])
```

A rule can also compute the color of each glyph with a closure, in `rule` (called
on every glyph) or in `color` (only on those matching the conditions). It
receives a `LatexGlyphContext` (`token`, `previousToken`, `nextToken`, `index`,
`string`, `type`, `isDigit()`, `digitValue`, `shape`, ...) and returns a color, a
`PaintStyle`, or `null` to leave that glyph alone. Listing the objects it reads
in `dependsOn` makes the formula recolor itself whenever one of them changes in
place.

```groovy
latexStyle(apply: formula, rules: [
    [rule: { ctx -> ctx.isDigit() ? A.interpolate(B, ctx.digitValue / 9d)
                                  : null },
     dependsOn: [A, B]],
])
A.copyFrom(JMColor.parse("white"))   // it recolors itself on the next frame
```

== `apply(...)`

Applies styling and common props to one or many existing objects.

#tbl(W3,
  [Key], [Type], [Description],
  [`obj`], [object / List / array], [*required*, target(s)],
  [`style`], [Map / String], [Style properties],
  [`parts`], [Map], [Style of the pieces of a composite object by name (see
    #link(<parts-map>)[`parts:` map])],
  [`state`], [Map], [Named values of an object with state, set without animating:
    `state:[aperture: 1]`],
  [`transform`], [Map / AffineJTransform], [`scale`, `shift`, `rotate`,
    `affine`...],
  [`layer` / `visible` / `stack`], [-], [Common props],
  [`homogeneize`], [double / Map], [Equalize bounding boxes, before
    `transform`/`stack`. Several objects in `obj` are equalized among themselves;
    a single group has its elements equalized (see `group(...)`). Same syntax as
    in `group(...)`],
  [`layout`], [String / Map / GroupLayout], [Arrange the children of a container
    that already exists. Same syntax as in `group(...)`; non-containers are
    ignored with a warning],
  [`align`], [AlignType / String], [Align objects against a common bounding box,
    after the layout. Several objects in `obj` are aligned among themselves; a
    single group has its elements aligned (see `group(...)`)],
  [`gap` / `gaps`], [double / `[h,v]`], [Gap between children, only when `layout`
    is a LayoutType name],
)

```groovy
apply(obj: [a, b, c], style: [drawColor: "blue"], transform: [rotate: PI/2])
apply(obj: [t1, t2], homogeneize: [anchor: "baseline", gaps: 0.1])
apply(obj: g, homogeneize: [anchor: "lower", gaps: 0.1])
apply(obj: g, align: "upper")
apply(obj: boxes, layout: [type: "box", size: 4, gaps: 0.1,
                           refPoint: refPoint])
apply(obj: mySlider, parts: [knob: [color: "green"],
                             labels: [drawAlpha: 0.5]])
```

A single object that is not a group has nothing to be equalized with, so it is
ignored with a warning.

= Scene

== `camera(...)`

Immediate (non animated) camera state: the static counterpart of
`animCamera(...)`. Entries are applied *in the order they are written*, like the
`transform:` map. Returns the affected `Camera`.

#tbl((17%, 27%, 56%),
  [Key], [Type], [Description],
  [`camera`], [Camera / String], [Camera acted on: `default` (scene camera) or
    `fixed`. Read first, wherever it is written],
  [`rect`], [Rect / `[xmin,ymin,xmax,ymax]` / number / preset], [Region shown
    (alias `zoomToRect`)],
  [`zoomToObjects`], [object / List], [Zooms the view to the objects (in or out)],
  [`adjustToObjects`], [object / List], [Zooms *out* only: leaves the view alone
    if they already fit],
  [`centerAtObjects`], [object / List], [Centers on the objects, then adjusts the
    zoom],
  [`zoomToAllObjects` / `adjustToAllObjects` / `centerAtAllObjects`], [boolean],
    [Same three actions over the whole scene],
  [`center`], [`[x,y]` / Coords], [Center of the view (alias `moveTo`)],
  [`shift`], [`[dx,dy]` / Coords], [Translation of the view],
  [`rotate`], [double], [Angle the camera is turned to, in radians,
    counterclockwise, around the center of the view, so the scene appears rotated
    clockwise (alias `rotation`)],
  [`rotateBy`], [double], [Angle added to the current rotation],
  [`scale`], [double], [Scales the view (`> 1` zooms out)],
  [`zoom`], [double], [Reciprocal of `scale` (`zoom: 5 == scale: 0.2`)],
  [`width`], [double], [Width of the math view, in math units],
  [`gaps`], [double / `[h,v]`], [Margin left around the objects by the actions
    above. Read first, wherever it is written],
  [`centered`], [boolean], [Whether the zoom/adjust actions recenter the view on
    the objects (default `true`). Read first, wherever it is written],
  [`save` / `restore` / `reset`], [boolean], [Save, restore or reset the camera
    view],
)

```groovy
camera(zoomToAllObjects: true, gaps: 0.1)
camera(zoomToObjects: [a, b], centered: false)   //fit them without panning
camera(rect: [-1, -1, 1, 1])
camera(center: [1, 0], scale: 2)
camera(camera: "fixed", scale: 1.5)
camera(rotate: 15*DEGREES)                       //tilt the view
```

#lead[Reading it back.] `camera.mathView` is the `Rect` currently shown (so
`camera.mathView.center`, `.width`, `.upperLeft`), and
`camera.mathToScreen(x, y)` / `camera.screenToMath(x, y)` convert between the
two coordinate systems.

#lead[With a rotated camera] `camera.mathView` is no longer what is on screen:
it keeps meaning the center, the zoom level and the aspect ratio, while
`camera.visibleMathBoundingBox` is the smallest `Rect` containing everything the
camera shows, which is what objects reaching the whole screen (`line`, `ray`,
`axes`, grids, dynamic `funcGraph`) are drawn against. Objects drawn with the
fixed camera do not rotate.

== `config(...)`

Global scene configuration, replacing a block of `config.setXXX(...)` calls.
Belongs to the configuration part of the script: the library refuses to change
the media size or the fps once the sketch is running.

#tbl(W3w,
  [Key], [Type], [Description],
  [`quality`], [String], [`low` (854x480, 30fps), `medium` (1280x720, 60fps),
    `high` (1920x1080, 60fps). Applied first],
  [`size`], [`[w,h]`], [Output size in pixels],
  [`mediaWidth` / `mediaHeight`], [int], [Output size (aliases
    `mediaW`/`width`, `mediaH`/`height`)],
  [`fps`], [int], [Frames per second],
  [`createMovie` / `saveToPNG`], [boolean], [Video file / one png per frame],
  [`outputDir` / `outputFileName`], [String], [Destination of the generated file],
  [`showPreviewWindow`], [boolean], [Show the preview window (default `true`)],
  [`previewWindowSize`], [`[w,h]`], [Size of the preview window],
  [`backgroundColor`], [String / PaintStyle], [Background color],
  [`backgroundImage`], [String], [Image file in the resources images folder],
  [`drawShadow`], [boolean], [Global shadow effect],
  [`shadow`], [Map], [`[kernel, offsetX, offsetY, color, alpha]` in pixels
    (`kernelSize` is an alias of `kernel`); also enables the shadow],
  [`resourcesDir`], [String], [Resources directory],
  [`secondaryResourcesDir`], [String], [Second folder searched when a resource is
    not in `resourcesDir`; `null` clears it],
  [`soundsEnabled` / `ffmpeg`], [boolean / String], [Sound processing and path to
    the ffmpeg executable],
  [`loggingLevel`], [String / int], [`off` `error` `warn` `info` `debug`, or
    `0`..`4`],
  [`limitFPS` / `printProgressBar` / `showDebugFrameNumbers`], [boolean],
    [Misc flags],
  [`defaultLambda`], [String / Closure], [Default timing function for every
    animation],
  [`load`], [String], [Config file parsed first, so the rest of the map overrides
    it (alias `file`)],
)

```groovy
config(mediaWidth: 800, mediaHeight: 600, fps: 25,
       createMovie: true, outputFileName: "animation",
       backgroundColor: "white")
config(quality: "high", shadow: [kernel: 8, offsetX: 5, offsetY: 5,
                                 alpha: 0.5])
```

Setting a media size (`quality`, `size`, `mediaWidth`/`mediaHeight`) inside the
GUI overrides the size the editor had given the preview window, so the sketch is
drawn at one size and shown fitted into another: objects look cut off or
off-centre although the camera is right. A warning is logged when this happens.
Leave the size out while you work in the preview and set it only for the final
render.

= Layouts

== `group(...)` <group>

Builds a `MathObjectGroup` from existing objects (`obj`) or from `number` copies
of one (`copies`), optionally arranged with a `layout`.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`obj`], [object / List / Map], [-], [Objects forming the group; a Map
    `[name: obj, ...]` names each element (excludes `copies`)],
  [`copies`], [object], [-], [Object to copy `number` times (excludes `obj`)],
  [`number`], [int], [-], [Number of copies, required with `copies`],
  [`layout`], [String / Map / GroupLayout], [-], [Layout arranging the elements],
  [`gap`], [double], [`0`], [Gap, only when `layout` is a LayoutType name],
  [`homogeneize`], [double / Map], [-], [Equalize the bounding boxes of all
    elements, before the layout],
  [`align`], [AlignType / String], [-], [Align every element against the bounding
    box of the group, after the layout],
  [`init`], [Closure], [-], [`{ obj -> ... }` run once on the finished group, to
    place an element against a sibling],
)

`homogeneize` gives every element the same bounding box (the largest width and
height in the group) plus extra gaps. `true` uses the `center` anchor with no
gaps; a plain number applies that gap to the four sides with a `center` anchor; a
map takes `anchor` (`center` `upper` `rupper` `right` `rlower` `lower` `llower`
`left` `lupper` `baseline` `lbaseline` `rbaseline`), `axis` (`both` by default,
`horizontal` to equalize only the widths, `vertical` only the heights) and `gaps`
(a number, `[h, v]`, or `[upper, right, lower, left]`). The extra gaps are always
applied to the four sides, whatever the axis.

The three `baseline` anchors are the ones for texts: the common height becomes
the largest ascent above the baseline plus the deepest descent below it, and
every baseline lands at the same height inside its box, so whatever positions the
boxes afterwards (a layout, a stack, labels) keeps the texts on one line. Objects
that do not know their baseline sit on it with their lower side.

```groovy
def g = group(obj: [latex(text: "aqua"), latex(text: "blue"),
                    latex(text: "$x^2$")],
              homogeneize: [anchor: "baseline", axis: "vertical"],
              layout: "right")
```

With `axis: "vertical"` the widths are left as they are, which is what you want
for a row of texts: they share the line but each one keeps its own width.

`align` moves every element against the bounding box of the group itself:
`upper`, `lower`, `left`, `right`, `hcenter`, `vcenter` or `baseline`. It runs
after the layout, so in one call the layout arranges the elements and `align`
corrects them afterwards. `baseline` aligns the baselines of the texts with each
other, the same thing `g.alignBaselines()` does.

```groovy
def g4 = group(obj: [a, b, c], layout: "lower", align: "left")
```

A Map in `obj:` names the elements: the keys go into the dictionary of the group,
in writing order, so each element is reachable with `g["name"]` and addressable
in a #link(<parts-map>)[`parts:` map].

```groovy
def g  = group(obj: [a, b, c], homogeneize: 0.1, layout: "right")
def g2 = group(obj: [a, b, c], homogeneize: [anchor: "lower",
                                             gaps: [0.1, 0.2]])

def g3 = group(obj: [body: shape(type: "circle"),
                     head: shape(type: "square")],
               layout: "upper", gap: 0.1)
g3["head"].color("red")
apply(obj: g3, parts: [head: [fillColor: "gold"]])
```

#lead[Reading it back.]

```groovy
g[0]; g[-1]; g[0..2]; g["head"]   // by index, from the end, a range, by name
g.size(); g.each { }; g.collect { it.center }
g.getObjects(); g as List          // the elements as a plain List
g.getDict()                        // name -> element, for the named ones
g.indexOf(obj)
```

== `compound(...)`

Builds a `CompoundMathObject`: a composite object made of named pieces. Takes the
same parameters as #link(<group>)[`group(...)`], but the result is a scene object
of its own.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`from`], [group], [-], [Group whose members become the pieces, with their
    names. The same objects, not copies],
  [`obj`], [object / List / Map], [-], [Pieces forming the compound; a Map
    `[name: obj, ...]` names each piece (excludes `copies`)],
  [`copies`], [object], [-], [Object to copy `number` times (excludes `obj`)],
  [`number`], [int], [-], [Number of copies, required with `copies`],
  [`layout`], [String / Map / GroupLayout], [-], [Layout arranging the pieces],
  [`gap`], [double], [`0`], [Gap, only when `layout` is a LayoutType name],
  [`homogeneize`], [double / Map], [-], [Equalize the bounding boxes of all
    pieces, before the layout],
  [`align`], [AlignType / String], [-], [Align every piece against the bounding
    box of the compound, after the layout],
  [`state`], [Map / List], [-], [Named values the geometry follows:
    `[aperture: 0]`, a `Scalar` the script already has, or
    `[fate: ["dead", "alive"]]` for one that takes named states],
  [`pose`], [Closure], [-], [`{ obj -> ... }` placing the pieces from the state
    alone, off their rest position (excludes `rebuild`)],
  [`rebuild`], [Closure], [-], [`{ obj -> ... }` moving the pieces by the
    difference (excludes `pose`)],
  [`init`], [Closure], [-], [`{ obj -> ... }` run once on the finished object, to
    place a piece against a sibling],
)

The difference with `group` is where the pieces live. A group is a logical
container: adding it to the scene adds its members one by one, and the group
itself draws nothing. A compound _is_ the scene object: it is added, drawn,
removed, layered and animated as a single thing, and its pieces are internal to
it. Use `group` to operate in bulk on objects that are their own citizens, and
`compound` to build a new object out of parts.

The names given in `obj:` go into the dictionary of the compound, in writing
order, which is also the drawing order. Each piece is reachable with `c["name"]`
and addressable in a #link(<parts-map>)[`parts:` map].

`c.name` also works, since Groovy falls back to the `get(String)` of the
compound, but only as a fallback: a real property of the object is found first.
`c.upper`, `c.left`, `c.right`, `c.center`, `c.width`, `c.boundingBox`,
`c.layer`, `c.visible`, `c.mp`, `c.objects` and `c.path` answer the property and
never the piece, and since a `Boxable` anchor is a usable point the mistake does
not fail, it just places things wrong. The subscript has no fallback and always
means the piece, and the builder warns when a piece name collides with a
property.

The pieces are reachable by index too, `c[0]`, `c[-1]`, `c[0..2]`, `c.size()`,
and the compound is iterable, `c.each { }`. `c.parts()` lists every name it
exposes.

`layer` places the compound among the scene objects and flattens the layers of
its pieces; the layers of the pieces only order them among themselves, so to put
one piece over another set its layer after creating the compound.

#lead[From a group you already built.] `from:` takes the members of a group as
the pieces, with the names they carry there. They are the very same objects, so
this converts a group into a single object instead of duplicating anything; take
them out of the scene first if they had been added on their own. The same thing
without a block is the coercion `myGroup as CompoundMathObject`.

```groovy
def pieces = group(obj: [base: sh1, arm: sh2])
def obj = compound(from: pieces, style: [color: "gray"],
                   parts: [arm: [thickness: 4]])
def quick = pieces as CompoundMathObject   // no block, no extra parameters
```

```groovy
def balance = compound(
    obj: [base: shape(type: "square"),
          arm : shape(type: "segment", from: [-2, 1], to: [2, 1]),
          panL: shape(type: "polygon",
                      points: [[-2,1],[-1.2,1],[-1.35,.65],[-1.85,.65]]),
          panR: shape(type: "polygon",
                      points: [[1.2,1],[2,1],[1.85,.65],[1.35,.65]])],
    style: [color: "gray"],
    parts: [panL: [fillColor: "red"], "pan*": [thickness: 3]],
    addToScene: true)

balance["arm"].color("black")
animRotate(obj: balance, angle: 7 * DEGREES, center: [0, 1])
// the whole object moves as one
```

#lead[Placing a piece against a sibling (`init:`).] The values of the `obj:` map
are evaluated before the container exists, so a piece cannot refer there to
another one. `init:` is a closure run once on the finished object, when they are
all in place under their names:

```groovy
def cat = compound(
    obj:  [box:    shape(type: "rectangle", rect: [-1, -.5, 1, .5]),
           status: latex(text: "?")],
    init: { c -> c["status"].stack()
                            .withDestinyAnchor(AnchorType.UPPER)
                            .toObject(c["box"]) })
```

It runs last, after `layout:`, `transform:`, `style:` and `parts:`, so nothing
the block does can undo it, and before the first `rebuild:`, so it is the right
place to set the geometry the state starts from. `group(...)` takes it too. The
alternative without `init:` is to build the piece into a variable first and refer
to it in the map, which is often clearer for a single reference.

#lead[Compounds with behaviour (`state:` and `pose:`).] A compound can derive its
geometry from a few named values instead of being placed once and for all.
`state:` declares them, `pose:` says where the pieces go, and the closure runs at
the start and then only when a value has actually changed:

```groovy
def box = compound(
    obj:   [bottom: shape(type: "rectangle", rect: [-1, -.5, 1, -.4]),
            left:   shape(type: "rectangle", rect: [-1, -.5, -.9, .5]),
            right:  shape(type: "rectangle", rect: [.9, -.5, 1, .5]),
            lid:    shape(type: "rectangle", rect: [-1, .4, 1, .5])],
    state: [aperture: 0],                        // 0 closed, 1 open
    pose: { b ->
        b["lid"].rotate(b["left"].boundingBox.upperLeft,
                        125*DEGREES * b.state("aperture"))
    })

apply(obj: box, state: [aperture: 1])                // set it, no animation
animState(obj: box, to: [aperture: 1], runtime: 1.5) // animate it
box.state("aperture")                                // read it
```

With `pose:` the pieces are put back to the position they were built in before
every run, so the closure reads `obj.state("n")` and places them outright: values
set backwards, animations interrupted halfway and closures that are not exact
inverses of themselves all stop mattering. The rest position follows the object,
so shifting or rotating the whole compound keeps working; moving one _piece_ by
hand does not, and it is put back at the next change of a value.

The restore is not free: it copies the path of every piece on each frame where a
value changed. `rebuild:` is the same closure written in differences, for when
that shows: the pieces are left where the previous run put them and
`obj.delta("n")` is what moves them, at the price of a closure that has to be an
exact inverse of itself. `obj.previousState("n")` is the value the geometry on
screen was built from. Only one of the two may be written.

Either way, build the pieces in their initial-state position, since the closure
first runs after the layout.

A value may also be a `Scalar` the script already has (`state: [aperture: t]`),
and then whatever drives that `Scalar` (a slider, an `animScalar`, an updater)
drives the compound too. A copy still gets a `Scalar` of its own.

A value declared with a list of names takes one of them instead of a range of
numbers, the first being the initial state. Underneath it is still a value
holding an index, so it is copied and restored like any other, but it reads and
writes with the word and `animState` refuses it, since there is nothing in
between two named states:

```groovy
state: [aperture: 0, fate: ["undecided", "dead", "alive"]]

box.stateName("fate")             // "undecided"
box.previousStateName("fate")     // its state at the previous run
apply(obj: box, state: [fate: "alive"])
```

The state and the parts are different namespaces: `box["lid"]` is a piece,
`box.state("aperture")` is a value. For methods of your own
(`box.openBox(1.5)`) subclass `StatefulCompound` in Java or Groovy
(`setAbsoluteRebuild(true)` in the constructor is what `pose:` does); the same
`apply(state:)` and `animState(...)` work on it.

== `layout(...)`

Builds a `GroupLayout` (usable with `.setLayout(...)` or the `animSetLayout`
DSL). Selected by `type` (default `simple`). Common param `gaps` accepts a number
or `[h, v]`. With `apply`, the layout is applied immediately to the given
group(s); `homogeneize` (same syntax as in `group(...)`) equalizes bounding boxes
first, reading `apply` the way `apply(...)` reads `obj`: several objects are
equalized among themselves, a single group has its elements equalized.

`apply` and `homogeneize` belong to the `layout(...)` call only. The same map
nested in another block (`group(layout: [...])`, `apply(layout: [...])`,
`animSetLayout(layout: [...])`, or `outer`/`inner` of a `compose` layout) refuses
both with a warning: there the object holding the map is the one laid out, and
the block that owns the objects is where they are named.

The bounding boxes used to arrange the elements include their internal objects
(labels, marks...), so a labeled object is not overlapped by its neighbours. Pass
`ignoreInternals: true` (singular `ignoreInternal` is accepted too, and
`.setIgnoreInternal(true)` on the layout) to arrange the objects themselves,
ignoring their labels. The `stack:` map accepts the same key.

#tbl(W3b,
  [`type`], [Required], [Optional],
  [`simple`], [`layout` (LayoutType name)], [`refPoint`],
  [`box`], [`rowSize`], [`corner`, `direction` (BoxDirection)],
  [`flow`], [`width`, `corner`], [`direction`],
  [`circular`], [-], [`center` (default `[0,0]`), `radius` (default `1`),
    `clockwise`, `rotation`, `initialAngle`],
  [`spiral`], [-], [`orientation` (SpiralOrientation), `center`, `spiralGap`],
  [`heap`], [-], [`base`],
  [`pascal`], [-], [`refPoint`],
  [`path`], [`path`], [`rotation`, `parametric`],
  [`random`], [-], [`rect`, `distribution`
    (`normal`/`uniform`/`poisson`/`thomas`), `seed`],
  [`poisson`], [-], [`rect`, `minDistance`, `maxAttempts`, `seed`],
  [`thomas`], [-], [`rect`, `numClusters`, `clusterDeviation`, `seed`],
  [`destiny`], [`destinyGroup`], [-],
)

`LayoutType` names: `center` `right` `left` `upper` `lower` `uright` `uleft`
`dright` `dleft` `rupper` `lupper` `rlower` `llower` `diag1`-`diag4`.

The random layouts scatter the objects inside `rect` (a `Rect`,
`[xmin,ymin,xmax,ymax]`, a number for a centered square, or a preset name such as
`screen`; the legacy `deviation` builds the rect from a
half-width around `center`/`refPoint`). Without it, the camera math view is used. `poisson` keeps a minimum
distance between objects, so nothing overlaps; `thomas` groups them in clusters,
giving an organic look. Each object gets metadata describing how it was placed
(`layout.clusterIndex`, `layout.minDistance`, ...), readable with `getMetadata`.

```groovy
def lay = layout(type: "circular", center: [0, 0], radius: 3)
def box = layout(type: "box", rowSize: 3, gaps: [0.1, 0.1], corner: [0, 0])
layout(type: "box", size: 3, apply: g,
       homogeneize: [anchor: "lower", gaps: 0.1])
def pd  = layout(type: "poisson", rect: [-2, -2, 2, 2], minDistance: 0.4)
def th  = layout(type: "thomas", rect: "screen", numClusters: 4,
                 clusterDeviation: 0.08)
```

= Affine Transforms

== `affineTransform(...)`

Builds an `AffineJTransform`, selected by `type`. The `origin` and `destiny`
params accept a single coordinate (`[x,y]`, `[x,y,z]`, a `Vec`, a `Point`,
`"random"`) or a list of them; how many are needed depends on the type. `alpha`
interpolates between the identity (`0`) and the full transform (`1`).

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`type`], [String], [-], [Kind of transform (required)],
  [`origin`], [coordinate / List], [-], [Origin data],
  [`destiny`], [coordinate / List], [-], [Destiny data],
  [`alpha`], [double], [`1`], [Interpolation between identity and full
    transform],
  [`angle`], [double], [-], [Rotation angle in radians, `rotation` type (2D)],
  [`angles`], [List], [-], [Rotation angles `[ax, ay, az]`, `rotation` type (3D)],
  [`scale`], [double / List], [-], [Factor, `[sx, sy]` or `[sx, sy, sz]`,
    `scale` type],
  [`axis`], [object / List], [-], [Axis of a `reflectionByAxis`: a `Line`, a
    `Ray`, a `CTLine`-like object, a 2-point `Shape` (segment) or 2 coordinates],
  [`apply`], [object / List], [-], [Objects the transform is applied to
    immediately],
)

#tbl((28%, 40%, 32%),
  [`type`], [`origin`], [`destiny`],
  [`identity`], [-], [-],
  [`translation`], [1 point (or the vector alone)], [1 point],
  [`rotation`], [center (default `[0,0]`)], [-],
  [`scale`], [center (default `[0,0]`)], [-],
  [`reflection`], [1 point], [1 point],
  [`reflectionByAxis`], [2 axis points (or 1 + `to`), or the `axis` param], [-],
  [`similarity`], [2 points (3 in 3D)], [2 points (3 in 3D)],
  [`inverseSimilarity`], [2 points], [2 points],
  [`affine`], [3 points, or a `Rect`/object], [3 points, or a `Rect`/object],
  [`rotateScaleXY`], [3 points], [3 points],
)

`similarity` is the direct one (rotation + translation + uniform scale);
`inverseSimilarity` adds a reflection. For `affine`, a `Rect` or any object is
expanded into the corners of its bounding box, so one region is mapped onto
another.

With `apply`, the transform is applied immediately to the given object or list of
objects, and it is returned anyway so it can be reused later. The result can also
be fed to any object builder through its `transform:` param, either as
`transform: [affine: tr]` or directly as `transform: tr`.

```groovy
def tr = affineTransform(type: "reflection", from: A, to: B)
def tr = affineTransform(type: "similarity", from: [A, B], to: [C, D],
                         alpha: 0.5)
def tr = affineTransform(type: "affine", from: [[0,0],[1,0],[0,1]],
                         to: [D, E, F])
def tr = affineTransform(type: "reflectionByAxis", axis: line(a: A, b: B))
affineTransform(type: "rotation", from: [0, 0], angle: PI/4, apply: [a, b])
```

= Constructible Objects

#tip[
  Every parameter standing for a *straight line* (`axis`, `mirrorAxis`,
  `directrix`, `line`, `line1`, `line2`, `segment`) takes the same forms: a
  `CTLine`, a `CTSegment` or any other `CTAbstractLine`, a plain `Line` or
  `Ray`, a 2-point `Shape`, or a pair of points such as `[A, B]` or
  `[[0,0], [1,1]]`. A constructible line is kept as the input, so what it
  follows goes on being followed. A bare direction (a `Vec`) is refused: it
  says which way the line runs, not where it is. A `dir:` parameter accepts all
  of those *and* a bare direction.
]

== Values

A number that takes part in a construction is a `scalar(...)`, the same object a
script animates. Built from a formula or from a measurement it is recomputed
whenever any of the objects it reads changes, so whatever is built on it
follows:

```groovy
def t = scalar(0)
def r = scalar(expression: "1 + cos(t)", vars: [t: t])   //follows t
def C = ctCircle(center: [0, 0], radius: r, addToScene: true)
def d = scalar(distance: [A, B])
```

Every parameter standing for a number (`radius`, `angle`, `ratio`) takes a plain
number or a scalar, and given a scalar the object goes on following it. The
full list of forms is in #link(<scalar>)[`scalar(...)`].

This is also what a `locus` needs: with the whole construction hanging from
formulas, everything between the swept parameter and the traced point is
recomputed on every sample.

== Points

#tbl((36%, 30%, 34%),
  [Method], [Required], [Optional],
  [`ctPoint(at:[x,y])`], [`at`], [label, style, layer, visible, transform, `name`],
  [`ctPoint(type:"center", of:)`], [`of` (circle/arc/sector/ellipse/regular polygon)], [-],
  [`ctPoint(type:"centroid", of:)`], [`of` (polygon) or `points`], [-],
  [`ctPoint(type:"focus", of:)`], [`of` (CTEllipse)], [`index` (0/1)],
  [`ctPoint(type:"vertex", of:)`], [`of` (polygon/polyline/segment/ellipse)], [`index` (from 0)],
  [`ctPoint(type:"closest", on:, point:)`], [`on` (PointOwner or line), `point`], [-],
  [`ctPoint(type:"incenter"|"circumcenter"|"orthocenter", of:)`], [`of`
    (ctTriangle)], [-],
  [`ctMidPoint(A:, B:)`], [`a`, `b`], [-],
  [`ctMidPoint(segment:)`], [`segment` (any line form)], [-],
  [`ctIntersectionPoint(A:, B:)`], [`a`, `b` (Constructible or line)], [`index` (0/1)],
  [`ctMirrorPoint(point:, axis:)`], [`point`, `axis` (any line form)], [-],
  [`ctMirrorPoint(point:, A:, B:)`], [`point`, `a`, `b`], [-],
  [`ctMirrorPoint(point:, center:)`], [`point`, `center`], [-],
  [`ctRotatedPoint(point:, center:, angle:)`], [`point`, `center`, `angle`], [-],
  [`ctTranslatedPoint(point:, vector:)`], [`point`, `vector` (CTVector)], [-],
  [`ctPointOnObject(on:)`], [`on` (PointOwner or line)], [-],
  [`ctPointOnShape(shape:, t:)`], [`shape` (anything with a path; aliases
    `path`, `obj`, `on`)], [`t` (0 to 1, a number or a `scalar(...)`, default 0,
    alias `parameter`), `parametrized`],
)

#tip[
  All CT methods accept: `label`, `style`, `layer`, `visible`, `transform`,
  `name` (the Geogebra-style label the log shows the object by, not drawn on
  screen). `label:` takes the same forms as anywhere else (a String, a
  spec map, or a list of them) and sits on the object the constructible draws:
  on the dot for a point, along the path for a line, a circle or a conic.
  `ctLatex` is the only one without it.
]

```groovy
def A = ctPoint(at: [0, 0], label: r"$A$")
def M = ctMidPoint(A: A, B: B)
def I = ctIntersectionPoint(A: line1, B: circle1, index: 0)
def R = ctRotatedPoint(point: A, center: O, angle: Math.PI/3)
def O = ctPoint(type: "center", of: circle1)
def G = ctPoint(type: "centroid", of: poly)
def V = ctPoint(type: "vertex", of: poly, index: 2)
def P = ctPoint(type: "closest", on: segment1, point: A)
def I = ctPoint(type: "incenter", of: tri)
def t = scalar(0)
def Q = ctPointOnShape(shape: curve, t: t)   //sweep t to walk Q along the path
```

`ctPointOnShape` measures the path by arc length, so `t` is the fraction of the
length covered and the point moves at constant speed. `parametrized: false`
spreads `t` evenly over the Bézier segments instead, however long each one is,
which is the way to land exactly on the vertices of a polygon. Same name and
same default as `animMoveAlongPath`.

Its parameter being a `scalar(...)` is what makes it the natural input of a
`locus`: the point recomputes itself from the path and the parameter, so a
locus can sweep it, and nothing has to be written as an updater.

The point also reports what the curve is doing under it: `getTangent()`,
`getNormal()`, `getCurvature()` (signed), `getCurvatureRadius()`,
`getCurvatureCenter()` (what an evolute traces), and the two derivatives they
are built from. The curvature and its friends belong to the curve, so both
parametrizations agree on them; the raw derivatives are taken in the Bézier
parameter and their length is not geometric. On a path of Bézier pieces the
centre of curvature runs off to infinity at an inflection and jumps where two
pieces meet, so a locus tracing it wants `maxJump`.

== Lines

#tbl((34%, 66%),
  [Method], [Forms],
  [`ctLine`], [`A:, B:` or `A:, dir:` (`Vec`, `[x,y]`, or a line)],
  [`ctSegment`], [`A:, B:`],
  [`ctRay`], [`A:, B:` or `A:, dir:`],
  [`ctVector`], [`A:, B:`],
  [`ctAngleBisector`], [`A:, B:` (vertex), `C:` or `line1:, line2:` (+`index:` 0/1)],
  [`ctPerpBisector`], [`A:, B:` or `segment:` (any line form)],
  [`ctLineOrthogonal`], [`A:, B:` or `A:, dir:`],
  [`ctTransformedLine(line:, ...)`], [`mirrorAxis:` / `mirrorCenter:` /
    `vector:` / `center:+angle:`],
)

```groovy
def l  = ctLine(A: A, B: B)
def s  = ctSegment(A: A, B: B, style: [color: "blue"])
def s2 = ctSegment(A: A, B: B, label: [text: r"$c$", t: 0.5])
def pb = ctPerpBisector(segment: s)
def pb = ctPerpBisector(segment: [A, B])   // any line form, here 2 points
def tl = ctTransformedLine(line: l, mirrorAxis: l2)
def tl = ctTransformedLine(line: l, center: O, angle: Math.PI/4)
def bi = ctAngleBisector(line1: l, line2: l2, index: 0)
```

#lead[Reading it back.]

```groovy
l.getP1(); l.getP2(); l.getDirection()   // the points it holds, and its direction
s.getValue()                             // length of a ctSegment
l.mathObject                             // the Line / Shape it draws
```

== Circles and conics

#tbl((34%, 66%),
  [Method], [Forms],
  [`ctCircle`], [`center:+radius:` / `center:+through:` / `A:+B:+C:` /
    `inscribedIn:` (ctTriangle) / `circumscribedTo:` (ctTriangle)],
  [`ctCircleArc`], [`center:+A:+B:` / `A:+B:+C:`],
  [`ctCircleSector`], [`center:+A:+B:` / `A:+B:+C:`],
  [`ctSemiCircle`], [`A:, B:`],
  [`ctEllipse`], [`focus1:, focus2:, A:`],
  [`ctParabola`], [`focus:` + (`directrix:` / `A:+dir:`)],
  [`ctHyperbola`], [`focus1:, focus2:, A:`],
  [`ctTransformedCircle(circle:, ...)`], [`mirrorAxis:` / `mirrorCenter:` /
    `vector:` / `center:+angle:`],
)

The parabola and the hyperbola are unbounded, so what they draw is decided by the
camera: only the part that fits in the view is generated, and it is recomputed
whenever the camera moves. `trange: [t0, t1]` pins it instead, which is what an
animation needs while the camera is moving.

All four conics accept `ctIntersectionPoint` against a line and
`ctTangent(point:, conic:)`, which gives the tangent at the point when it lies on
the curve and the two that touch it when it lies outside.

`index` counts the intersections that really exist: a ray drops what falls behind
its origin, a segment what falls past its ends, and an arc, a sector or a
semicircle are only met where they are drawn. So `index: 0` is always the first
point one can see, and asking for one that is not there gives NaN.

```groovy
def c  = ctCircle(center: O, radius: 1.5, label: [text: r"$\Gamma$", t: 0.25])
def c  = ctCircle(A: P, B: [1,1], C: Q)   // circle through P, (1,1) and Q
def a  = ctCircleArc(center: O, A: P, B: Q)  // from P, counterclockwise up to the angle of Q
def a  = ctCircleArc(A: P, B: [1,1], C: Q)   // from P to Q, passing through (1,1)
def s  = ctCircleSector(A: P, B: [1,1], C: Q) // same, closed onto the center
def e  = ctEllipse(focus1: F1, focus2: F2, A: P)
// ellipse with focus F1, F2 that passes through P
def p  = ctParabola(focus: F, directrix: d)     // any CTAbstractLine, Line, Ray,
def p  = ctParabola(focus: F, directrix: [A,B]) // 2-point Shape or pair of points
def h  = ctHyperbola(focus1: F1, focus2: F2, A: P)  // both branches
def h  = ctHyperbola(focus1: F1, focus2: F2, A: P, trange: [-2, 2])
def tc = ctTransformedCircle(circle: c, vector: v)
```

#lead[Reading it back.]

```groovy
c.getCircleCenter(); c.getCircleRadius()  // the radius is a Scalar: build on it
c.getConicCoefficients()                  // the coefficients of its equation
e.getFocus1(); e.getFocus2(); e.getEllipseCenter()
e.getSemiMajorAxis(); e.getSemiMinorAxis(); e.getTiltAngle()
c.mathObject                              // the Shape it draws
```

== `ctLatex(...)`

#tbl(W4,
  [Param], [Type], [Default], [Description],
  [`text`], [String], [*required*], [LaTeX expression],
  [`anchor`], [`[x,y]` / Coords], [*required*], [Anchor point],
  [`anchorType`], [String], [`"DIAG4"`], [Anchor side (see AnchorType)],
  [`gap`], [double], [`0.3`], [Gap from anchor],
  [`scale`], [double], [`0.5`], [Size scale],
  [`format`], [String / List], [`0.00`], [Number format of the `{#0}`..`{#9}`
    arguments, as in `latex(...)`],
  [`latexStyle`], [LatexStyle / String], [-], [Per-glyph coloring style, or a
    registered name],
  [`colorToIndices` / `colorToTags`], [String / List], [-], [Per-glyph and
    per-region coloring, as in `latex(...)`],
  [`style`], [Map], [-], [Style properties],
)

```groovy
def lbl = ctLatex(text: r"\alpha", anchor: [1, 0],
                  anchorType: "left", gap: 0.15, scale: 0.7)
```

== Other constructibles

#tbl((28%, 72%),
  [Method], [Forms],
  [`ctPolygon`], [`points:[A,B,C,...]` (free) or `A:+B:+sides:` (regular)],
  [`ctTriangle`], [By points: `A:+B:+C:` (or `points:[A,B,C]`) or
    `A:+B:+angleA:+angleB:`. By lengths: `sideAB:+sideBC:+sideCA:`,
    `sideAB:+angleA:+angleB:`, `sideAB:+sideBC:+angleB:` (the angle they form) or
    `sideAB:+sideBC:+angleA:` + `index:` (ambiguous case, 0 or 1). The length
    forms take an optional placement `A:`, `angle:`; with none they own where
    they sit and can be shifted and rotated, never resized],
  [`ctAngleMark`], [`center:, A:, B:` + optional `radius:`, `isRight:`],
  [`ctTangent`], [`point:+circle:` + optional `index:` or `circle1:+circle2:` +
    optional `index:`],
)

```groovy
def poly = ctPolygon(points: [A, B, C, D])
def hex  = ctPolygon(A: A, B: B, sides: 6)
def tri  = ctTriangle(A: [0, 0], B: [1, 0], C: [1, 1])
def eq   = ctTriangle(A: [0, 0], B: [1, 0], angleA: PI/3, angleB: PI/3)
def rig  = ctTriangle(sideAB: 3, sideBC: 4, sideCA: 5)  //movable, rigid
def eq2  = ctTriangle(sideAB: 1, angleA: PI/3, angleB: PI/3)
def sas  = ctTriangle(sideAB: 3, angleB: PI/2, sideBC: 4)
def ssa  = ctTriangle(sideAB: 2, sideBC: 1.2, angleA: PI/6, index: 1)
```


A polygon and a polyline give back their vertices, which are the objects
themselves and not copies, so whatever is built on one follows it:

```groovy
poly[2]; poly.getVertex(2)   // vertex 2; a negative index counts back
poly.getPoints()             // every vertex
poly(0.25)                   // the point at 25% of the perimeter
```

A `ctTriangle` builds its notable elements itself, once each and always the same
object back, so they can be read straight from it. Index 0, 1, 2 is the vertex
A, B, C for the ones that hang from a vertex, and the side $A B$, $B C$, $C A$
for the ones that hang from a side:

```groovy
tri.getSide(0); tri.getMedian(0); tri.getAltitude(0)
tri.getPerpBisector(0); tri.getAngleBisector(0)
tri.getCentroid(); tri.getIncenter(); tri.getCircumcenter(); tri.getOrthocenter()
tri.getIncircle(); tri.getCircumcircle()
tri.angleA; tri.sideAB          //scalars: the input when the form takes it,
                                //a derived read-only one otherwise
animScalar(obj: tri.angleB, to: 30*DEGREES, runtime: 3)
def I = ctPoint(type: "incenter", of: tri)      //the same, from the DSL
def c = ctCircle(inscribedIn: tri)
def C = ctCircle(circumscribedTo: tri)
rig.shift(1, 2).rotate(PI/6)
def am   = ctAngleMark(center: O, A: P1, B: P2, radius: 0.15, isRight: true)
def t    = ctTangent(point: P, circle: c, index: 0)
def t    = ctTangent(circle1: c1, circle2: c2, index: 2)
```

= Animations

Every `anim*` command *runs immediately* unless `run: false` is given, and always
*returns* the created `Animation` (useful for composing with `animGroup` /
`animJoin`).

#lead[Common params (every anim command):]

#tbl(W3,
  [Key], [Type], [Description],
  [`obj`], [MathObject / List], [Object(s) to animate],
  [`runtime`], [double], [Duration in seconds (default `1`)],
  [`run`], [boolean], [Play now (default `true`); `false` just builds it],
  [`effects`], [Map], [Extra effects (see below); ignored by animations that do
    not support them],
  [`delay`], [double], [Stagger the parts an animation is made of, in `(0, 1)`:
    the objects of a list, the elements of a group or compound, the glyphs of a
    formula],
  [`lambda`], [String / Closure], [Timing function (see below)],
  [`advanced`], [Map], [Low-level `Animation` setters (`useObjectState`,
    `reversed`, `repeat`...; see below)],
  [`noLogs`], [boolean], [Suppress the "Begin animation" log line],
)

An *empty* `obj` list (or an empty `anims` list in `animGroup` / `animJoin`) is
not an error: a warning is logged and the command returns an animation that does
nothing and takes no time, so a script that builds its objects on the fly needs
no guard around the call. A missing `obj` is still an error.

== `effects:` map

#tbl(W3,
  [Key], [Type], [Description],
  [`turns`], [int], [Number of extra rotations],
  [`scale`], [double], [Scale bump amplitude],
  [`alpha`], [double], [Alpha (transparency) bump],
  [`jump`], [double], [Jump height],
  [`jumptype`], [String], [`PARABOLICAL`(def) `SEMICIRCLE` `ELLIPTICAL`
    `TRIANGULAR` `FOLIUM` `SINUSOIDAL` `SINUSOIDAL2` `CRANE` `BOUNCE1`
    `BOUNCE2`],
)

```groovy
animShift(obj: sq, dx: 1, effects: [jump: 1, jumptype: "crane", turns: 1])
```

== `lambda:` timing

A String names a parameterless `UsefulLambdas` function (prefix-matching):
`smooth`(def) `backAndForth` `backAndForthLinear` `backAndForthBounce1`
`backAndForthBounce2` `bounce1` `bounce2` `reverse`. A Closure gives a custom
timing, e.g. `lambda: { t -> t * t }`. For parameterized lambdas pass the
operator directly, e.g. `lambda: restrictTo(0.25, 0.5)`.

`lambda` is only the *easing* layer. The time an animation uses is the
composition of five layers, applied in this order:

#tbl((17%, 33%, 50%),
  [Layer], [Set with], [Answers],
  [allocation], [`advanced: [allocationParameter: [a, b]]`], [which slice of the
    container's time this animation gets (owned by `delay` / `animJoin`)],
  [direction], [`advanced: [reversed: true]`], [forwards or backwards],
  [repeat], [`advanced: [repeat: n]`], [how many passes],
  [behaviour], [`advanced: [behaviour: ...]`], [*where* it is at each instant of
    its progress],
  [easing], [`lambda:`], [*how smoothly* that progress is travelled],
)

Behaviour and easing are separate because they answer different questions. Write
the behaviour *linearly* and let `lambda` smooth it:

```groovy
// linear round trip, with corners at both ends: nothing left to smooth it
animShift(obj: sq, dx: 2, lambda: "backAndForthLinear")

// same round trip, but eased: brakes into the far point and into the return
animShift(obj: sq, dx: 2, advanced: [behaviour: "backAndForthLinear"])
```

A *reversed* animation runs from its end to its beginning and finishes there, so
it undoes whatever it creates: `appear(obj: sq, advanced: [reversed: true])`
behaves as a disappear, with no separate command. The same holds for a
non-monotonic easing: `lambda: "backAndForth"` returns to the start and therefore
ends as if the animation had never run.

Animation *effects* (jump, turns, scale, alpha) can carry their own easing with
`advanced: [effectLambda: ...]`, sharing the other three layers. That way the
decoration can go and come back while the movement goes straight to its
destination:

```groovy
animShift(obj: sq, dx: 3, effects: [scale: 0.5],
          advanced: [effectLambda: "backAndForth"])
```

== `advanced:` map

#tbl(W3w,
  [Key], [Type], [Description],
  [`useObjectState`], [boolean], [Save and restore the object state on every
    frame (default `true`)],
  [`addObjectsToScene`], [boolean], [Let the animation add and remove its objects
    (default `true`)],
  [`shouldInterpolateStyles`], [boolean], [Interpolate colors, thickness and
    alpha (default `true`)],
  [`shouldResetAtFinish`], [boolean], [Reset the animation once its playback is
    over, so the same animation can be played again (default `false`)],
  [`allocationParameter`], [`[a, b]`], [Sub-range of the total time the animation
    occupies],
  [`reversed`], [boolean], [Play the animation backwards, undoing what it
    creates],
  [`repeat`], [int], [Times the animation is played within its runtime],
  [`behaviour`], [String / Closure], [Shape of the progress, written linearly;
    `lambda` smooths its result],
  [`effectLambda`], [String / Closure], [Separate easing for the effects],
)

== `animShift(...)` / `animRotate(...)` / `animScale(...)`

#tbl(W3b,
  [Command], [Required], [Optional],
  [`animShift`], [`obj` + (`vector` or `dx`/`dy`/`dz`)], [-],
  [`animRotate`], [`obj` + `angle` (radians)], [`center`],
  [`animScale`], [`obj` + (`scale` or `sx`/`sy`/`sz`)], [`center`],
)

`center` accepts a point/Vec or `[x,y]`; if omitted the object uses its own
center. `scale` is a single number (uniform) or `[sx,sy]`.

```groovy
animShift(obj: sq, dx: 1, dy: 0.5)
animRotate(obj: sq, angle: 90 * DEGREES, center: [0, 0], runtime: 2)
animScale(obj: sq, scale: 0.5, effects: [turns: 1])
```

== `animCamera(...)`

#tbl((24%, 24%, 52%),
  [`type`], [Required], [Description],
  [`shift`], [`vector` or `dx`/`dy`/`dz`], [Pan the view],
  [`scale`], [`scale` or `zoom`], [`scale > 1` zooms out;
    `zoom: 5 == scale: 0.2`],
  [`rotate`], [`angle`], [Turn the camera by an angle, in radians,
    counterclockwise, so the scene appears rotated clockwise],
  [`zoomToRect`], [`rect`], [Rect, `[xmin,ymin,xmax,ymax]`, a
    number `n` (square `[-n,n]`) or a preset],
  [`zoomToObjects`], [`obj`], [Fit the given object(s)],
  [`zoomToAllObjects`], [-], [Fit all objects in the scene],
)

Optional `camera:` selects the Camera (default: scene camera).

```groovy
animCamera(type: "shift", dx: 1, dy: -1, runtime: 4)
animCamera(type: "scale", zoom: 5)
animCamera(type: "zoomToRect", rect: [-1, -1, 1, 1])
animCamera(type: "rotate", angle: PI/6, runtime: 2)
```

== `animAffine(...)`

Points accept a point/Vec or `[x,y]`.

#tbl((28%, 72%),
  [`type`], [Params],
  [`similarity`], [`from`/`to`: 2 points each, or two `Rect`],
  [`inverseSimilarity`], [`from`/`to`: 2 points each],
  [`reflection`], [`from`, `to` (single points)],
  [`reflectionByAxis`], [`axis`: a `Line`, `Ray`, `CTLine`-like object, 2-point
    `Shape` or 2 points],
  [`affine`], [`from`/`to`: 3 points each],
)

A ready-made transform can be animated as is with `transform:`, with no `type`
nor points:
`animAffine(obj: s, transform: affineTransform(type: "rotation", angle: PI/2))`.

```groovy
animAffine(type: "affine", obj: grid, from: [A, B, C], to: [D, E, F])
animAffine(type: "reflection", obj: sq, from: [1, 0], to: [-1, 0])
animAffine(type: "similarity", obj: tri, from: [A, B], to: [C, D])
```

== `animMoveAlongPath(...)`

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`obj`], [MathObject], [*required*], [Single object to move],
  [`path`], [Pathable], [*required*], [Path to follow (Shape, JMPath, function
    graph...)],
  [`anchor`], [String], [`center`], [Point of the object on the path],
  [`rotation`], [String], [`fixed`], [`fixed` `rotate` (follow tangent) `smart`
    (never upside down)],
  [`parametrized`], [boolean], [`true`], [`true` arc-length (constant speed);
    `false` Bézier],
  [`rotationSmoothing`], [double], [`0.01`], [Corner smoothing half-window; `0` =
    abrupt turns],
)

```groovy
animMoveAlongPath(obj: dot, path: circle)
animMoveAlongPath(obj: arrow, path: curve, rotation: "rotate", runtime: 3)
```

== `animStackTo(...)` / `animAlign(...)` / `animDistribute(...)` / `animSetLayout(...)`

#tbl((25%, 47%, 28%),
  [Command], [Required], [Optional],
  [`animStackTo`], [`obj`, `dst`], [`anchor` (def `center`), `gap`],
  [`animAlign`], [`obj`, `dst`, `type` (`left`/`right`/`upper`/`lower`/`hcenter`/
    `vcenter`/`baseline`/`baseline_to_lower`)], [-],
  [`animDistribute`], [`obj` (group/List), `orientation` (`horizontal`/`vertical`/
    `both`; alias `axis`)], [`respectOrder` (def `true`)],
  [`animSetLayout`], [`obj` (group/List), `layout`], [`refPoint`, `gap`],
)

`animDistribute` spreads the objects evenly between the two extreme ones, which
stay where they are: the animated `distribute(...)` of a group. It needs several
objects, so a single group means its elements.

`to` accepts anything with a bounding box (a MathObject, a Rect, a point/Vec,
`camera.getMathView()`), a point `[x,y]` or a region `[xmin,ymin,xmax,ymax]`, in
both `animStackTo` and `animAlign` (`dst` and `destiny` are accepted as aliases).
The destination is never moved; aligning against a point is aligning against a
degenerate box. `layout` is a `LayoutType` name (`right`, `upper`,
`diag1`...), a `GroupLayout` (from the `layout(...)` DSL), or a spec map
`[type: "circular", center: [0,0], radius: 2]`.

```groovy
animStackTo(obj: sq, to: circle, anchor: "right", gap: 0.1)
animAlign(obj: sq, to: circle, type: "left")
animAlign(obj: sq, to: [0, 0], type: "left")   // against a point
animDistribute(obj: g, orientation: "horizontal")
animSetLayout(obj: g, layout: "right", gap: 0.1)
```

== `animStyle(...)`

Animates a style change, interpolating only the properties the destination
mentions.

#tbl(W3,
  [Key], [Type], [Description],
  [`obj`], [MathObject / List], [*required*, target(s)],
  [`style`], [Map / String], [Destination style; a String animates every property
    of that named style],
  [`from`], [MathObject], [Destination is the current style of that object
    (alternative to `style`)],
  [`parts`], [Map], [Destination style of each piece by name (see
    #link(<parts-map>)[`parts:` map]); alone or together with `style`/`from`],
)

```groovy
animStyle(obj: c, style: [drawColor: "blue"], runtime: 2)
animStyle(obj: c, from: t)
animStyle(obj: sl, parts: [knob: [color: "green"], labels: [drawAlpha: 0]])
```

== `play { ... }`

Plays together every animation written inside the block, with no need to build
them with `run: false`:

```groovy
play {
    appear(runtime: 1, obj: v, type: "movein", from: "right")
    appear(runtime: 1, obj: [u1, u2], type: "movein", from: "left")
}
```

The rule is one line: *everything built inside the block plays together when the
block closes*, and nothing plays on its own before that. A `run: false` written
inside is redundant and the animation still takes part. Blocks nest, so a whole
block is a single animation of the block containing it.

`sequence { ... }` is the same thing but playing the children *one after
another*, and the two nest in any combination:

```groovy
sequence {
    play { appear(obj: a); appear(obj: b) }
    animShift(obj: a, dx: 2)
    play { disappear(obj: a); disappear(obj: b) }
}
```

The innermost block wins, so `skip { play { ... } }` builds the group and then
skips it whole.

A block that builds nothing inside plays the animations it *returns*, made with
`run: false`, which is how a list built in a loop is played:

```groovy
def anims = steps.collect { animShift(obj: p, vector: it, runtime: 0.1,
                                      run: false) }
sequence { anims }
```

`play` and `sequence` take no parameters, since their timing comes from the
children. When the container needs its own (`delay`, `gap`, `runtime`,
`lambda`), `animGroup` and `animJoin` accept the very same block:

```groovy
animGroup(delay: 0.5) { appear(obj: a); appear(obj: b) }
animJoin(gap: 0.5) { animShift(obj: a, dx: 2); animRotate(obj: a, angle: PI) }
```

== `skip { ... }` and `section("name") { ... }`

#tbl((26%, 74%),
  [Block], [What it does],
  [`skip { ... }`], [Runs the block *without rendering any frame*: every
    animation inside is taken straight to its final state, so the scene ends up
    where it would have been at no cost],
  [`section("name") { ... }`], [Runs the block writing to the log its name, the
    frames it added and how long it took. Changes nothing in the scene],
)

`skip` is for writing a scene: it jumps over the part already checked and reaches
the one being worked on, without commenting code out and losing everything it
sets up. It is not a faithful preview: an animation whose effect depends on going
through the intermediate frames (an updater that accumulates, a trail being
drawn) does not end the same way.

```groovy
skip {
    section("intro") { play { appear(obj: title); appear(obj: axes) } }
}
section("main") {
    play { morph(from: a, to: b); animShift(obj: c, dx: 1) }
}
```

== `harryhausen { ... }`

Frame by frame animation, the manual way: the block runs *once per frame* and the
frame is rendered right after it, so what the block changes is what the frame
shows. Named after Ray Harryhausen, who animated moving the model between two
exposures.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`runtime`], [double], [`1`], [Duration of the block in seconds (alias
    `seconds`)],
  [`frames`], [int], [-], [Number of frames to run instead of a duration; wins
    over `runtime`],
)

Inside the block these names are given, with no need to declare anything (they
exist inside the block only):

#tbl((24%, 76%),
  [Name], [Meaning],
  [`dt`], [Time step between two frames, `1 / fps`],
  [`t`], [Seconds elapsed since the block started; `0` on the first frame],
  [`frame` / `frames`], [Index of the current frame, from 0, and how many will be
    run],
  [`progress`], [From `0` on the first frame to `1` on the last],
  [`stop()`], [Ends the block after the current frame],
)

```groovy
def A = point()
scene.add(A)

harryhausen(runtime: 10) {
    A.shift((1 - 2 * Math.random()) * dt, (1 - 2 * Math.random()) * dt)
}

// A shorthand for the runtime, and stopping early
harryhausen(5) {
    ball.shift(0, -9.8 * dt * dt)
    if (ball.center.y < 0) stop()
}
```

A block that declares parameters gets `t, dt, frame` in that order, which is what
a helper method written apart needs:
`harryhausen(frames: 120) { t, dt -> step(t, dt) }`. Inside `skip { ... }` the
body runs the same number of times but no frame is rendered.

== `animate { ... }`

The same block with *the parameter already interpolated*: it runs once per frame
and hands the value going from `from` to `to` with the easing of `lambda`
applied. It turns a formula into an animation without writing the interpolation
by hand and without mounting an updater.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`from`], [double], [`0`], [Value the parameter starts at],
  [`to`], [double], [`1`], [Value the parameter ends at],
  [`runtime`], [double], [`1`], [Duration of the block in seconds (alias
    `seconds`)],
  [`frames`], [int], [-], [Number of frames to run instead of a duration; wins
    over `runtime`],
  [`lambda`], [lambda], [linear], [Timing function applied to the parameter],
  [`variable`], [String], [-], [Name the parameter takes inside the block
    (aliases `var`, `varName`)],
)

The parameter is the implicit `it`, or the name given with `variable:`.
Everything `harryhausen` gives is given here too (`dt`, `t`, `frame`/`frames`,
`progress`, `stop()`), plus `value`, the parameter under a fixed name.
`progress` is the linear one, from 0 to 1; the parameter is the one carrying the
easing.

```groovy
animate(from: 0, to: 2 * PI, runtime: 3, lambda: "smooth") {
    p.moveTo(cos(it), sin(it))
}

// With a name of its own, which is what a formula reads like
animate(from: -2, to: 2, variable: "x") {
    p.moveTo(x, x * x)
    label.setText("x = ${round(x, 2)}")
}
```

The name exists inside the block only, so a variable of the script called the
same is left untouched outside it. A block that declares parameters gets
`value, progress, dt` in that order: `animate(to: 10) { v, prog -> step(v, prog) }`.

== `timeline { ... }`

Writes an animation of several voices saying *the second each one starts at*, and
plays the whole thing as a single animation, so it nests in a `sequence`, is
skipped by a `skip` or is built with `run: false` and combined later.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`runtime`], [double], [last end], [Total duration in seconds (alias
    `seconds`). Only *lengthens* the timeline, to leave the scene still at the
    end; a shorter one is reported and ignored],
  [`run`], [boolean], [`true`], [Play the animation immediately after building
    it],
)

Inside the block these names are given, with no need to declare anything (they
exist inside the block only):

#tbl((30%, 70%),
  [Name], [Meaning],
  [`at(second) { ... }`], [Schedules what the inner block builds at that second.
    Several animations inside play *together*, as in `play`; write a `sequence`
    inside for one after another],
  [`at("mark") { ... }`], [The same, at a mark set earlier],
  [`after(gap) { ... }`], [Starts `gap` seconds after the *end* of the previous
    block; `after { ... }` chains right after it],
  [`mark(name)` / `mark(name, second)`], [Names the second the last block ends
    at, or an arbitrary one],
  [`now` / `last` / `total`], [Second the last block ends at, second it starts at
    (`at(last)` puts something in parallel with it), and second the timeline ends
    at so far],
  [`timeOf(name)`], [Reads a mark back, to compute a time from it:
    `at(timeOf("mid") + 0.3) { ... }`],
)

Each of the scheduling calls hands back the second its block ends at.

```groovy
timeline {
    at(0)      { appear(obj: title) }
    at(0.5)    { animShift(obj: a, dx: 2, runtime: 2) }
    after(0.3) { disappear(obj: title) }
    mark("mid")
    at("mid")  { play { highlight(obj: b); animRotate(obj: b, angle: PI) } }
    at(last)   { animCamera(scale: 1.2) }   // parallel with the previous block
}
```

The times are kept as they are written, so a timeline takes no `lambda`,
`effects` nor `delay`: an easing over the whole thing would bend those seconds.
Set them on the animations inside, which keep every parameter of their own. An
animation written straight into the block, outside any `at`/`after`, is scheduled
at second 0 and reported.

== `defaults(...) { ... }`

Gives everything built inside the block the parameters it does not name itself,
so what a whole passage shares is written once instead of on every line. It
covers both sides of the DSL: *how an animation runs* and *what an object is
like*.

#tbl((16%, 20%, 18%, 46%),
  [Key], [Type], [Applies to], [Description],
  [`runtime`], [double], [animations], [Duration of every animation of the block],
  [`lambda`], [see `lambda:`], [animations], [Timing function of every animation
    of the block],
  [`effects`], [map], [animations], [Effects, *merged key by key* with the ones
    the call gives],
  [`delay`], [double], [animations], [Stagger of every animation made of several
    parts],
  [`advanced`], [map], [animations], [Advanced options, *merged key by key* with
    the ones the call gives],
  [`noLogs`], [boolean], [animations], [Suppress the "Begin animation" log line],
  [`style`], [style spec], [objects], [Style of every object, applied *before*
    the one of the call],
  [`transform`], [map], [objects], [Transform of every object, *merged key by
    key* with the one of the call],
  [`layer`], [int], [objects], [Rendering layer of every object],
  [`visible`], [boolean], [objects], [Visibility flag of every object],
  [`fixedCamera`], [boolean], [objects], [Draw every object with the fixed
    camera],
  [`addToScene`], [boolean], [objects], [Add every object to the scene as it is
    created],
)

```groovy
defaults(runtime: 2, lambda: "smooth") {
    animShift(obj: a, dx: 2)                     // runtime 2, smooth
    animScale(obj: c, scale: 2, runtime: 0.5)    // runtime 0.5: the call wins

    defaults(effects: [jump: 0.5]) {
        animShift(obj: d, dx: 1)                 // runtime 2, smooth and a jump
    }
}

// Three red circles at half size, each one still in its own variable
def a, b, c
defaults(style: [color: "red"], transform: [scale: 0.5], addToScene: true) {
    a = shape(type: "circle")
    b = shape(type: "circle", transform: [shift: [2, 0]])  // scaled, shifted
    c = shape(type: "circle", style: [thickness: 4])       // red, and thicker
}
```

*The call always wins*, and so does the innermost block when they are nested.
Styles are not replaced but applied one after another, the call last; two
`transform` maps are merged into one, the keys of the block first, so the object
is transformed once. The block plays nothing, collects nothing and hands back
whatever the block returns, so it can be written inside or outside a
`play`/`sequence` block indistinctly.

`stack`, `label` and `parts` are deliberately left out: stacking would pile every
object of the block on the same spot, one label text repeated on all of them
makes no sense, and `parts` names the pieces of one particular object.
*`apply(...)` takes nothing either*, since it configures objects that already
exist and were not built by the block. *The containers take nothing*: `play`,
`sequence`, `timeline`, `animGroup` and `animJoin` get their timing from their
children, which already carry the defaults.

== `temporarily(...) { ... }`

Takes out of the scene everything that entered it inside the block, so an
annotation is written where it is used and not twice.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`runtime`], [double / boolean], [-], [Animate the exit: seconds of the
    `disappear`, or `true` for one second. Absent, the removal costs no frame.
    Alias: `fade`],
  [`type`], [String], [`fadeout`], [Kind of exit, as in `disappear`: `fadeout`,
    `shrinkout`, `moveout`. Naming it asks for an animation even without
    `runtime`],
)

```groovy
temporarily {
    def arrow = arrow(from: a, to: b)
    def note  = latex(r"$f'(x)$", stack: [to: arrow, at: "upper"])
    play { appear(obj: [arrow, note]) }
    animWait(1)
}   // arrow and note leave the scene here

temporarily(runtime: 0.5) {
    def guide  = shape(type: "segment", from: a, to: b, addToScene: true)
    def result = latex(r"$c = 5$")
    play { appear(obj: result) }
    keep(result)        // the guide goes, the result stays
}
```

#lead[What it undoes is what reached the scene], not what the block created: the
object list of the scene is photographed on the way in and compared on the way
out, so every route in is covered. A `scene.add(...)` written by hand, an
`addToScene: true` and the objects an animation brings in by itself (`appear`,
`morph`, `showCreation`) are all taken out the same way. Objects are compared *by
reference*, so two equal objects are told apart.

#lead[Only additions are undone]: something that was in the scene before the
block and left it inside is not put back. Blocks nest with no special case, and,
like `defaults`, this one plays nothing by itself and hands back whatever the
block returns.

== `animGroup(...)` / `animJoin(...)`

Two forms. *As a block*, the children go inside and nothing needs `run: false`:

```groovy
animGroup(delay: 0.5) { appear(obj: a); appear(obj: b) }
animJoin(gap: 0.5) { animShift(obj: a, dx: 2); animRotate(obj: a, angle: PI) }
```

It is `play { }` / `sequence { }` plus the parameters of the container, and it
nests with them exactly the same way. `anims` and the block are mutually
exclusive.

*As a list*, the children are built first with `run: false`, which is what a list
assembled in a loop needs. Beware: a child written inline without `run: false` is
played on its own while the argument list is evaluated, and the container then
replays it; the DSL warns when it detects this.

#tbl((19%, 14%, 33%, 34%),
  [Command], [Required], [Optional], [Timing],
  [`animGroup`], [`anims`], [`delay` (0-1 stagger), `effects`, `lambda`],
    [Plays *together*; total = max child runtime (no `runtime`)],
  [`animJoin`], [`anims`], [`runtime`, `gap`, `lambda`], [Plays *in sequence*;
    default total = sum of children, default lambda linear],
)

```groovy
def a1 = animShift(obj: s1, dx: 1, run: false)
def a2 = animRotate(obj: s2, angle: 90 * DEGREES, run: false)

animGroup(anims: [a1, a2], delay: 0.5)
animJoin(anims: [a1, a2], runtime: 3)
animJoin(anims: [a1, a2], gap: 0.5)   // half a second of pause in between
```

`gap` (alias `gaps`) is the pause *between* one child and the next, in seconds: n
children give n-1 pauses, and there is none at the beginning or at the end. It is
a `WaitAnimation` inserted in the list, so it counts as one more child: without
`runtime` the total grows by the pauses, and with `runtime` the pauses are scaled
along with everything else.

A list of animations also converts directly, which is the shortest form when no
other parameter is needed:

```groovy
[a1, a2] as AnimationGroup   // played together
[a1, a2] as JoinAnimation    // one after another (no gap here: use animJoin)
```

== `sound(...)`

Two modes, chosen by the presence of `at`. Files live in `resources/sounds` (`!`
prefix for an absolute path) and need an external ffmpeg, set with
`config(ffmpeg: ...)`.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`file`], [String], [*required*], [Sound file (aliases `sound`, `name`, `src`)],
  [`pitch`], [double], [`1`], [Playback pitch],
  [`at`], [double], [-], [Fraction `[0,1]` of the runtime at which the sound
    plays. *Its presence* builds a `PlaySoundAt` animation; without it the sound
    is added to the current frame and `null` is returned],
  [`strict`], [boolean], [`true`], [Plays when the time parameter is *strictly*
    greater than `at`. `false` fires on the first frame when the lambda stays
    flat at the start],
  [`runtime` / `run` / `lambda`], [-], [-], [Common anim params (animation mode)],
)

```groovy
sound(file: "pop.wav")                        // at the current frame
def a = animRotate(obj: sq, angle: 90 * DEGREES, runtime: 6, run: false)
animGroup(anims: [a, sound(file: "whoosh.mp3", runtime: 6, at: .4,
                           run: false)])
```

== `animWait(...)`

An animation that does nothing. Played alone it is a pause; built with
`run: false` it is a filler inside `animJoin` (a gap between steps) or
`animGroup` (padding a group to a longer total).

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`runtime`], [double], [`1`], [Waiting time in seconds (alias `seconds`)],
  [`run`], [boolean], [`true`], [Play now; `false` just builds it],
)

```groovy
animWait(runtime: 2)                                   // just wait
animJoin(anims: [a1, animWait(runtime: .5, run: false), a2])
```

It is not called `wait` because that is a final method of `java.lang.Object`.

== `appear(...)` / `disappear(...)`

Bring objects into / out of the scene. `obj` is a single object or a List.

#tbl((17%, 40%, 43%),
  [Command], [`type` (default)], [Type-specific params],
  [`appear`], [`showcreation`\* (`fadein`, `growin`, `movein`)], [`growin`:
    `angle`, `orientation`; `movein`: `enter` (edge; aliases `from`, `anchor`)],
  [`disappear`], [`fadeout`\* (`shrinkout`, `moveout`)], [`shrinkout`: `angle`,
    `orientation`; `moveout`: `exit` (edge; aliases `to`, `anchor`)],
)

\*`showcreation` aliases: `draw`, `sketch`. `orientation`: `horizontal`
`vertical` `both`. Edge values: `left` `right` `upper` `lower` `upper_left`
`upper_right` `lower_left` `lower_right` `center`.

```groovy
appear(obj: s, type: "growin", angle: PI, orientation: "horizontal")
disappear(obj: [s1, s2], type: "moveout", to: "right")
```

== `morph(...)`

Morphs one object into another (removes `from`, adds `to`).

#tbl(W3,
  [Key], [Type], [Description],
  [`from` / `to`], [MathObject], [*required* (initial / final object)],
  [`type`], [String], [`auto`(def) `flip` `twist`],
  [`orientation`], [String], [`flip`: `horizontal` `vertical` `both`],
  [`pivotal`], [int], [`twist`: pivotal segment index (both must be `Shape`)],
)

```groovy
morph(from: square, to: circle)
morph(from: a, to: b, type: "flip", orientation: "horizontal")
```

== `highlight(...)`

Momentarily draws attention to objects without permanent changes.

#tbl((26%, 74%),
  [`type`], [Params],
  [`highlight` (def)], [`scale` (def `1.5`)],
  [`twist`], [`scale`, `angle` (def 15°)],
  [`contour`], [`box` (bbox instead of outline), `gap`, `color`, `thickness`,
    `amplitude`, `style`],
  [`boxHighlight`], [-],
)

```groovy
highlight(obj: s, scale: 2)
highlight(obj: text, type: "contour", box: true, gap: 0.1, color: "yellow",
          thickness: 12)
```

== `animState(...)`

Animates the named values of a compound built with `state:` (or of a
`StatefulCompound` subclass), which animates its geometry.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`obj`], [compound / List], [*required*], [Object(s) with state],
  [`to`], [Map / double], [*required*], [Final value per name: `[aperture: 1]`. A
    plain number when the object declares exactly one],
  [`from`], [Map / double], [current], [Initial value per name],
  [`runtime` / `run` / `lambda` / `delay` / `effects`], [-], [-], [Common anim
    params],
)

```groovy
animState(obj: box, to: [aperture: 1], runtime: 1.5)
animState(obj: box, to: 1)                          // single value: shorthand
animState(obj: balance, to: [leftWeight: 3, rightWeight: 1])
```

The non animated counterpart is `apply(obj: box, state: [aperture: 1])`.

== `animScalar(...)`

Animates the scalar value of `Parametric` object(s) (e.g. the `t` of a
density/vector field) from `from` to `to`.

#tbl(W4,
  [Key], [Type], [Default], [Description],
  [`obj`], [Parametric / List], [*required*], [Scalar object(s)],
  [`from`], [double], [`0`], [Initial value],
  [`to`], [double], [*required*], [Final value],
  [`runtime` / `run` / `lambda`], [-], [-], [Common anim params],
)

```groovy
animScalar(obj: t, from: 0, to: 2*PI, runtime: 3)
```

== `mathTransform(...)`

Morphs a LaTeX formula into another, mapping/removing/adding individual glyphs,
optionally over sequential stages.

#tbl(W3w,
  [Key], [Type], [Description],
  [`from` / `to`], [String / LatexMathObject], [*required* (initial / final
    formula)],
  [`maps`], [List], [Glyph mappings: `[orig, dst]` pairs, or
    `[orig:.., dst:.., stage:.., style:.., asGroup:.., lambda:.., effects:..]`],
  [`removes` / `adds`], [List], [Ints/lists of indices, or maps
    `[indices:.., style:.., stage:.., effects:..]`],
  [`align`], [`[toIndex, fromIndex]`], [Align `to` glyph over a `from` glyph],
  [`defaultRemove` / `defaultAdd`], [String], [Default styles (`shrink_out` /
    `grow_in`)],
  [`defaultRemoveStage` / `defaultAddStage`], [int], [Default stages (`1`)],
  [`delay`], [double], [Stagger sub-animations within each stage `(0,1)`],
  [`runtime` / `run` / `lambda`], [-], [Common anim params],
)

Map `style`: `interpolation` `flip_horizontally` `flip_vertically` `flip_both`.
`effects` on maps: `[turns, scale, alpha, jump, jumptype]`; on removes/adds only
`turns`.

```groovy
mathTransform(
    from: r"$aX b$", to: r"$Z$", runtime: 3,
    removes: [[indices: 0, style: "moveout_left", stage: 1]],
    maps:    [[orig: 1, dst: 0, stage: 2]])
```

