@motionscript/plot 0.0.0-stage → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +201 -0
  3. package/dist/browser/chunks/chunk-37IWXGPH.js +2 -0
  4. package/dist/browser/chunks/chunk-37IWXGPH.js.map +7 -0
  5. package/dist/browser/index.js +84 -0
  6. package/dist/browser/index.js.map +7 -0
  7. package/dist/browser/kit.js +2 -0
  8. package/dist/browser/kit.js.map +7 -0
  9. package/dist/browser/manifest.json +12 -0
  10. package/dist/engine.d.ts +14 -0
  11. package/dist/engine.d.ts.map +1 -0
  12. package/dist/engine.js +14 -0
  13. package/dist/engine.js.map +1 -0
  14. package/dist/graph2d/curve-cache.d.ts +60 -0
  15. package/dist/graph2d/curve-cache.d.ts.map +1 -0
  16. package/dist/graph2d/curve-cache.js +79 -0
  17. package/dist/graph2d/curve-cache.js.map +1 -0
  18. package/dist/graph2d/curve-error.d.ts +12 -0
  19. package/dist/graph2d/curve-error.d.ts.map +1 -0
  20. package/dist/graph2d/curve-error.js +15 -0
  21. package/dist/graph2d/curve-error.js.map +1 -0
  22. package/dist/graph2d/curve.d.ts +104 -0
  23. package/dist/graph2d/curve.d.ts.map +1 -0
  24. package/dist/graph2d/curve.js +531 -0
  25. package/dist/graph2d/curve.js.map +1 -0
  26. package/dist/graph2d/graph2d.d.ts +164 -0
  27. package/dist/graph2d/graph2d.d.ts.map +1 -0
  28. package/dist/graph2d/graph2d.js +404 -0
  29. package/dist/graph2d/graph2d.js.map +1 -0
  30. package/dist/graph2d/index.d.ts +32 -0
  31. package/dist/graph2d/index.d.ts.map +1 -0
  32. package/dist/graph2d/index.js +32 -0
  33. package/dist/graph2d/index.js.map +1 -0
  34. package/dist/graph2d/plane-fill.d.ts +110 -0
  35. package/dist/graph2d/plane-fill.d.ts.map +1 -0
  36. package/dist/graph2d/plane-fill.js +248 -0
  37. package/dist/graph2d/plane-fill.js.map +1 -0
  38. package/dist/graph2d/plane.d.ts +179 -0
  39. package/dist/graph2d/plane.d.ts.map +1 -0
  40. package/dist/graph2d/plane.js +359 -0
  41. package/dist/graph2d/plane.js.map +1 -0
  42. package/dist/graph2d/shared.d.ts +40 -0
  43. package/dist/graph2d/shared.d.ts.map +1 -0
  44. package/dist/graph2d/shared.js +70 -0
  45. package/dist/graph2d/shared.js.map +1 -0
  46. package/dist/graph3d/expression.d.ts +30 -0
  47. package/dist/graph3d/expression.d.ts.map +1 -0
  48. package/dist/graph3d/expression.js +35 -0
  49. package/dist/graph3d/expression.js.map +1 -0
  50. package/dist/graph3d/graph3d.d.ts +186 -0
  51. package/dist/graph3d/graph3d.d.ts.map +1 -0
  52. package/dist/graph3d/graph3d.js +404 -0
  53. package/dist/graph3d/graph3d.js.map +1 -0
  54. package/dist/graph3d/index.d.ts +22 -0
  55. package/dist/graph3d/index.d.ts.map +1 -0
  56. package/dist/graph3d/index.js +22 -0
  57. package/dist/graph3d/index.js.map +1 -0
  58. package/dist/graph3d/shared.d.ts +61 -0
  59. package/dist/graph3d/shared.d.ts.map +1 -0
  60. package/dist/graph3d/shared.js +101 -0
  61. package/dist/graph3d/shared.js.map +1 -0
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +4 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/kit/equations.d.ts +56 -0
  67. package/dist/kit/equations.d.ts.map +1 -0
  68. package/dist/kit/equations.js +63 -0
  69. package/dist/kit/equations.js.map +1 -0
  70. package/dist/kit/expression.d.ts +60 -0
  71. package/dist/kit/expression.d.ts.map +1 -0
  72. package/dist/kit/expression.js +268 -0
  73. package/dist/kit/expression.js.map +1 -0
  74. package/dist/kit/index.d.ts +15 -0
  75. package/dist/kit/index.d.ts.map +1 -0
  76. package/dist/kit/index.js +13 -0
  77. package/dist/kit/index.js.map +1 -0
  78. package/dist/nodes.d.ts +19 -0
  79. package/dist/nodes.d.ts.map +1 -0
  80. package/dist/nodes.js +19 -0
  81. package/dist/nodes.js.map +1 -0
  82. package/package.json +68 -3
  83. package/registry.json +34 -0
  84. package/src/engine.ts +13 -0
  85. package/src/graph2d/curve-cache.ts +103 -0
  86. package/src/graph2d/curve-error.ts +19 -0
  87. package/src/graph2d/curve.ts +657 -0
  88. package/src/graph2d/graph2d.ts +586 -0
  89. package/src/graph2d/index.ts +31 -0
  90. package/src/graph2d/plane-fill.ts +308 -0
  91. package/src/graph2d/plane.ts +457 -0
  92. package/src/graph2d/shared.ts +103 -0
  93. package/src/graph3d/expression.ts +51 -0
  94. package/src/graph3d/graph3d.ts +615 -0
  95. package/src/graph3d/index.ts +21 -0
  96. package/src/graph3d/shared.ts +143 -0
  97. package/src/index.ts +3 -0
  98. package/src/kit/equations.ts +102 -0
  99. package/src/kit/expression.ts +323 -0
  100. package/src/kit/index.ts +29 -0
  101. package/src/nodes.ts +19 -0
  102. package/README.md +0 -4
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/graph2d/curve.ts", "../../src/graph2d/curve-cache.ts", "../../src/graph2d/curve-error.ts", "../../src/graph2d/graph2d.ts", "../../src/graph2d/plane.ts", "../../src/graph2d/plane-fill.ts", "../../src/graph2d/shared.ts", "../../src/graph3d/expression.ts", "../../src/graph3d/graph3d.ts", "../../src/graph3d/shared.ts", "../../src/nodes.ts"],
4
+ "sourcesContent": ["/**\n * Turning `y = f(x)` into polylines you can actually hand a rasterizer.\n *\n * This is the whole difficulty of a 2D grapher, and it is not the sampling loop\n * anybody writes first. That loop — walk the pixel columns, evaluate, skip\n * anything that isn't on screen — fails in four separate ways, and each failure\n * looks like a *different* bug:\n *\n * 1. **Dropping off-view points instead of clipping segments.** If the last\n * point you keep is the last one that landed inside the box, the curve stops\n * in mid-air an arbitrary distance from the edge. `log(x)` appears to end\n * somewhere over the plot rather than plunging out of the bottom of it. The\n * fix is to build the segment anyway and clip *it* against the frame, so the\n * curve leaves through the edge exactly where it really crosses.\n *\n * 2. **Handing the path API astronomical coordinates.** `log(1e-300)` is about\n * -690, which at any sane zoom is millions of pixels below the box. Skia's\n * scan converter is fixed-point, so a coordinate past roughly ±32k overflows\n * and the contour is *silently dropped* — the curve doesn't render wrong, it\n * doesn't render at all. Clipping (1) already bounds what reaches the path\n * API to the frame plus a hair; {@link COORD_LIMIT} bounds the arithmetic on\n * the way there, so no infinity ever reaches the clipper to turn a `t` into a\n * `NaN`.\n *\n * 3. **Sampling uniformly in x.** Near an asymptote one pixel of x is thousands\n * of pixels of y, so a uniform grid gives a visible polygonal kink where the\n * curve turns; far from one, the same grid spends hundreds of samples on a\n * straight line. {@link sampleCurve} subdivides recursively instead — split\n * any segment that is not flat *in screen space*, stop at a quarter pixel —\n * so sample density follows curvature and comes out dense near `x = 0` for\n * `log` and sparse where it is nearly straight. The flatness test pairs a\n * distance check with an **angle** check, because the distance alone reads a\n * symmetric wiggle (whose midpoint sits exactly on the chord) as flat.\n *\n * 4. **Treating \"undefined\" and \"very large\" as the same thing.** `log(-1)` is\n * `NaN`: a genuine hole, and the path has to break. `log(1e-300)` is finite\n * and enormous: the curve is still there, still continuous, and has to keep\n * being drawn until it leaves the frame. Conflating the two is the other half\n * of the cut-off in (1). At a defined/undefined edge this bisects to find\n * where the domain actually ends, so `sqrt(x)` terminates at the origin\n * rather than at whichever sample happened to be last.\n *\n * Everything here is pure and works in node-local pixels, so it is testable with\n * no renderer: the caller supplies the graph→pixel mapping and gets back runs of\n * points ready to stroke.\n */\n\nimport type { Vector2 } from \"@motionscript/sdk\"\n\n/**\n * The graph→pixel mapping, as four numbers rather than two closures: this is the\n * hot path (a few thousand projections per curve per frame) and the closures\n * would be called through a megamorphic call site.\n *\n * Pixels are the node's own local space — origin at the node's centre, y **up**,\n * which is both the space `Graphics` draws in and, conveniently, the orientation\n * a graph is thought about in.\n */\nexport interface PlaneMap {\n /** Graph x at the node's horizontal centre. */\n centerX: number\n /** Graph y at the node's vertical centre. */\n centerY: number\n /** Pixels per graph unit along x. */\n scaleX: number\n /** Pixels per graph unit along y. */\n scaleY: number\n}\n\n/** An axis-aligned rectangle in node-local pixels, y-up. */\nexport interface PixelRect {\n left: number\n right: number\n bottom: number\n top: number\n}\n\n/**\n * Hard bound on a projected coordinate, in node pixels.\n *\n * Not a rendering decision — clipping is what decides what the rasterizer sees.\n * This exists so the *arithmetic* between projection and clipping stays finite:\n * `1/x` at 1e-320 projects to something that overflows to `Infinity`, and an\n * `Infinity` in the clipper's `q / p` produces a `NaN` `t` that quietly discards\n * the segment — which would put the cut-off of (1) back in through a side door.\n *\n * Large enough that no clamped point is ever inside a plausible frame (so the\n * clamp can't bend a visible part of the curve), small enough to stay far below\n * the fixed-point limit even after a parent scales the node up.\n */\nconst COORD_LIMIT = 16_000\n\n/** How far a sample may sit from the chord before the segment is split, in px. */\nconst TOLERANCE_PX = 0.25\n\n/**\n * How narrow a segment may get before the subdivision stops, in pixels of x.\n *\n * A **pixel** size rather than a recursion depth, and that is the single change\n * that bounds this module's cost. Leaves tile the frame, so however violent the\n * function is the whole tree costs at most `2 · width / MIN_SEGMENT_PX`\n * evaluations — about seven and a half thousand on a 1920-px node, against the\n * million a depth of ten over five hundred seeds allowed. Half a pixel because\n * that is where one more halving cannot move a stroked line by as much as its\n * own antialiasing.\n */\nconst MIN_SEGMENT_PX = 0.5\n\n/**\n * How far the two halves of a segment may disagree in direction before it is\n * split, in radians (~2.9°).\n *\n * The reason the distance test alone is not enough: a symmetric wiggle puts its\n * midpoint *exactly* on the chord, so the perpendicular distance is zero and a\n * distance-only test declares a visible kink flat. Two tests, and a segment has\n * to pass both.\n */\nconst ANGLE_TOLERANCE = 0.05\n\n/**\n * Backstop on the recursion depth.\n *\n * {@link MIN_SEGMENT_PX} stops the subdivision first at every box size\n * {@link seedCount} can produce — the deepest is about six levels on an 8000-px\n * node — so this only ever fires if a seed interval somehow came out enormous.\n * It is here so the recursion cannot run away on a stack rather than because any\n * curve needs it.\n */\nconst MAX_DEPTH = 12\n\n/**\n * Evaluations one curve may spend in one frame.\n *\n * The refinement alone cannot reach this — see {@link MIN_SEGMENT_PX} — so what\n * this really bounds is the boundary bisection, which is logarithmic per domain\n * edge but unbounded in the *number* of edges: `sqrt(sin(1000x))` has hundreds.\n * At roughly a tenth of a microsecond for a compiled expression tree this is\n * about four milliseconds, which is as much of a 60 fps frame as one curve may\n * have to itself.\n */\nconst MAX_EVALUATIONS = 40_000\n\n/**\n * Evaluations every curve is guaranteed, however many of them there are: the\n * seed pass with room for two levels of refinement over all of it.\n *\n * A graph carrying twenty curves draws all twenty coarsely rather than the first\n * three well and the rest not at all.\n */\nconst MIN_EVALUATIONS = 2_048\n\n/**\n * Evaluations all the curves on one node share in a frame.\n *\n * Three at their individual ceiling, or the verification harness's six at twenty\n * thousand each — about twelve milliseconds in the worst case, which is the\n * point past which degrading the curves beats dropping the frame.\n */\nconst FRAME_EVALUATIONS = 120_000\n\n/**\n * How many times a defined/undefined edge is bisected before the curve is cut.\n *\n * Generous because the interesting case is not `sqrt(x)`, where the value at the\n * boundary is finite and a handful of steps land on it. It is `log(x)`, whose\n * boundary value is unbounded: each halving buys a constant amount of extra\n * descent, so terminating early leaves the curve stopping *inside* the frame,\n * which is the exact artefact this module exists to prevent. The loop exits as\n * soon as the defined side has left the frame, so the full count is only ever\n * paid on a boundary that stays on screen.\n */\nconst BOUNDARY_STEPS = 60\n\n/** Roughly how far apart the initial uniform samples are laid, in pixels. */\nconst SEED_SPACING_PX = 8\nconst MIN_SEEDS = 64\nconst MAX_SEEDS = 512\n\n/**\n * Ceiling on the points one curve may emit.\n *\n * Adaptive subdivision is self-limiting on anything anybody plots deliberately;\n * it is not self-limiting on `sin(10000x)`, where every segment genuinely is\n * curved at every scale and the recursion expands to its full width. This is the\n * backstop, and going over it degrades the curve to the resolution already\n * reached rather than dropping it.\n */\nconst MAX_POINTS = 24_000\n\n/** A `y = f(x)` curve, sampled by {@link sampleCurve}. */\nexport type CurveFunction = (x: number) => number\n\n/** One evaluation of the curve, projected and classified. */\ninterface Sample {\n /**\n * Offset from the view's centre, in graph units — the parameter of record.\n *\n * The sampler walks `t` and not `x` because `px` is then one multiply,\n * `t · scaleX`, instead of a round trip out to `centerX` and back:\n * `x = centerX + t` followed by `(x - centerX) · scaleX` is a literal\n * add-then-subtract through a number whose ulp can be wider than the whole\n * visible window. At `centerX = 1e9` and a span of 1e-6 that round trip leaves\n * eight distinct pixel positions across the screen, and the curve staircases.\n */\n t: number\n /** `centerX + t` — what `f` actually saw, kept for the bisections' float-resolution guards. */\n x: number\n /** Graph y — kept raw, so the sign is readable either side of a pole. */\n y: number\n /** Node-local pixels, clamped to {@link COORD_LIMIT}. */\n px: number\n py: number\n /**\n * Whether there is a point here at all: `false` for `NaN` (outside the\n * function's domain) **and** for `±Infinity` (a pole landed on exactly). Both\n * break the path; what separates them is where the bisection either side\n * converges to, which the caller never has to know.\n */\n ok: boolean\n}\n\nfunction blankSample(): Sample {\n return { t: 0, x: 0, y: 0, px: 0, py: 0, ok: false }\n}\n\n/**\n * Reused {@link Sample} storage.\n *\n * Every evaluation used to be a fresh object. Six curves at sixty frames a\n * second is millions of short-lived allocations a second, which is a collector\n * running *through* the animation rather than between takes. The recursion is a\n * depth-first walk over one seed interval and nothing outlives that interval, so\n * the arena hands out slots and the seed loop resets the mark — which makes\n * every allocation after the first frame a field write on an object whose hidden\n * class never changes.\n *\n * Module scope, and therefore not re-entrant. Nothing re-enters: one\n * {@link sampleCurve} runs to completion before the next begins, and an arena\n * per call would allocate the thing it exists to avoid.\n */\nconst arena: Sample[] = []\nlet arenaMark = 0\n\nfunction takeSample(): Sample {\n const slot = arena[arenaMark]\n if (slot !== undefined) {\n arenaMark++\n return slot\n }\n const fresh = blankSample()\n arena.push(fresh)\n arenaMark++\n return fresh\n}\n\n/**\n * A ceiling on curve evaluations, shared by every curve drawn in one frame.\n *\n * Shared rather than per curve because what a frame can afford is a property of\n * the frame: one curve with a pole every three pixels must not be allowed to\n * cost what six ordinary ones do, and six ordinary ones must not each be held to\n * a sixth of the budget when five of them are straight lines. A cheap curve\n * hands its slack to whatever comes after it.\n */\nexport interface SampleBudget {\n /** Evaluations left for the curves still to be drawn. */\n remaining: number\n /** How many curves are still to be drawn, this one included. */\n pending: number\n}\n\n/** A frame's budget, to be handed to each of `curves` {@link sampleCurve} calls. */\nexport function sampleBudget(curves: number): SampleBudget {\n return { remaining: FRAME_EVALUATIONS, pending: Math.max(1, curves) }\n}\n\n/**\n * Samples `f` across the x range `clip` covers and returns the polylines to\n * stroke: node-local pixel points, clipped to `clip`, split wherever the curve\n * genuinely stops.\n *\n * `clip` should be the node's box grown by about a stroke width, so a curve\n * running just outside the frame still paints the half of its stroke that falls\n * inside it. Nothing outside that rectangle is returned, which is what keeps the\n * rasterizer's fixed-point limit out of reach — see (2) in the module note.\n */\nexport function sampleCurve(\n f: CurveFunction,\n map: PlaneMap,\n clip: PixelRect,\n budget?: SampleBudget\n): Vector2[][] {\n const pen = new CurvePen(clip)\n\n // Sample across the *clip*, not the box: the extra sliver either side is what\n // lets a curve enter the frame already at full stroke width instead of\n // starting with a cap flush against the edge.\n //\n // In offsets from the centre rather than in x — see {@link Sample.t}.\n const tLo = clip.left / map.scaleX\n const tHi = clip.right / map.scaleX\n if (!(tHi > tLo) || !(map.scaleX > 0) || !(map.scaleY > 0)) return []\n\n const seeds = seedCount(clip.right - clip.left)\n const step = (tHi - tLo) / seeds\n\n // This curve's slice of the frame's evaluations. Bounded above so one curve\n // can't eat the frame, and below so a graph full of them still draws every one.\n const share = budget\n ? Math.min(\n MAX_EVALUATIONS,\n Math.max(\n MIN_EVALUATIONS,\n Math.floor(budget.remaining / Math.max(1, budget.pending))\n )\n )\n : MAX_EVALUATIONS\n let spent = 0\n\n const write = (slot: Sample, t: number): Sample => {\n const x = map.centerX + t\n const y = f(x)\n spent++\n const ok = Number.isFinite(y)\n slot.t = t\n slot.x = x\n slot.y = y\n slot.px = clampCoord(t * map.scaleX)\n // A non-finite y has no position; 0 is a placeholder the `ok` flag keeps\n // anyone from reading.\n slot.py = ok ? clampCoord((y - map.centerY) * map.scaleY) : 0\n slot.ok = ok\n return slot\n }\n\n const at = (t: number): Sample => write(takeSample(), t)\n\n /**\n * The last defined sample on the way from `from` towards `to`, where exactly\n * one of the two is defined.\n *\n * Three exits. Off the frame, because past the edge the only thing further\n * halvings buy is a longer segment that is going to be clipped to the same\n * place anyway — this is the one `log` leaves by, and the reason\n * {@link BOUNDARY_STEPS} is generous. *Converged*, because a boundary whose\n * value is finite stops moving after a handful of steps and `sqrt` was paying\n * for all sixty of them. And float resolution, which nothing reaches.\n */\n const edgeOf = (from: Sample, to: Sample): Sample => {\n let inside = from\n let outsideT = to.t\n for (let i = 0; i < BOUNDARY_STEPS; i++) {\n if (spent >= share) break\n if (offFrame(inside, clip)) break\n\n const midT = (inside.t + outsideT) / 2\n if (midT === inside.t || midT === outsideT) break // float resolution\n const mid = at(midT)\n if (!mid.ok) {\n outsideT = midT\n continue\n }\n const settled =\n Math.abs(mid.px - inside.px) < TOLERANCE_PX &&\n Math.abs(mid.py - inside.py) < TOLERANCE_PX\n inside = mid\n if (settled) break\n }\n return inside\n }\n\n const emit = (\n a: Sample,\n m: Sample | null,\n b: Sample,\n floored: boolean\n ): void => {\n if (a.ok && b.ok) {\n if (floored && diverges(a, m, b, clip)) {\n pen.break()\n return\n }\n pen.draw(a, b)\n return\n }\n if (a.ok) {\n const edge = edgeOf(a, b)\n if (edge.t !== a.t) pen.draw(a, edge)\n pen.break()\n return\n }\n if (b.ok) {\n pen.break()\n const edge = edgeOf(b, a)\n if (edge.t !== b.t) pen.draw(edge, b)\n else pen.start(b)\n return\n }\n pen.break()\n }\n\n const refine = (a: Sample, b: Sample, depth: number): void => {\n // Out of budget, or out of room in the path: draw the chord. Deliberately\n // *not* routed through the pole test — with no midpoint there is nothing to\n // judge by, and a vertical line through an asymptote is an ugly frame where\n // a missing branch is a wrong one.\n if (spent >= share || pen.points >= MAX_POINTS) {\n emit(a, null, b, false)\n return\n }\n const m = at((a.t + b.t) / 2)\n const floored = depth >= MAX_DEPTH || b.px - a.px <= MIN_SEGMENT_PX\n if (floored || flat(a, m, b)) {\n // \"Gave up\" rather than \"settled\" is what identifies a pole, which is why\n // the midpoint travels with the verdict — see {@link diverges}.\n emit(a, m, b, floored)\n return\n }\n refine(a, m, depth + 1)\n refine(m, b, depth + 1)\n }\n\n // The seed endpoints come from their own pair rather than from the arena:\n // `previous` has to survive the reset that recycles the interval it was the\n // right-hand end of.\n const ends: [Sample, Sample] = [blankSample(), blankSample()]\n let previous = write(ends[0], tLo)\n for (let i = 1; i <= seeds; i++) {\n // Recomputed from the ends rather than accumulated, so rounding can't drift\n // the grid off the right-hand edge over five hundred additions.\n const next = write(ends[i & 1], i === seeds ? tHi : tLo + i * step)\n arenaMark = 0\n refine(previous, next, 0)\n previous = next\n }\n arenaMark = 0\n\n if (budget) {\n budget.remaining = Math.max(0, budget.remaining - spent)\n budget.pending = Math.max(0, budget.pending - 1)\n }\n\n return pen.finish()\n}\n\n/** How many uniform samples to lay down before refining anything. */\nfunction seedCount(widthPx: number): number {\n const wanted = Math.round(widthPx / SEED_SPACING_PX)\n return Math.min(MAX_SEEDS, Math.max(MIN_SEEDS, wanted))\n}\n\n/**\n * Whether the arc `a → m → b` is straight enough, in **screen** space, to draw\n * as one segment.\n *\n * Screen space and not graph space, because what \"straight enough\" has to mean\n * is \"the eye can't tell\" — a tolerance in graph units would subdivide a flat\n * line to death when zoomed out and give up mid-curve when zoomed in.\n */\nfunction flat(a: Sample, m: Sample, b: Sample): boolean {\n // A domain edge somewhere inside: always split, so the bisection that finds it\n // runs on as short an interval as possible.\n if (a.ok !== m.ok || m.ok !== b.ok) return false\n // Nothing defined anywhere across it — there is no curve here to be wrong\n // about, so stop.\n if (!a.ok) return true\n\n const dx = b.px - a.px\n const dy = b.py - a.py\n const length = Math.hypot(dx, dy) || 1\n const distance = Math.abs((m.px - a.px) * dy - (m.py - a.py) * dx) / length\n if (distance > TOLERANCE_PX) return false\n\n // The second half of the test — see {@link ANGLE_TOLERANCE}.\n const first = Math.atan2(m.py - a.py, m.px - a.px)\n const second = Math.atan2(b.py - m.py, b.px - m.px)\n let turn = Math.abs(first - second)\n if (turn > Math.PI) turn = 2 * Math.PI - turn\n return turn < ANGLE_TOLERANCE\n}\n\n/**\n * Whether a segment the sampler had to give up on is an **asymptote** — the one\n * case where drawing nothing beats drawing the chord.\n *\n * Two conditions, and the second is the one this module was missing. The ends\n * must have run off *opposite* edges, which is what a vertical line through the\n * frame looks like. And the curve must actually turn back inside the segment: a\n * midpoint outside the range its own ends span is a pole (`tan` climbs to +∞ on\n * one side of π/2 and arrives from -∞ on the other, so the midpoint is always\n * outside), while a midpoint *between* them is an ordinary steep climb.\n *\n * Leaving the second test out is what cut `log(x)` off in mid-air. Once the y\n * axis is stretched, `log` crosses the whole visible band inside a single leaf;\n * the ends of that leaf are both off the frame, so its neighbours clip away to\n * nothing, and treating the leaf itself as a pole deletes not a gap but the\n * entire crossing — the exact artefact this module exists to prevent, arriving\n * through the door meant to keep it out. `exp`, `x^20` and `1/x` all reach it.\n *\n * A `null` midpoint means the sampler ran out of budget and has nothing to judge\n * by. Draw it: only a curve that has already spent forty thousand evaluations\n * can get here, and a spurious vertical line is the lesser wrong.\n */\nfunction diverges(\n a: Sample,\n m: Sample | null,\n b: Sample,\n clip: PixelRect\n): boolean {\n if (!straddles(a, b, clip)) return false\n if (m === null) return false\n if (!m.ok) return true\n return m.y < Math.min(a.y, b.y) || m.y > Math.max(a.y, b.y)\n}\n\n/** Whether the two ends have run off *opposite* edges of the frame. */\nfunction straddles(a: Sample, b: Sample, clip: PixelRect): boolean {\n return (\n (a.py > clip.top && b.py < clip.bottom) ||\n (a.py < clip.bottom && b.py > clip.top)\n )\n}\n\n/** Whether a sample has left the frame entirely. */\nfunction offFrame(sample: Sample, clip: PixelRect): boolean {\n return sample.py > clip.top || sample.py < clip.bottom\n}\n\nfunction clampCoord(value: number): number {\n if (value > COORD_LIMIT) return COORD_LIMIT\n if (value < -COORD_LIMIT) return -COORD_LIMIT\n // `NaN` cannot reach here (the caller checks `ok` first), but a `-0` would\n // print oddly in a test snapshot and costs nothing to normalise.\n return value === 0 ? 0 : value\n}\n\n/**\n * Accumulates clipped polylines, one segment at a time.\n *\n * Incremental rather than \"collect every point, then clip the polyline\" because\n * a curve is built by a recursion that already knows where its breaks are, and\n * because the intermediate list is the thing that would carry the astronomical\n * coordinates this module exists to keep out of the renderer.\n */\nclass CurvePen {\n private readonly runs: Vector2[][] = []\n private open: Vector2[] = []\n /** The clipped end of the last drawn segment, for joining the next one to. */\n private cursor: Vector2 | null = null\n /** Points emitted so far, against {@link MAX_POINTS}. */\n points = 0\n\n constructor(private readonly clip: PixelRect) {}\n\n /** Draws `a → b`, clipped; continues the open run when it joins on. */\n draw(a: Sample, b: Sample): void {\n const segment = clipSegment(a.px, a.py, b.px, b.py, this.clip)\n if (!segment) {\n // Wholly outside the frame: whatever run was open ended at the edge.\n this.break()\n return\n }\n const [start, end] = segment\n if (this.cursor && samePoint(this.cursor, start)) {\n this.push(end)\n } else {\n this.break()\n this.push(start)\n this.push(end)\n }\n this.cursor = end\n }\n\n /** Begins a run at `b` without drawing anything into it yet. */\n start(b: Sample): void {\n this.break()\n if (inside(b.px, b.py, this.clip)) {\n this.push({ x: b.px, y: b.py })\n this.cursor = { x: b.px, y: b.py }\n }\n }\n\n /** Ends the open run. A run of fewer than two points draws nothing. */\n break(): void {\n if (this.open.length >= 2) this.runs.push(this.open)\n this.open = []\n this.cursor = null\n }\n\n finish(): Vector2[][] {\n this.break()\n return this.runs\n }\n\n private push(point: Vector2): void {\n this.open.push(point)\n this.points++\n }\n}\n\n/** Points within a tenth of a pixel are the same point, for run-joining. */\nfunction samePoint(a: Vector2, b: Vector2): boolean {\n return Math.abs(a.x - b.x) < 0.1 && Math.abs(a.y - b.y) < 0.1\n}\n\nfunction inside(x: number, y: number, clip: PixelRect): boolean {\n return x >= clip.left && x <= clip.right && y >= clip.bottom && y <= clip.top\n}\n\n/**\n * Liang–Barsky clip of one segment against `clip`. Returns the surviving\n * `[start, end]`, or `null` when the segment lies wholly outside.\n *\n * The piece of this module that turns \"the curve stops in mid-air\" into \"the\n * curve leaves through the edge\": the off-frame endpoint is kept and the\n * *segment* is cut, rather than the point being discarded and the segment never\n * built.\n */\nfunction clipSegment(\n ax: number,\n ay: number,\n bx: number,\n by: number,\n clip: PixelRect\n): [Vector2, Vector2] | null {\n const dx = bx - ax\n const dy = by - ay\n let t0 = 0\n let t1 = 1\n\n // Each edge as (p, q): the segment survives where p·t <= q.\n const edges: [number, number][] = [\n [-dx, ax - clip.left],\n [dx, clip.right - ax],\n [-dy, ay - clip.bottom],\n [dy, clip.top - ay],\n ]\n for (const [p, q] of edges) {\n if (p === 0) {\n // Parallel to this edge: outside it means the whole segment is out.\n if (q < 0) return null\n continue\n }\n const r = q / p\n if (p < 0) {\n if (r > t1) return null\n if (r > t0) t0 = r\n } else {\n if (r < t0) return null\n if (r < t1) t1 = r\n }\n }\n return [\n { x: ax + t0 * dx, y: ay + t0 * dy },\n { x: ax + t1 * dx, y: ay + t1 * dy },\n ]\n}\n", "/**\n * One frame's worth of sampled curves, kept for the next one.\n *\n * `renderSelf` runs every frame; between two frames the thing that has normally\n * changed is nothing at all — the playhead moved through a stretch where this\n * node holds still, or some other node redrew and took the whole scene with it.\n * Re-running the sampler for a picture whose curves have not moved is the single\n * largest avoidable cost in the node.\n *\n * ## Returning the *same arrays* is half the point\n *\n * `skia-render` keys its geometry cache by node and shape index and compares the\n * new descriptor against the old with a shallow walk that short-circuits on\n * reference identity. Handing back a fresh array of identical points would still\n * miss, rebuild the wasm path, and pay for the walk that discovered it needn't\n * have. Handing back the same array instance skips both.\n *\n * ## Two keys, of deliberately different kinds\n *\n * The **plane** is compared structurally: eight numbers, so a pan of a hundredth\n * of a pixel is a miss and a redraw of an untouched frame is a hit.\n *\n * The **function** is compared by *identity*, which `compileCurveCached` is what\n * makes meaningful — equal source is the identical closure, so an inspector\n * keystroke that doesn't change the expression re-samples nothing. And a curve\n * caught mid-tween is a fresh closure on every frame by design (`blendCurve`),\n * so it correctly misses: it really is changing.\n *\n * Colour and opacity are deliberately *not* in the key. They live on the paint,\n * not on the shape, so dragging a curve's colour must not throw its geometry\n * away.\n */\n\nimport type { Vector2 } from \"@motionscript/sdk\"\n\nimport {\n sampleCurve,\n type CurveFunction,\n type PixelRect,\n type PlaneMap,\n type SampleBudget,\n} from \"./curve\"\n\n/** Everything about the view that decides where a curve's points come out. */\nexport function runsKeyOf(map: PlaneMap, clip: PixelRect): string {\n return `${map.centerX}|${map.centerY}|${map.scaleX}|${map.scaleY}|${clip.left}|${clip.right}|${clip.bottom}|${clip.top}`\n}\n\ninterface Entry {\n key: string\n sample: CurveFunction\n runs: Vector2[][]\n}\n\n/** The sampled runs of every curve on one node, held across frames. */\nexport class CurveRunsCache {\n private readonly entries = new Map<string, Entry>()\n\n /**\n * Whether {@link runs} would answer from memory rather than re-sample.\n *\n * Asked of every curve *before* any of them is sampled, so the frame's\n * evaluation budget can be divided among the ones that actually have work to\n * do — five cached curves and one that moved gives the whole budget to the\n * one that moved.\n */\n holds(id: string, sample: CurveFunction, key: string): boolean {\n const hit = this.entries.get(id)\n return hit !== undefined && hit.key === key && hit.sample === sample\n }\n\n /** This curve's polylines, sampled only if something that decides them moved. */\n runs(\n id: string,\n sample: CurveFunction,\n key: string,\n map: PlaneMap,\n clip: PixelRect,\n budget?: SampleBudget\n ): Vector2[][] {\n const hit = this.entries.get(id)\n if (hit !== undefined && hit.key === key && hit.sample === sample) {\n return hit.runs\n }\n const runs = sampleCurve(sample, map, clip, budget)\n this.entries.set(id, { key, sample, runs })\n return runs\n }\n\n /**\n * Forgets every curve not in `ids` — the ones deleted, switched off, or faded\n * to nothing since the last frame.\n *\n * Without it a node whose equation list is edited a hundred times holds a\n * hundred stale point lists, none of which anything will ever ask for again.\n */\n retain(ids: readonly string[]): void {\n if (this.entries.size === ids.length) return\n for (const id of this.entries.keys()) {\n if (!ids.includes(id)) this.entries.delete(id)\n }\n }\n}\n", "/**\n * Why an expression won't compile as `y = f(x)`, in the terms the tile shows.\n *\n * A one-line binding of the shared checker to this node's single free variable,\n * and it is worth its own name for what it *rejects*: `x + y` is a perfectly\n * good surface and a mistake on a curve, and the tile has to say so rather than\n * silently plotting `x`. The 3D graph's `expressionError` is the same function\n * bound the other way.\n */\n\nimport {\n CURVE_VARIABLES,\n expressionError,\n} from \"../kit/expression\"\n\n/** `null` when `source` compiles as `y = f(x)`, else the reason it doesn't. */\nexport function curveError(source: string): string | null {\n return expressionError(source, CURVE_VARIABLES)\n}\n", "import {\n node,\n Clip,\n Fills,\n Graphics2D,\n Node2D,\n clamp,\n lerpNumber,\n property,\n type Color,\n type Node2DProps,\n type NormalizedColor,\n type RenderContext2D,\n} from \"@motionscript/sdk\"\n\nimport { sampleBudget, type PixelRect } from \"./curve\"\nimport { CurveRunsCache, runsKeyOf } from \"./curve-cache\"\nimport {\n formatTick,\n inflate,\n labelLadder,\n labelWidthEm,\n planeBox,\n resolvePlane,\n tickLadder,\n toPx,\n toPy,\n type PlaneView,\n type ResolvedPlane,\n type TickLadder,\n} from \"./plane\"\nimport {\n PLANE_SOURCE,\n planeUniforms,\n planeVisible,\n type PlaneStyle,\n} from \"./plane-fill\"\nimport {\n compileCurveCached,\n lerpColor,\n lerpCurves,\n resolveColor,\n snapFlag,\n snapValue,\n type CurveResolved,\n} from \"./shared\"\n\n// --- Authored props --------------------------------------------------------\n// The loose shapes a caller writes. Each has a matching `*Resolved` type below:\n// what the `@property` mapper turns it into and what the tween interpolates.\n\n/** One plotted curve, `y = f(x)`. */\nexport interface Graph2DEquation {\n /** Identity across list changes — see {@link Graph2D.equations}. */\n id: string | number\n /** The expression, evaluated as `y = f(x)` (e.g. `sin(x)`, `log(x)`). */\n expression: string\n /** The curve's colour: any {@link Color} — a CSS string or an RGBA tuple. */\n color: Color\n /** Whether the curve is drawn. Defaults to true. */\n visible?: boolean\n /** How opaque the curve is, 0–1. Defaults to 1. */\n opacity?: number\n}\n\n/** The paper the curves are drawn on: the grid, the axes and their numbers. */\nexport interface Graph2DGrid {\n /** Whether to rule the plane at all. Defaults to true. */\n showGrid?: boolean\n /** Whether to draw the faint lines between the numbered ones. Defaults to true. */\n showMinorGrid?: boolean\n /** Whether to draw the two lines through the origin. Defaults to true. */\n showAxes?: boolean\n /** Whether to number the axes. Defaults to true. */\n showLabels?: boolean\n colorGrid?: Color\n colorMinorGrid?: Color\n colorAxis?: Color\n colorLabel?: Color\n /** Width of a grid line. The minor lines are drawn at two thirds of it. */\n lineWidth?: number\n /** Width of the two axis lines. */\n axisWidth?: number\n /** Size of the axis numbers. Also sets how close two of them may sit. */\n fontSize?: number\n /** Family the axis numbers are set in. Empty takes the built-in face. */\n fontFamily?: string\n}\n\nexport interface Graph2DProps extends Node2DProps {\n /** The curves to plot, each evaluated as `y = f(x)` across the visible range. */\n equations: Graph2DEquation[]\n /** Graph x at the centre of the box — see {@link PlaneView}. */\n centerX: number\n /** Graph y at the centre of the box. */\n centerY: number\n /** Width of the visible window, in graph units. */\n xSpan: number\n /**\n * Height of the visible window in graph units, or **0 to keep units square**,\n * which is the default and almost always what you want. See {@link PlaneView}.\n */\n ySpan: number\n grid: Graph2DGrid\n /** The paper's colour. */\n background: Color\n /** How opaque the paper is, 0–1. At 0 the graph draws over whatever is behind it. */\n backgroundOpacity: number\n /** How thick a plotted curve is drawn. */\n lineWidth: number\n}\n\n// --- Resolved props --------------------------------------------------------\n// Canonical, fully defaulted, interpolatable. Nothing here is optional, so the\n// draw never re-derives a default and the tweens never test for one.\n\n/** {@link Graph2DGrid} after {@link resolveGrid}. */\ninterface GridResolved {\n showGrid: boolean\n showMinorGrid: boolean\n showAxes: boolean\n showLabels: boolean\n colorGrid: NormalizedColor\n colorMinorGrid: NormalizedColor\n colorAxis: NormalizedColor\n colorLabel: NormalizedColor\n lineWidth: number\n axisWidth: number\n fontSize: number\n fontFamily: string\n}\n\n/**\n * The grid a node with nothing said about it draws — the same dark palette the\n * 3D graph opens on, so the two read as one family when they share a scene.\n */\nconst DEFAULT_GRID: GridResolved = {\n showGrid: true,\n showMinorGrid: true,\n showAxes: true,\n showLabels: true,\n colorGrid: resolveColor(\"#374151\"),\n colorMinorGrid: resolveColor(\"#1f2937\"),\n colorAxis: resolveColor(\"#6b7280\"),\n colorLabel: resolveColor(\"#9ca3af\"),\n lineWidth: 2,\n axisWidth: 3,\n fontSize: 22,\n fontFamily: \"\",\n}\n\n/** How far a curve may run past the box before it is cut away, in stroke widths. */\nconst CLIP_MARGIN = 1\n\n/**\n * How close two stacked numbers may sit before the ladder steps up, in font\n * sizes.\n *\n * Only the vertical one lives here. Across, how close two numbers may sit\n * depends on how *long* they are — five characters near the origin, fourteen at\n * a deep zoom — so that ladder is built by `labelLadder`, which measures what it\n * just produced and widens itself once if it has to. Down, a number is one line\n * whatever it says.\n */\nconst LABEL_PITCH_Y = 2.6\nconst LABEL_GAP = 0.45\n/**\n * Narrowest the box a number is aligned within may be, in font sizes.\n *\n * Deliberately wider than the numbers an ordinary view carries, so a\n * right-aligned `-1200` and a right-aligned `5` end in the same place. It only\n * ever grows from here — see `labelWidthEm`.\n */\nconst LABEL_BOX = 6\n\n/**\n * A 2D function grapher — any number of `y = f(x)` curves over a ruled plane,\n * with numbered axes.\n *\n * <Graph2D\n * width={960} height={540}\n * equations={[{ id: 1, expression: \"log(x)\", color: \"#3B82F6\" }]}\n * />\n *\n * The sibling of {@link Graph3D}, and deliberately built the same way: the same\n * little expression language, the same equation tiles, the same match-by-id\n * tween over the list. What it does *not* share is the hard part, which is not\n * the maths but the drawing — see `curve.ts`, which exists because the obvious\n * sampling loop cuts `log(x)` off in mid-air, silently drops `1/x` entirely on a\n * fixed-point rasterizer, and puts a visible polygonal kink wherever a curve\n * turns sharply.\n *\n * Every prop is a `@property`, so a constant, a `() => signal()` binding, and\n * `set()`/`to()` on a ref all work and agree. The non-scalar props each declare\n * a `mapper` (loose authored shape → one canonical internal shape) and a `tween`\n * (how that shape interpolates) — without the tween, `to({ grid: … })` would hold\n * the old value and snap at the end, which is the standard trap for an\n * object-valued attribute.\n *\n * **The view is four plain numbers, not a transform.** `centerX`/`centerY` and\n * `xSpan`/`ySpan` are each independently tweenable, which is what makes \"pan\n * across while zooming in\" one command and what keeps every frame reproducible\n * under scrubbing and export. The canvas's pan/zoom gesture writes exactly these\n * four, so a view flown by hand is a view a `to` can animate afterwards.\n */\n@node({\n key: \"graph2d\",\n parentKey: \"node\",\n forkable: true,\n layout: {\n children: \"freeform\",\n defaultWidthMode: \"fixed\",\n defaultHeightMode: \"fixed\",\n acceptsChildren: true,\n },\n seed: {\n width: 960,\n height: 540,\n },\n})\nexport class Graph2D extends Node2D<Graph2DProps> {\n /**\n * The plotted curves.\n *\n * The mapper compiles each expression **once per write** (memoised by source),\n * so the draw — which re-runs every frame — never re-parses; and it folds\n * `visible`/`opacity` into a single number, so hiding is expressible as a\n * fade. An expression that doesn't parse is dropped and the rest still draw,\n * which is what lets the inspector commit on every keystroke.\n *\n * The tween matches the two lists by `id` — see `lerpCurves`.\n */\n @property({ default: [], mapper: resolveEquations, tween: lerpCurves })\n declare equations: Graph2DEquation[]\n\n @property({ default: 0 }) declare centerX: number\n @property({ default: 0 }) declare centerY: number\n @property({ default: 20 }) declare xSpan: number\n @property({ default: 0 }) declare ySpan: number\n\n /** The paper. Flags snap at the end of a tween; the rest lerp. */\n @property({ default: {}, mapper: resolveGrid, tween: lerpGrid })\n declare grid: Graph2DGrid\n\n @property({ default: \"#111827\", mapper: resolveColor, tween: lerpColor })\n declare background: Color\n @property({ default: 1 }) declare backgroundOpacity: number\n @property({ default: 5 }) declare lineWidth: number\n\n /**\n * The node's one piece of memoisation, and it earns its keep: a graph that is\n * holding still re-samples nothing at all. See `curve-cache.ts`.\n */\n private readonly runs = new CurveRunsCache()\n\n /** Where the node is looking, as the four props hold it. */\n private get view(): PlaneView {\n return {\n centerX: this.centerX,\n centerY: this.centerY,\n xSpan: this.xSpan,\n ySpan: this.ySpan,\n }\n }\n\n /**\n * Confines every mark to the node's own box — its own drawing as well as its\n * children's, which is why this is `clipPathSelf` and not `clipSelf`.\n *\n * Belt and braces over the geometric clipping in `curve.ts` rather than a\n * substitute for it: that clipping is what keeps astronomical coordinates away\n * from the rasterizer, and this is what stops a stroke's half-width and its\n * round joins from painting a hair outside the frame at the edges.\n */\n protected override clipPathSelf(): Clip | null {\n const width = this.layoutBounds.width\n const height = this.layoutBounds.height\n if (width <= 0 || height <= 0) return null\n return new Clip().rect({ x: 0, y: 0, width, height })\n }\n\n // ---- Drawing ------------------------------------------------------------\n\n protected override renderSelf(ctx: RenderContext2D): void {\n const width = this.layoutBounds.width\n const height = this.layoutBounds.height\n if (width <= 0 || height <= 0) return\n\n const plane = resolvePlane(this.view, width, height)\n const grid = this.grid as unknown as GridResolved\n const box = planeBox(plane)\n\n // One ladder per axis, chosen from the *scale* rather than from a fixed\n // count: what decides how finely the plane is ruled is how close two numbers\n // would sit on screen, which is the only thing that stays true as the view is\n // flown around.\n const xTicks = labelLadder(plane.xMin, plane.xMax, plane.scaleX, grid.fontSize)\n const yTicks = tickLadder(\n plane.yMin,\n plane.yMax,\n plane.scaleY,\n grid.fontSize * LABEL_PITCH_Y\n )\n\n this.drawPlane(ctx, plane, grid, xTicks, yTicks)\n if (grid.showLabels) this.drawLabels(ctx, plane, box, grid, xTicks, yTicks)\n\n this.drawCurves(ctx, plane, box)\n }\n\n /**\n * The paper, the two orders of ruling and the two axes — one rect, one shader,\n * one draw, whatever the zoom.\n *\n * This was six hundred filled rects and three draws, and the rect count is the\n * part that mattered: it changed as the ladder stepped, and the renderer keys\n * its geometry cache by shape *index*, so a grid that gained a line invalidated\n * every curve behind it. One shape means the curves' keys never move. See\n * `plane-fill.ts` for why the built-in grid fill can't do this job and for what\n * the phase uniforms are protecting against.\n */\n private drawPlane(\n ctx: RenderContext2D,\n plane: ResolvedPlane,\n grid: GridResolved,\n xTicks: TickLadder,\n yTicks: TickLadder\n ): void {\n const style: PlaneStyle = {\n paper: this.background as NormalizedColor,\n paperOpacity: clamp01(this.backgroundOpacity),\n showGrid: grid.showGrid,\n showMinorGrid: grid.showMinorGrid,\n showAxes: grid.showAxes,\n colorGrid: grid.colorGrid,\n colorMinorGrid: grid.colorMinorGrid,\n colorAxis: grid.colorAxis,\n lineWidth: grid.lineWidth,\n axisWidth: grid.axisWidth,\n }\n // Nothing to paint at all — clear paper, no rulings, no axes — is a whole\n // fragment pass over the box that would come out transparent.\n if (!planeVisible(style)) return\n\n ctx.draw(\n new Graphics2D()\n .rect({ x: 0, y: 0, width: plane.width, height: plane.height })\n .fill(\n Fills.shader(PLANE_SOURCE, {\n uniforms: planeUniforms(plane, style, xTicks, yTicks),\n coords: \"local\",\n })\n )\n )\n }\n\n /**\n * The numbers along each axis.\n *\n * Pinned to the frame when their axis has left it, which is the behaviour\n * every map and every grapher has and the reason it is worth the arithmetic:\n * pan far enough that the origin is off screen and a graph whose numbers went\n * with it is a graph you can no longer read. The row also flips to the other\n * side of its axis when there is no room on the near one, so a view sitting\n * just above the x-axis still numbers it.\n */\n private drawLabels(\n ctx: RenderContext2D,\n plane: ResolvedPlane,\n box: PixelRect,\n grid: GridResolved,\n xTicks: TickLadder,\n yTicks: TickLadder\n ): void {\n const labels = new Graphics2D()\n const gap = grid.fontSize * LABEL_GAP\n // Per axis, because the two ladders carry different numbers: a view can be\n // ten units wide and a billionth of one tall.\n const xBox = grid.fontSize * Math.max(LABEL_BOX, labelWidthEm(xTicks))\n const yBox = grid.fontSize * Math.max(LABEL_BOX, labelWidthEm(yTicks))\n const font = {\n fontSize: grid.fontSize,\n // An empty family is dropped rather than passed on: the renderer's font\n // provider holds only what the manifest registered, so asking it for a\n // family called `\"\"` shapes against nothing and paints no glyphs. Left\n // unset it inherits the scene's own default face.\n ...(grid.fontFamily === \"\" ? {} : { fontFamily: grid.fontFamily }),\n wrap: false,\n }\n\n // The x-axis row: under the axis when there is room beneath it, over it when\n // there isn't.\n const axisY = clamp(toPy(plane, 0), box.bottom, box.top)\n const under = axisY - gap - grid.fontSize >= box.bottom\n const rowY = under ? axisY - gap - grid.fontSize / 2 : axisY + gap + grid.fontSize / 2\n\n for (const x of xTicks.major) {\n labels.text({\n ...font,\n text: formatTick(x, xTicks.step),\n x: toPx(plane, x),\n y: rowY,\n width: xBox,\n textAlign: \"center\",\n })\n }\n\n // The y-axis column: to the left of the axis when there is room, to the\n // right when the axis is against the left edge.\n // Room for a couple of characters, not for the whole alignment box: the box\n // is deliberately wide (a right-aligned `-1200` and a right-aligned `5` end\n // in the same place), and demanding all of it would push the column to the\n // wrong side of an axis that has plenty of room for the numbers it holds.\n const axisX = clamp(toPx(plane, 0), box.left, box.right)\n const leftOf = axisX - gap - grid.fontSize * 1.5 >= box.left\n const columnX = leftOf ? axisX - gap - yBox / 2 : axisX + gap + yBox / 2\n\n // Zero belongs to the row, not the column: drawn in both it would be two\n // numbers stacked on the origin. Skipped only when the row is actually\n // showing it, so a view with the y-axis off screen still numbers its own 0.\n const rowHasZero = plane.xMin <= 0 && 0 <= plane.xMax\n\n for (const y of yTicks.major) {\n if (y === 0 && rowHasZero) continue\n labels.text({\n ...font,\n text: formatTick(y, yTicks.step),\n x: columnX,\n y: toPy(plane, y),\n width: yBox,\n textAlign: leftOf ? \"right\" : \"left\",\n })\n }\n\n ctx.draw(labels.fill(Fills.color(grid.colorLabel)))\n }\n\n /**\n * The curves themselves, one stroked polyline per run.\n *\n * One `draw` per run rather than one per curve, and that is not an\n * optimisation to undo: shapes chained before a `stroke()` are combined into a\n * single surface, and stroking a union of disjoint open paths paints nothing —\n * so a curve that leaves the frame and comes back would lose *both* halves.\n * `ChartLine` learned the same thing about its spotlight runs.\n */\n private drawCurves(\n ctx: RenderContext2D,\n plane: ResolvedPlane,\n box: PixelRect\n ): void {\n const equations = this.equations as unknown as CurveResolved[]\n const weight = Math.max(0.1, this.lineWidth)\n const clip = inflate(box, weight * CLIP_MARGIN)\n const key = runsKeyOf(plane, clip)\n\n // What actually has to be sampled, counted before any of it is — so a frame\n // where five of six curves are unchanged hands the whole evaluation budget\n // to the one that moved rather than reserving five sixths of it for work\n // that is already done.\n const live: string[] = []\n let misses = 0\n for (const equation of equations) {\n // Fully faded out (hidden, or the far end of a cross-fade): sample nothing.\n if (equation.opacity <= 0.001) continue\n live.push(equation.id)\n if (!this.runs.holds(equation.id, equation.sample, key)) misses++\n }\n const budget = misses > 0 ? sampleBudget(misses) : undefined\n this.runs.retain(live)\n\n for (const equation of equations) {\n if (equation.opacity <= 0.001) continue\n\n const stroke = {\n weight,\n fill: Fills.color(equation.color, {\n opacity: clamp01(equation.opacity),\n }),\n // Round on both counts because this is a *curve*: a miter join on a\n // sharp turn throws a spike, and a butt cap leaves a visible flat end\n // wherever a run stops at a domain edge.\n cap: \"round\" as const,\n join: \"round\" as const,\n }\n\n const runs = this.runs.runs(\n equation.id,\n equation.sample,\n key,\n plane,\n clip,\n budget\n )\n for (const run of runs) {\n if (run.length < 2) continue\n ctx.draw(new Graphics2D().line({ points: run, radius: 0 }).stroke(stroke))\n }\n }\n }\n}\n\n// --- Mappers & tweens ------------------------------------------------------\n\n/** Mapper for {@link Graph2D.equations}. */\nfunction resolveEquations(\n input: Graph2DEquation[] | undefined\n): CurveResolved[] {\n if (!input) return []\n const out: CurveResolved[] = []\n for (const equation of input) {\n const sample = compileCurveCached(equation.expression)\n if (!sample) continue // unparseable or empty — drop it, keep the rest\n out.push({\n id: String(equation.id),\n sample,\n color: resolveColor(equation.color),\n opacity: equation.visible === false ? 0 : (equation.opacity ?? 1),\n })\n }\n return out\n}\n\n/**\n * Mapper for {@link Graph2D.grid}.\n *\n * Merges onto `previous` rather than onto the bare defaults, which is what makes\n * a partial write mean \"change this much and leave the rest\". It matters most on\n * a `to` command, whose target states only the fields that command touches: a\n * tween that dims the grid must not silently hand the axis numbers back to their\n * default size on the way.\n */\nfunction resolveGrid(\n input: Graph2DGrid | undefined,\n previous?: GridResolved\n): GridResolved {\n const base = previous ?? DEFAULT_GRID\n return {\n showGrid: input?.showGrid ?? base.showGrid,\n showMinorGrid: input?.showMinorGrid ?? base.showMinorGrid,\n showAxes: input?.showAxes ?? base.showAxes,\n showLabels: input?.showLabels ?? base.showLabels,\n colorGrid: colorOr(input?.colorGrid, base.colorGrid),\n colorMinorGrid: colorOr(input?.colorMinorGrid, base.colorMinorGrid),\n colorAxis: colorOr(input?.colorAxis, base.colorAxis),\n colorLabel: colorOr(input?.colorLabel, base.colorLabel),\n lineWidth: input?.lineWidth ?? base.lineWidth,\n axisWidth: input?.axisWidth ?? base.axisWidth,\n fontSize: input?.fontSize ?? base.fontSize,\n fontFamily: input?.fontFamily ?? base.fontFamily,\n }\n}\n\nfunction colorOr(\n input: Color | undefined,\n fallback: NormalizedColor\n): NormalizedColor {\n return input === undefined ? fallback : resolveColor(input)\n}\n\n/** Tween for {@link Graph2D.grid}. */\nfunction lerpGrid(\n from: GridResolved,\n to: GridResolved,\n t: number\n): GridResolved {\n return {\n showGrid: snapFlag(from.showGrid, to.showGrid, t),\n showMinorGrid: snapFlag(from.showMinorGrid, to.showMinorGrid, t),\n showAxes: snapFlag(from.showAxes, to.showAxes, t),\n showLabels: snapFlag(from.showLabels, to.showLabels, t),\n colorGrid: lerpColor(from.colorGrid, to.colorGrid, t),\n colorMinorGrid: lerpColor(from.colorMinorGrid, to.colorMinorGrid, t),\n colorAxis: lerpColor(from.colorAxis, to.colorAxis, t),\n colorLabel: lerpColor(from.colorLabel, to.colorLabel, t),\n lineWidth: lerpNumber(from.lineWidth, to.lineWidth, t),\n axisWidth: lerpNumber(from.axisWidth, to.axisWidth, t),\n fontSize: lerpNumber(from.fontSize, to.fontSize, t),\n // A face has no halfway point, so it changes once the tween lands.\n fontFamily: snapValue(from.fontFamily, to.fontFamily, t),\n }\n}\n\nfunction clamp01(value: number): number {\n return Number.isFinite(value) ? clamp(value, 0, 1) : 1\n}\n", "/**\n * What part of the plane a 2D graph is looking at, and the furniture that makes\n * it readable: the graph→pixel mapping, the tick ladder, and how a number on an\n * axis is written.\n *\n * ## The view is four numbers, and none of them is a matrix\n *\n * `centerX`/`centerY` say where you are, `xSpan`/`ySpan` say how much you can\n * see. Four independently tweenable numbers rather than a transform, for exactly\n * the reason the 3D graph's camera is three: the studio renders a *timeline*, so\n * every frame has to be reproducible from stored values under scrubbing and\n * export, and \"pan across while zooming in\" has to be one command rather than a\n * hand-built parallel over matrix entries.\n *\n * ## `ySpan: 0` means \"keep the grid square\"\n *\n * The vertical span is the one number that usually shouldn't be typed. A grapher\n * whose units are square is one where a circle is round and a 45° line looks\n * like one, and the span that achieves that depends on the node's *box* — which\n * the author changes by dragging a handle, not by editing this field. So zero\n * means \"derive it\", the same convention a node's own width and height use for\n * auto-sizing, and any positive value takes over.\n *\n * Nothing is lost by it: the moment a gesture or a command scales the axes\n * apart, the derived value is written down and the field is an ordinary number\n * from then on — the same handover the canvas makes when a resize drag turns a\n * filling axis into a fixed one.\n */\n\nimport { niceStep } from \"@motionscript/sdk/component\"\nimport type { PixelRect, PlaneMap } from \"./curve\"\n\n/** Where a 2D graph is looking, as the four numbers its props hold. */\nexport interface PlaneView {\n /** Graph x at the centre of the node's box. */\n centerX: number\n /** Graph y at the centre of the node's box. */\n centerY: number\n /** Width of the visible window, in graph units. */\n xSpan: number\n /** Height of the visible window in graph units, or 0 to keep units square. */\n ySpan: number\n}\n\n/**\n * Bounds on a span.\n *\n * A hundred decades inside the double's own range, so nothing downstream —\n * `width / span`, `step * scale`, `y * scale` — can overflow or fall into the\n * denormals on the way through.\n *\n * Deliberately far wider than anything usable, because what actually limits how\n * far a plane can be zoomed is not a floor on the span but a **ratio**:\n * `x - centerX` keeps about sixteen significant digits, so a window narrower\n * than `|centerX| · 4.3e-13` has fewer distinct representable x values across it\n * than it has pixels, and the curve staircases whatever these constants say.\n * That limit belongs to the *gesture* — see `planeSpanLimits` — which is the\n * thing a person can be stopped at. A scene that deliberately authored a deeper\n * zoom still renders, badly and visibly, rather than being silently clamped to\n * something it never asked for.\n */\nexport const MIN_SPAN = 1e-200\nexport const MAX_SPAN = 1e200\n\n/** A {@link PlaneView} resolved against a box: everything in concrete numbers. */\nexport interface ResolvedPlane extends PlaneMap {\n /** The box, in node-local pixels. */\n width: number\n height: number\n /** Visible graph range, low to high. */\n xMin: number\n xMax: number\n yMin: number\n yMax: number\n /** The vertical span actually in force — the derived one when `ySpan` is 0. */\n ySpan: number\n}\n\n/**\n * Resolves a view against the node's box: what a graph unit is worth in pixels,\n * and which part of the plane that puts on screen.\n *\n * The single place `ySpan: 0` is turned into a number, so nothing downstream —\n * the grid, the curves, the gesture — has to know the convention exists.\n */\nexport function resolvePlane(\n view: PlaneView,\n width: number,\n height: number\n): ResolvedPlane {\n const w = Math.max(1, width)\n const h = Math.max(1, height)\n const xSpan = clampSpan(view.xSpan)\n const scaleX = w / xSpan\n // Square units: a graph unit is the same number of pixels either way, so the\n // visible height is however many of them fit in the box.\n const ySpan = view.ySpan > 0 ? clampSpan(view.ySpan) : h / scaleX\n const scaleY = h / ySpan\n\n return {\n centerX: view.centerX,\n centerY: view.centerY,\n scaleX,\n scaleY,\n width: w,\n height: h,\n xMin: view.centerX - xSpan / 2,\n xMax: view.centerX + xSpan / 2,\n yMin: view.centerY - ySpan / 2,\n yMax: view.centerY + ySpan / 2,\n ySpan,\n }\n}\n\n/** Holds a span inside the range a double can still draw a grid over. */\nexport function clampSpan(span: number): number {\n if (!Number.isFinite(span) || span <= 0) return MIN_SPAN\n return Math.min(MAX_SPAN, Math.max(MIN_SPAN, span))\n}\n\n/** The node's box as a pixel rectangle, y-up and centred on the origin. */\nexport function planeBox(plane: ResolvedPlane): PixelRect {\n return {\n left: -plane.width / 2,\n right: plane.width / 2,\n bottom: -plane.height / 2,\n top: plane.height / 2,\n }\n}\n\n/** Grows a rectangle by `margin` on every side. */\nexport function inflate(rect: PixelRect, margin: number): PixelRect {\n return {\n left: rect.left - margin,\n right: rect.right + margin,\n bottom: rect.bottom - margin,\n top: rect.top + margin,\n }\n}\n\n/** Graph x → node-local pixels. */\nexport function toPx(plane: PlaneMap, x: number): number {\n return (x - plane.centerX) * plane.scaleX\n}\n\n/** Graph y → node-local pixels, y-up. */\nexport function toPy(plane: PlaneMap, y: number): number {\n return (y - plane.centerY) * plane.scaleY\n}\n\n// --- Moving the view -------------------------------------------------------\n\n/** Slides the view by a delta in **graph units**. Spans are untouched. */\nexport function panPlane(\n view: PlaneView,\n dx: number,\n dy: number\n): PlaneView {\n return {\n ...view,\n centerX: view.centerX + dx,\n centerY: view.centerY + dy,\n }\n}\n\n/**\n * Scales the view about a fixed point in **graph units** — the gesture every map\n * makes: whatever was under the cursor stays under the cursor.\n *\n * `factorX`/`factorY` are how much bigger the *window* gets, so a factor above 1\n * zooms out. Passing the same factor twice keeps a square grid square, and is\n * the only case that leaves an auto {@link PlaneView.ySpan} auto: scaling one\n * axis on its own is the statement \"this axis is mine now\", so the derived span\n * is written down and stops following the box.\n */\nexport function zoomPlane(\n view: PlaneView,\n plane: ResolvedPlane,\n factorX: number,\n factorY: number,\n anchorX: number,\n anchorY: number\n): PlaneView {\n const uniform = factorX === factorY && view.ySpan <= 0\n const xSpan = clampSpan(plane.xMax - plane.xMin) * factorX\n const ySpan = plane.ySpan * factorY\n return {\n // The anchor keeps its distance from each edge as a *fraction* of the span,\n // which is what \"stays under the cursor\" means once the span has changed.\n //\n // Written as a *displacement from the current centre* rather than as\n // `anchor + (centre - anchor) · factor`, which is the same expression\n // rearranged and much better conditioned. The anchor is up to half a span\n // away from the centre, so at a wide view the direct form subtracts two\n // enormous numbers to recover a small one and loses it: at a span of 1e200 a\n // centre of 4 comes back as 0, and at a stop — where the factor is exactly 1\n // and the centre must not move at all — it came back as 0 rather than as 4.\n // This form multiplies the big quantity by `1 - factor`, which is what is\n // actually small, and is bit-exact when the wheel is doing nothing.\n centerX: view.centerX + (anchorX - view.centerX) * (1 - factorX),\n centerY: view.centerY + (anchorY - view.centerY) * (1 - factorY),\n xSpan: clampSpan(xSpan),\n ySpan: uniform ? 0 : clampSpan(ySpan),\n }\n}\n\n// --- Ticks -----------------------------------------------------------------\n\n/** One axis's tick ladder: where the numbers go and how finely to rule between. */\nexport interface TickLadder {\n /** Distance between labelled ticks, in graph units. */\n step: number\n /** Distance between the faint lines between them. */\n minorStep: number\n /**\n * Faint lines inside one labelled step — `step / minorStep`, as a whole\n * number.\n *\n * The faint lines have no list of their own any more: they are generated by\n * the plane's shader from a pitch and a phase, so what the ladder has to hand\n * over is how many of them fit rather than where each one is. Building and\n * filtering a few hundred numbers per axis per frame for a paint that never\n * reads them was most of what the old ladder cost.\n */\n divisions: number\n /** Labelled positions across the visible range, ascending. */\n major: number[]\n}\n\n/**\n * Cap on how many *numbers* one axis may carry.\n *\n * The step is chosen from how close two labels may sit, so this is unreachable\n * in normal use — a 1920-px axis at the tightest legal pitch holds about\n * seventeen. It is there because `xSpan` is an animatable number and a tween\n * passing through an absurd value must not try to shape a thousand strings for\n * one frame. Half what it was, because it no longer bounds the grid: the shader\n * rules the plane at O(1) however far it is zoomed.\n */\nconst MAX_TICKS = 200\n\n/**\n * Largest tick index the ladder will build from.\n *\n * Past `2^53` an integer is no longer exactly representable, so `first + i`\n * repeats values and `(first + i) * step` produces garbage. At that view the\n * ticks genuinely have no distinct values, and an empty ladder says so.\n */\nconst MAX_INDEX = Number.MAX_SAFE_INTEGER\n\n/**\n * The decade a positive number sits in: `1` for 42, `-2` for 0.03.\n *\n * The nudge is not cosmetic. `Math.log10(1e-7)` is `-7.000000000000001` in V8,\n * so a bare `floor` answers -8 for an exact power of ten — which put the leading\n * digit of a 1e-7 step at 10 and handed {@link minorDivisions} the wrong answer\n * on several perfectly ordinary zooms.\n */\nfunction decadeOf(value: number): number {\n return Math.floor(Math.log10(value) + 1e-12)\n}\n\n/**\n * The tick ladder for a range, at a scale, given the closest two labels may sit.\n *\n * Steps climb the 1–2–5 ladder, which is the one every plotting tool uses\n * because those are the numbers people can subdivide in their heads. Minor lines\n * subdivide a step into 5 — or into 4 when the step is a 2, so that the faint\n * lines land on halves rather than on fifths of a two.\n */\nexport function tickLadder(\n min: number,\n max: number,\n pxPerUnit: number,\n minSpacingPx: number\n): TickLadder {\n const span = max - min\n if (!(span > 0) || !(pxPerUnit > 0)) {\n return { step: 1, minorStep: 1, divisions: 1, major: [] }\n }\n\n const step = niceStep(Math.max(minSpacingPx, 1) / pxPerUnit)\n const divisions = minorDivisions(step)\n\n return {\n step,\n minorStep: step / divisions,\n divisions,\n major: multiplesWithin(min, max, step),\n }\n}\n\n/**\n * A digit's advance in the built-in face, as a fraction of the font size.\n *\n * Approximate on purpose: the node draws its numbers through the renderer's\n * shaper and this arithmetic runs in `scene-core`, which has no font metrics and\n * shouldn't grow any. It only has to be close enough to decide whether two\n * labels would collide, and it errs wide.\n */\nconst DIGIT_EM = 0.58\n/** Blank either side of a number, in font sizes. */\nconst LABEL_MARGIN = 0.8\n/**\n * Room an x-axis number needs before the ladder steps up, as multiples of its\n * font size — the pitch {@link labelLadder} widens *from*.\n *\n * Wider than the five characters it nominally buys, and left that way: it is\n * what every graph anywhere near the origin has always been ruled at, and\n * tightening it here would re-space every existing scene to fix a problem only\n * deep zoom has.\n */\nconst BASE_LABEL_PITCH = 5\n\n/**\n * A ladder whose numbers are guaranteed not to overlap, however long they are.\n *\n * The base pitch assumes a label of about five characters, which is what an\n * axis anywhere near the origin actually carries. Zoom to `x = 1.23456789` at a\n * span of 1e-6 and every number is fourteen, so the ladder is rebuilt once at a\n * pitch wide enough to hold the widest one it just produced. One extra pass and\n * not a loop: the second ladder is coarser than the first, so its labels are no\n * longer, and a third pass could only ever agree with the second.\n */\nexport function labelLadder(\n min: number,\n max: number,\n pxPerUnit: number,\n fontSize: number\n): TickLadder {\n const base = fontSize * BASE_LABEL_PITCH\n const ladder = tickLadder(min, max, pxPerUnit, base)\n const needed = fontSize * labelWidthEm(ladder)\n return needed > base ? tickLadder(min, max, pxPerUnit, needed) : ladder\n}\n\n/**\n * Room the widest number on `ladder` needs beside its neighbour, in font sizes.\n *\n * Also what the node aligns its numbers within — a right-aligned `-1200` and a\n * right-aligned `5` end in the same place only if the box holds the longer of\n * the two.\n */\nexport function labelWidthEm(ladder: TickLadder): number {\n return DIGIT_EM * widestLabel(ladder) + LABEL_MARGIN\n}\n\n/**\n * How wide a number on this ladder can be, in characters.\n *\n * The longest is not always at an extreme — `-0.5` is longer than `10` — so this\n * scans, which costs at most {@link MAX_TICKS} formats and is a rounding error\n * beside the labels the node is about to shape anyway.\n */\nexport function widestLabel(ladder: TickLadder): number {\n let widest = 0\n for (const value of ladder.major) {\n const length = formatTick(value, ladder.step).length\n if (length > widest) widest = length\n }\n return widest\n}\n\n/**\n * How many faint lines a step is divided into. A 1 or a 5 takes five, a 2 takes\n * four — see {@link tickLadder}.\n */\nfunction minorDivisions(step: number): number {\n const magnitude = Math.pow(10, decadeOf(step))\n const leading = Math.round(step / magnitude)\n return leading === 2 ? 4 : 5\n}\n\n/**\n * Every multiple of `step` within `[min, max]`, ascending.\n *\n * Built from an integer index times the step rather than by accumulation, so the\n * hundredth tick is still exactly a hundred steps out — repeated addition drifts\n * enough to put a `0.30000000000000004` on an axis.\n */\nfunction multiplesWithin(min: number, max: number, step: number): number[] {\n // The nudge absorbs the case where a bound *is* a multiple and lands a hair\n // outside it after the division, which would drop the tick on the edge.\n const first = Math.ceil(min / step - 1e-9)\n const last = Math.floor(max / step + 1e-9)\n if (!Number.isFinite(first) || !Number.isFinite(last)) return []\n if (Math.abs(first) > MAX_INDEX || Math.abs(last) > MAX_INDEX) return []\n\n const count = last - first + 1\n if (count <= 0 || count > MAX_TICKS) return []\n\n const out: number[] = new Array(count)\n for (let i = 0; i < count; i++) out[i] = (first + i) * step\n return out\n}\n\n/**\n * Largest number of digits worth writing out in full, before the exponent is the\n * shorter answer.\n */\nconst MAX_DIGITS = 14\n/** `toFixed` and `toExponential` both refuse an argument past 100. */\nconst MAX_FRACTION = 100\n\n/**\n * A tick value as it is written on the axis.\n *\n * The precision comes from the **step**, not from the value: what a number on an\n * axis has to do is tell you which tick it is, so a ladder of 0.2s reads\n * `0.2 0.4 0.6` and the same values on a ladder of 1s would be rounded away\n * rather than shown as `0.2` next to `1`.\n *\n * That is also the rule for choosing the exponent, and the reason the two fixed\n * magnitude thresholds this used to carry are gone. `1e6` as a ceiling is wrong\n * about *both* directions at once: pan to `x = 1e7` at the default zoom and a\n * whole axis of distinct ticks reads `1e7`, `1e7`, `1e7`; while at a span of\n * 1e-9 around `x = 1` no threshold on the magnitude helps at all, because the\n * values are all near 1 and it is the *step* that has run out of room. What\n * decides it is how many significant digits it takes to tell two neighbouring\n * ticks apart: the exponent is only shorter when that count is small and the\n * number is far from 1.\n */\nexport function formatTick(value: number, step: number): string {\n if (value === 0) return \"0\"\n if (!Number.isFinite(value)) return \"\"\n if (!(step > 0) || !Number.isFinite(step)) return String(value)\n\n const decade = decadeOf(Math.abs(value))\n // At least one: a value smaller than its own step isn't a tick of this ladder,\n // but it is still a number somebody may ask this to write.\n const digits = Math.max(1, decade - decadeOf(step) + 1)\n\n // Far from 1 and cheap to write in scientific form — `2e7`, `1.5e-9`.\n if ((decade >= 6 || decade <= -5) && digits <= 6) {\n return trimExponent(value.toExponential(digits - 1))\n }\n\n if (digits <= MAX_DIGITS) {\n const decimals = Math.min(MAX_FRACTION, Math.max(0, -decadeOf(step)))\n const text = value.toFixed(decimals)\n // `toFixed` keeps the zeros a nice step can't produce (0.50 for a 0.1 ladder\n // is only ever written that way by the formatter).\n return decimals > 0 ? text.replace(/\\.?0+$/, \"\") : text\n }\n\n // More digits than anybody can read off an axis. The exponent at least keeps\n // the ticks distinguishable from one another.\n const precision = Math.min(17, Math.max(0, digits - 1))\n return trimExponent(value.toExponential(precision))\n}\n\n/** `1.20e-7` → `1.2e-7`, and `1.00e+21` → `1e21`. */\nfunction trimExponent(text: string): string {\n const [mantissa, exponent] = text.split(\"e\")\n const trimmed = mantissa.replace(/\\.?0+$/, \"\")\n return `${trimmed}e${Number(exponent)}`\n}\n", "/**\n * The ruled plane as a **paint** rather than as geometry.\n *\n * The grid used to be one filled rect per line — up to four hundred per axis per\n * order, chained into one `Graphics` so at least the *draws* stayed constant.\n * That is the arrangement `ChartCanvas` makes, and for a chart it is right: a\n * chart's axes are ruled once and then sit still. A grapher's are not. Every\n * rect moves on every frame of a pan, so the renderer's shape cache never hits;\n * worse, the rect *count* changes as the ladder steps, and the cache is keyed by\n * shape index, so a grid that gained a line shifted the key of every curve drawn\n * after it and threw those away too.\n *\n * A shader has none of that. The plane is one shape whatever the zoom, so the\n * curves' cache keys are stable, the wasm path behind it is built once, and the\n * cost of ruling the plane stops depending on how much of it is ruled — which is\n * the whole reason the old {@link MAX_TICKS} cap existed.\n *\n * ## Why not the built-in `grid` fill\n *\n * `Fills.grid` measures its pitch in *divisions of the shape* — deliberately, so\n * a grid of eight stays a grid of eight when the shape resizes. A graph's pitch\n * is decided by the tick ladder and is a different number of pixels on each\n * axis, which one scalar `divisions` cannot say; and it carries one colour,\n * where a graph has two orders in two colours plus its axes.\n *\n * ## Everything variable is a uniform\n *\n * `getOrCompileSkSL` caches by *source string* and never evicts, so a source\n * built per frame is a new program per frame. {@link PLANE_SOURCE} is therefore a\n * module constant and every switch — the two orders, the axes, the paper — is\n * expressed by a uniform that can go to zero.\n *\n * ## The phase is the whole trick\n *\n * Uniforms reach the shader as **fp32**. Handing it \"where x = 0 is\" would be\n * fine at the default zoom and 1e15 pixels at a deep one off the origin, which\n * is not a number fp32 holds at all. So {@link planeUniforms} does the reduction\n * in double precision on the way in: it splits the centre into a whole number of\n * steps plus a remainder *before* anything is multiplied by the scale, and hands\n * over a phase inside a single cell. Every number the shader sees is bounded by\n * the node's own box.\n */\n\nimport type { NormalizedColor } from \"@motionscript/sdk\"\n\nimport type { ResolvedPlane, TickLadder } from \"./plane\"\n\n/**\n * The plane's paint program.\n *\n * Coverage-based rather than a hard test, and the ramp is one *device* pixel\n * wide (`1 / u_scale`, the idiom every built-in pattern fill uses), so a ruling\n * is a crisp hairline whether the camera is zoomed in on the node or the scene\n * is being exported at four times scale.\n *\n * Uniforms are declared widest-first out of habit rather than necessity — a\n * shader fill's uniforms are bound by *name* through reflection, unlike the\n * built-in fills, whose flat positional array makes declaration order load\n * bearing. `u_origin` and `u_scale` are supplied by the renderer.\n */\nexport const PLANE_SOURCE = `\nuniform vec4 u_paper;\nuniform vec4 u_minorColor;\nuniform vec4 u_majorColor;\nuniform vec4 u_axisColor;\nuniform vec2 u_origin;\nuniform vec2 u_pitch;\nuniform vec2 u_phase;\nuniform vec2 u_divisions;\nuniform vec2 u_axisAt;\nuniform float u_majorHalf;\nuniform float u_minorHalf;\nuniform float u_axisHalf;\nuniform float u_scale;\n\n// Coverage of a line \\`d\\` px away, of half-width \\`w\\`, ramped over \\`feather\\`.\n//\n// The zero test is not a shortcut. The ramp is centred on the line, so a line of\n// no width still lands half a pixel of coverage on the pixels it runs through —\n// an order you switched off, drawn faintly. Answering it here rather than in the\n// ramp is what lets one compiled program serve showGrid, showMinorGrid, showAxes\n// and a grid width of zero.\nfloat ink(float d, float w, float feather) {\n if (w <= 0.0) return 0.0;\n return clamp((w - d) / feather + 0.5, 0.0, 1.0);\n}\n\n// Distance from \\`t\\` to the nearest ruling. SkSL's mod is floor-based, so this\n// is right on either side of the phase without a sign test.\nfloat toRuling(float t, float pitch, float phase) {\n float m = mod(t - phase, pitch);\n return min(m, pitch - m);\n}\n\n// A straight colour under a coverage, premultiplied.\nvec4 lay(vec4 color, float cover) {\n float a = color.a * cover;\n return vec4(color.rgb * a, a);\n}\n\n// \\`src\\` over \\`dst\\`, both premultiplied.\nvec4 stack(vec4 src, vec4 dst) {\n return src + dst * (1.0 - src.a);\n}\n\nvec4 main(vec2 fragCoord) {\n vec2 p = fragCoord - u_origin;\n float feather = 1.0 / max(u_scale, 0.0001);\n\n // Floored well above zero: a mod by zero paints garbage rather than nothing.\n vec2 major = max(u_pitch, vec2(0.01));\n vec2 minor = major / max(u_divisions, vec2(1.0));\n\n vec2 dMajor = vec2(toRuling(p.x, major.x, u_phase.x),\n toRuling(p.y, major.y, u_phase.y));\n vec2 dMinor = vec2(toRuling(p.x, minor.x, u_phase.x),\n toRuling(p.y, minor.y, u_phase.y));\n\n float majorCover = max(ink(dMajor.x, u_majorHalf, feather),\n ink(dMajor.y, u_majorHalf, feather));\n\n // Every numbered ruling is also a faint one — the minor pitch divides the\n // major exactly — so the faint order is cut away inside the numbered one's\n // whole footprint, its ramp included. Drawing both would leave a hairline of\n // the faint colour either side of every numbered line, where the numbered\n // one's own ramp has not yet reached full coverage. The old ladder dropped\n // coincident minors from a list; a shader has no list, so it subtracts.\n float minorCover = max(\n ink(dMinor.x, u_minorHalf, feather)\n * (1.0 - ink(dMajor.x, u_majorHalf + feather, feather)),\n ink(dMinor.y, u_minorHalf, feather)\n * (1.0 - ink(dMajor.y, u_majorHalf + feather, feather)));\n\n float axisCover = max(ink(abs(p.x - u_axisAt.x), u_axisHalf, feather),\n ink(abs(p.y - u_axisAt.y), u_axisHalf, feather));\n\n vec4 c = lay(u_paper, 1.0);\n c = stack(lay(u_minorColor, minorCover), c);\n c = stack(lay(u_majorColor, majorCover), c);\n c = stack(lay(u_axisColor, axisCover), c);\n return c;\n}\n`\n\n/** What {@link planeUniforms} needs to know about how the plane is styled. */\nexport interface PlaneStyle {\n /** The paper's colour. */\n paper: NormalizedColor\n /** How opaque the paper is, 0–1. At 0 the graph draws over what is behind it. */\n paperOpacity: number\n showGrid: boolean\n showMinorGrid: boolean\n showAxes: boolean\n colorGrid: NormalizedColor\n colorMinorGrid: NormalizedColor\n colorAxis: NormalizedColor\n /** Width of a numbered ruling. The faint ones are drawn at two thirds of it. */\n lineWidth: number\n /** Width of the two lines through the origin. */\n axisWidth: number\n}\n\n/**\n * The uniform record {@link PLANE_SOURCE} is fed.\n *\n * A type alias rather than an interface so it satisfies the renderer's\n * `Record<string, number | number[]>` without a cast — interfaces have no\n * implicit index signature.\n */\nexport type PlaneUniforms = {\n u_paper: number[]\n u_minorColor: number[]\n u_majorColor: number[]\n u_axisColor: number[]\n u_pitch: number[]\n u_phase: number[]\n u_divisions: number[]\n u_axisAt: number[]\n u_majorHalf: number\n u_minorHalf: number\n u_axisHalf: number\n}\n\n/**\n * Where an axis is parked when it is not on screen, in box pixels.\n *\n * Finite on purpose: an infinity here would reach the coverage ramp as a `NaN`,\n * and a `NaN` compares false against every clamp, which paints the axis colour\n * over the entire plane rather than nowhere on it.\n */\nconst AXIS_OFF = -1e7\n\n/** Two thirds the width of a numbered ruling — the faint order's weight. */\nconst MINOR_WEIGHT = 2 / 3\n\n/** Past this an integer index is no longer exact, so the reduction is meaningless. */\nconst MAX_INDEX = Number.MAX_SAFE_INTEGER\n\n/**\n * Whether the plane has anything to paint at all — no paper, no rulings and no\n * axes is a draw the node can skip outright.\n */\nexport function planeVisible(style: PlaneStyle): boolean {\n if (style.paper[3] * clamp01(style.paperOpacity) > 0.001) return true\n if (style.showGrid && style.lineWidth > 0) return true\n return style.showAxes && style.axisWidth > 0\n}\n\n/**\n * The uniforms that put {@link PLANE_SOURCE} over a resolved plane.\n *\n * Everything handed over is bounded by the node's box — a pitch of tens of\n * pixels, a phase inside one cell, an axis position inside the frame or parked\n * at {@link AXIS_OFF}. That is deliberate and it is what makes the deep zoom\n * work: see the module note.\n */\nexport function planeUniforms(\n plane: ResolvedPlane,\n style: PlaneStyle,\n xTicks: TickLadder,\n yTicks: TickLadder\n): PlaneUniforms {\n const ruled = style.showGrid && style.lineWidth > 0\n const majorHalf = ruled ? style.lineWidth / 2 : 0\n\n return {\n u_paper: withAlpha(style.paper, clamp01(style.paperOpacity)),\n u_minorColor: rgba(style.colorMinorGrid),\n u_majorColor: rgba(style.colorGrid),\n u_axisColor: rgba(style.colorAxis),\n u_pitch: [\n pitchOf(xTicks.step, plane.scaleX),\n pitchOf(yTicks.step, plane.scaleY),\n ],\n // The shader's y runs down the box and the graph's runs up it, which is the\n // whole of the difference between these two: one signed scale each.\n u_phase: [\n phaseOf(plane.centerX, xTicks.step, plane.width / 2, -plane.scaleX),\n phaseOf(plane.centerY, yTicks.step, plane.height / 2, plane.scaleY),\n ],\n u_divisions: [\n Math.max(1, Math.round(xTicks.divisions)),\n Math.max(1, Math.round(yTicks.divisions)),\n ],\n u_axisAt: [\n // Only ever evaluated when the origin is inside the range, which bounds\n // `centre × scale` by half the box — the same reduction `phaseOf` makes,\n // for free.\n style.showAxes && plane.xMin <= 0 && 0 <= plane.xMax\n ? plane.width / 2 - plane.centerX * plane.scaleX\n : AXIS_OFF,\n style.showAxes && plane.yMin <= 0 && 0 <= plane.yMax\n ? plane.height / 2 + plane.centerY * plane.scaleY\n : AXIS_OFF,\n ],\n u_majorHalf: majorHalf,\n u_minorHalf: ruled && style.showMinorGrid ? majorHalf * MINOR_WEIGHT : 0,\n u_axisHalf: style.showAxes ? Math.max(0, style.axisWidth) / 2 : 0,\n }\n}\n\n/** A ladder step in pixels, floored where the shader floors it. */\nfunction pitchOf(step: number, scale: number): number {\n const pitch = Math.abs(step * scale)\n return Number.isFinite(pitch) && pitch > 0.01 ? pitch : 0.01\n}\n\n/**\n * Where the first ruling sits inside the box, in pixels from its leading edge —\n * a number in `[0, pitch)`.\n *\n * The centre is split into a whole number of steps plus a remainder *before* it\n * is ever multiplied by the scale, so the only quantity that ever carries the\n * centre's own magnitude is an integer count that then cancels. Subtracting a\n * tick value from the centre directly is exact at a span of 20 and has no\n * significant digits left at 1e-9 around x = 1e6 — where the lines would still\n * be drawn, just not evenly spaced.\n *\n * `signedScale` is negative for x and positive for y; see {@link planeUniforms}.\n */\nfunction phaseOf(\n center: number,\n step: number,\n halfBox: number,\n signedScale: number\n): number {\n const pitch = pitchOf(step, signedScale)\n const index = Math.round(center / step)\n if (!Number.isFinite(index) || Math.abs(index) > MAX_INDEX) return 0\n\n const remainder = center - index * step\n const anchor = halfBox + remainder * signedScale\n if (!Number.isFinite(anchor)) return 0\n return anchor - Math.floor(anchor / pitch) * pitch\n}\n\nfunction rgba(color: NormalizedColor): number[] {\n return [color[0], color[1], color[2], color[3]]\n}\n\nfunction withAlpha(color: NormalizedColor, opacity: number): number[] {\n return [color[0], color[1], color[2], color[3] * opacity]\n}\n\nfunction clamp01(value: number): number {\n if (!Number.isFinite(value)) return 1\n return value < 0 ? 0 : value > 1 ? 1 : value\n}\n", "/**\n * The mappers and tweens the {@link Graph2D} node is built from — the same shape\n * `graph3d/impl/shared.ts` has, and mostly the same short list, because a\n * `@property` is only as good as the `mapper`/`tween` it is declared with.\n *\n * Almost everything here is a re-export. That is the point: the match-by-id\n * equation tween lives in `nodes/graph-kit` because both graphs animate a list of\n * equations the same way, and colour normalisation lives in `nodes/view3d-kit`\n * because the 3D nodes needed it first. What is genuinely this node's own is one\n * function: turning a two-argument compiled expression into the one-argument\n * curve the sampler asks for, without losing the identity the tween compares.\n */\n\nimport {\n CURVE_VARIABLES,\n compileExpressionCached,\n lerpGraphEquations,\n type GraphEquationResolved,\n} from \"../kit\"\nimport type { CurveFunction } from \"./curve\"\n\n/**\n * Colour normalisation and its tween, plus the two number tweens that fix what a\n * plain lerp gets wrong.\n *\n * They live in `view3d-kit` because the 3D nodes needed them first, and there is\n * nothing three-dimensional about any of them — a `NormalizedColor` is an RGBA\n * tuple, which is both interpolatable and still a valid `Color` to hand back to\n * `Graphics`. Renamed on the way through so the call sites don't read as though\n * this node draws in perspective.\n */\nexport {\n resolveColor3D as resolveColor,\n lerpColor3D as lerpColor,\n lerpCount,\n snapFlag,\n snapValue,\n} from \"@motionscript/sdk/component\"\n\n/**\n * One curve after {@link Graph2D}'s mapper has run: compiled, resolved and fully\n * defaulted, so the per-frame draw never re-derives anything and the tween never\n * tests for an absent field.\n */\nexport type CurveResolved = GraphEquationResolved<CurveFunction>\n\n/**\n * Compiles `source` as `y = f(x)`, memoised, returning `null` for anything that\n * doesn't parse.\n *\n * Two caches deep and both earn their place. The inner one memoises the *parse*,\n * which is what keeps a node that re-draws sixty times a second from re-reading\n * its own expressions; this one memoises the **adapter**, and without it every\n * write would produce a fresh closure over the same parse — which the equation\n * tween reads as \"this expression changed\" and would blend a function into\n * itself, at double the sampling cost, for no visible difference.\n *\n * The adapter exists because arity is fixed in the parser for the sake of the\n * hot path (see `CompiledExpression`) while the sampler quite reasonably wants\n * to call `f(x)`. The unused second argument is 0 and unreachable: a curve is\n * compiled with `x` as its only variable, so nothing in the tree reads it.\n */\nconst curves = new Map<string, CurveFunction | null>()\n\nexport function compileCurveCached(source: string): CurveFunction | null {\n const trimmed = source.trim()\n if (trimmed === \"\") return null\n\n const hit = curves.get(trimmed)\n if (hit !== undefined) return hit\n\n const compiled = compileExpressionCached(trimmed, CURVE_VARIABLES)\n const curve: CurveFunction | null =\n compiled === null ? null : (x: number) => compiled(x, 0)\n curves.set(trimmed, curve)\n return curve\n}\n\n/**\n * Tween for the curve list — the shared match-by-id walk, with the one thing a\n * curve does differently: mid-morph its height is sampled from both functions\n * and blended, so it deforms into the new one rather than cross-fading through\n * a frame where both are drawn.\n */\nexport function lerpCurves(\n from: CurveResolved[],\n to: CurveResolved[],\n t: number\n): CurveResolved[] {\n return lerpGraphEquations(from, to, t, blendCurve)\n}\n\n/** How two curves blend mid-tween. */\nfunction blendCurve(\n a: CurveFunction,\n b: CurveFunction,\n t: number\n): CurveFunction {\n return (x) => {\n const start = a(x)\n return start + (b(x) - start) * t\n }\n}\n", "/**\n * The `z = f(x, y)` expression language, as this node reaches for it.\n *\n * The parser itself now lives in `nodes/graph-kit/expression`, because the 2D\n * graph writes its curves in the same language and neither node owns it — the\n * same move `shared.ts` made when the Protein node needed the colour and camera\n * helpers. What is left here is the *binding*: a surface's two free variables\n * are `x` and `y`, which is the one thing about the grammar that differs between\n * the two graphs, and every function below has it applied.\n *\n * Re-exported rather than re-implemented so this stays the one import a graph3d\n * module reaches for, and so the vocabulary the inspector lists cannot drift\n * from the names the parser will actually accept.\n */\n\nimport {\n SURFACE_VARIABLES,\n compileExpression as compile,\n compileExpressionCached as compileCached,\n expressionError as errorOf,\n type CompiledExpression,\n} from \"../kit/expression\"\n\nexport { EXPRESSION_VOCABULARY } from \"../kit/expression\"\nexport type {\n CompiledExpression,\n ExpressionVocabulary,\n} from \"../kit/expression\"\n\n/** Compiles `source` as `z = f(x, y)`. Throws on a syntax error. */\nexport function compileExpression(source: string): CompiledExpression {\n return compile(source, SURFACE_VARIABLES)\n}\n\n/** Why `source` won't compile as `z = f(x, y)`, or `null` when it does. */\nexport function expressionError(source: string): string | null {\n return errorOf(source, SURFACE_VARIABLES)\n}\n\n/**\n * Compiles with memoisation, returning `null` for anything that doesn't parse.\n *\n * The memoisation is what makes the equation tween cheap — equal source compiles\n * to the identical closure, so \"did this equation change\" is an `===` — and what\n * `surfaceRevision` reduces to a number. See the shared module for the rest.\n */\nexport function compileExpressionCached(\n source: string\n): CompiledExpression | null {\n return compileCached(source, SURFACE_VARIABLES)\n}\n", "import {\n command,\n easeInOut,\n node,\n type Command,\n type CommandArgs,\n Canvas3D,\n Fills,\n Geo,\n Graphics3D,\n Mat,\n Scene3D,\n lerpNumber,\n property,\n type Color,\n type NodeConfig,\n type NormalizedColor,\n type Canvas3DProps,\n type Vector3Input,\n} from \"@motionscript/sdk\"\nimport { orbitProps, type OrbitTarget } from \"@motionscript/sdk/component\"\n\nimport { compileExpressionCached } from \"./expression\"\nimport {\n lerpColor3D,\n lerpCount,\n lerpEquations,\n lerpFinite,\n cameraOrbit,\n resolveColor3D,\n sanitizeHeight,\n snapFlag,\n surfaceRevision,\n type EquationResolved,\n} from \"./shared\"\n\n// --- Authored props --------------------------------------------------------\n// The loose shapes a caller writes. Each has a matching `*Resolved` type below:\n// what the `@property` mapper turns it into and what the tween interpolates.\n\n/** One plotted surface, `z = f(x, y)`. */\nexport interface Graph3DEquation {\n /** Identity across list changes — see {@link Graph3D.equations}. */\n id: string | number\n /** The expression, evaluated as `z = f(x, y)` (e.g. `sin(x) + cos(y)`). */\n expression: string\n /** The mesh's colour: any {@link Color} — a CSS string or an RGBA tuple. */\n color: Color\n /** Whether the surface is drawn. Defaults to true. */\n visible?: boolean\n /**\n * How solid the mesh is, 0–1. Defaults to 1.\n *\n * Anything below 1 makes the surface blend rather than write depth, so\n * overlapping surfaces layer instead of occluding each other. Left at 1 the\n * surface is genuinely opaque and reads crisp — which is why that is the\n * default, and why fading one in or out still looks right on the way through.\n */\n opacity?: number\n}\n\n/**\n * Where the camera may go.\n *\n * Deliberately *not* OrbitControls. Those drive a camera from live mouse input,\n * and a rendered timeline has no pointer — worse, their damping is an\n * accumulator, so a scrubbed frame would differ from a played one and export\n * would stop being deterministic. What is honoured here is the framing\n * ({@link initialPosition}) and the limits; to *move* the camera, animate\n * {@link Graph3DProps.orbit} / {@link Graph3DProps.elevation} /\n * {@link Graph3DProps.zoom} from the timeline, which stays seekable.\n */\nexport interface Graph3DCamera {\n /** Initial camera position, which seeds the three spherical props. */\n initialPosition?: { x: number; y: number; z: number }\n /** Closest the camera may come to the origin. */\n minZoom?: number\n /** Furthest it may pull back. */\n maxZoom?: number\n}\n\n/** The helper grid and the axes drawn under the surfaces. */\nexport interface Graph3DGrid {\n /** Whether to draw the ground grid at all. Defaults to true. */\n showGrid?: boolean\n /** Total width and depth of the grid. Defaults to 20. */\n size?: number\n /** How many cells across. Defaults to 20. */\n divisions?: number\n /** Colour of the two lines through the origin. */\n colorCenterLine?: Color\n /** Colour of every other grid line. */\n colorGrid?: Color\n /** Whether to draw the X/Y/Z axes. Defaults to true. */\n showAxes?: boolean\n /** How far each axis reaches from the origin. Defaults to 10. */\n axesSize?: number\n}\n\nexport interface Graph3DProps extends Canvas3DProps {\n /** The surfaces to plot, each evaluated as `z = f(x, y)` over the domain. */\n equations: Graph3DEquation[]\n camera: Graph3DCamera\n grid: Graph3DGrid\n /** Half-width of the plotted domain: x and y run `-domain … +domain`. Default 10. */\n domain: number\n /**\n * Surface resolution per axis. Default 80, i.e. ~6.4k vertices per equation.\n * Lower it when plotting several surfaces at once.\n */\n segments: number\n /**\n * Clamp on `|z|`. Default 20. Without it a function with an asymptote (`1/x`,\n * `tan`) produces near-infinite vertices that stretch the mesh across the\n * whole scene and wreck the framing.\n */\n maxHeight: number\n /** Camera orbit about the vertical axis, in **degrees**. Animate this to spin. */\n orbit: number\n /** Camera elevation above the ground plane, in **degrees**. */\n elevation: number\n /** Camera distance from the origin, clamped by the camera's zoom limits. */\n zoom: number\n /** The 3D scene's background. */\n background: Color\n /** Skips the background entirely, letting the 2D scene behind show through. */\n transparentBackground: boolean\n}\n\n// --- Resolved props --------------------------------------------------------\n// Canonical, fully defaulted, interpolatable. Nothing here is optional, so the\n// builder never re-derives a default and the tweens never test for one.\n\n/** {@link Graph3DCamera} after {@link resolveCamera}. */\ninterface CameraResolved {\n initialPosition: { x: number; y: number; z: number }\n minZoom: number\n maxZoom: number\n}\n\n/** {@link Graph3DGrid} after {@link resolveGrid}. */\ninterface GridResolved {\n showGrid: boolean\n size: number\n divisions: number\n colorCenterLine: NormalizedColor\n colorGrid: NormalizedColor\n showAxes: boolean\n axesSize: number\n}\n\nconst DEFAULT_POSITION = { x: 15, y: 12, z: 18 }\n\n/**\n * A 3D function grapher — any number of `z = f(x, y)` surfaces over a helper\n * grid, with axes.\n *\n * Extends {@link Canvas3D} rather than `Node3D`: a grapher is a *viewport* on a\n * scene it owns entirely, not a thing placed inside somebody else's. `Canvas3D`\n * owns the bridge from the 2D scene graph into the 3D renderer, so subclassing\n * it means this class only has to describe *what* to draw. It overrides\n * `buildScene3D`, the single seam both the real render and the asset\n * declaration pass go through.\n *\n * <Graph3D\n * width=\"fill\" height=\"fill\"\n * equations={[{ id: 1, expression: \"sin(sqrt(x^2 + y^2))\", color: \"#3B82F6\" }]}\n * />\n *\n * Every prop is a `@property`, so a constant, a `() => signal()` binding, and\n * `set()`/`to()` on a ref all work and agree. The non-scalar props each declare\n * a `mapper` (loose authored shape → one canonical internal shape) and a `tween`\n * (how that shape interpolates) — without the tween, `to({ grid: … })` would\n * hold the old value and snap at the end, which is the standard trap for an\n * object-valued attribute.\n *\n * **There is no OrbitControls and no pointer.** The camera is driven by the\n * node's own `orbit`/`elevation`/`zoom` props, which is what keeps every frame\n * reproducible under scrubbing and export — see {@link Graph3DCamera}.\n */\n@node({\n key: \"graph3d\",\n parentKey: \"node\",\n forkable: true,\n layout: {\n children: \"freeform\",\n defaultWidthMode: \"fixed\",\n defaultHeightMode: \"fixed\",\n acceptsChildren: true,\n },\n seed: {\n width: 960,\n height: 540,\n },\n})\nexport class Graph3D extends Canvas3D<Graph3DProps> {\n /**\n * The plotted surfaces.\n *\n * The mapper compiles each expression **once per write** (memoised by source),\n * so the builder — which re-runs every frame — never re-parses; and it folds\n * `visible`/`opacity` into a single number, so hiding is expressible as a\n * fade. An expression that doesn't parse is dropped and the rest still draw,\n * which is what lets the inspector commit on every keystroke.\n *\n * The tween matches the two lists by `id` — see `lerpEquations`.\n */\n @property({ default: [], mapper: resolveEquations, tween: lerpEquations })\n declare equations: Graph3DEquation[]\n\n /** The camera limits, and the framing that seeds the three spherical props. */\n @property({ default: {}, mapper: resolveCamera, tween: lerpCamera })\n declare camera: Graph3DCamera\n\n /** The helper grid and axes. Flags snap at the end of a tween; the rest lerp. */\n @property({ default: {}, mapper: resolveGrid, tween: lerpGrid })\n declare grid: Graph3DGrid\n\n @property({ default: 10 }) declare domain: number\n /**\n * Surface resolution. Tweenable, but *structural*: a geometry is immutable, so\n * every intermediate value reallocates each surface's buffers. Set it once\n * unless the resolution change is itself the point.\n */\n @property({ default: 80 }) declare segments: number\n @property({ default: 20 }) declare maxHeight: number\n\n @property({\n default: \"#111827\",\n mapper: resolveColor3D,\n tween: lerpColor3D,\n })\n declare background: Color\n\n @property({ default: false, tween: snapFlag })\n declare transparentBackground: boolean\n\n // No `default:` — the baseline is derived from `camera.initialPosition` in\n // `applyCameraDefaults` below, which can't be expressed as a static default.\n // So the control's kind is stated rather than inferred from one.\n @property({ kind: \"number\" }) declare orbit: number\n @property({ kind: \"number\" }) declare elevation: number\n @property({ kind: \"number\" }) declare zoom: number\n\n constructor(props?: NodeConfig<Graph3D, Graph3DProps>) {\n super(props as NodeConfig<Canvas3D<Graph3DProps>, Graph3DProps>)\n this.applyCameraDefaults(props)\n // The backdrop is this node's own 2D fill: a `Canvas3D` composites its 3D\n // pass over its fill layers, and there is no 3D background pass to reach\n // for. Bound rather than copied so a tweened `background` carries, and so\n // `transparentBackground` stays the *absence* of a fill — which is what\n // lets the 2D scene behind actually show through.\n if (props?.fill === undefined) {\n this.applyProp(\"fill\", () =>\n this.transparentBackground ? [] : Fills.color(this.background)\n )\n }\n }\n\n /**\n * Frame the scene at a spherical camera placement. Every axis is optional,\n * so \"pull back\" and \"spin round\" stay separate intentions — see\n * {@link OrbitTarget}.\n */\n @command({\n args: [\n {\n key: \"target\", kind: \"orbit\", properties: {\n orbit: { kind: \"number\", step: 1, unit: \"°\" },\n elevation: { kind: \"number\", min: -89, max: 89, step: 1, unit: \"°\" },\n zoom: { kind: \"number\", min: 0.2, max: 24, step: 0.1, unit: \"×\" },\n },\n },\n ],\n })\n orbitTo(args: CommandArgs<{ target: OrbitTarget }> & { duration: number }): Command<Graph3DProps> {\n return this.to({\n data: orbitProps(args.data?.target ?? {}) as Partial<Graph3DProps>,\n duration: args.duration,\n easing: args.easing ?? easeInOut(),\n })\n }\n\n /**\n * Seeds `orbit`/`elevation`/`zoom` from `camera.initialPosition`, so a caller\n * who gives a raw position gets that framing and one who gives none gets\n * {@link DEFAULT_POSITION} — without having to restate the other two axes.\n *\n * Bound reactively rather than copied, so the default *tracks* the camera and\n * a camera signal keeps working. Writing any of the three (a `set`, or the\n * first frame of a `to`) replaces the binding with the explicit value, which\n * is exactly the handover wanted.\n */\n private applyCameraDefaults(\n props?: NodeConfig<Graph3D, Graph3DProps>\n ): void {\n const seed = (): Orbit3D =>\n orbitOf((this.camera as CameraResolved).initialPosition)\n\n if (props?.orbit === undefined) {\n this.applyProp(\"orbit\", () => seed().orbit)\n }\n if (props?.elevation === undefined) {\n this.applyProp(\"elevation\", () => seed().elevation)\n }\n if (props?.zoom === undefined) {\n this.applyProp(\"zoom\", () => seed().distance)\n }\n }\n\n\n // ---- Drawing ------------------------------------------------------------\n\n protected override buildScene3D(): Scene3D {\n const scene = new Scene3D()\n const g3 = new Graphics3D()\n const camera = this.camera as unknown as CameraResolved\n const grid = this.grid as unknown as GridResolved\n const equations = this.equations as unknown as EquationResolved[]\n\n // Polar placement is the camera's own vocabulary now, so these three pass\n // straight through; `cameraOrbit` is the quarter turn between the stored\n // zero and the camera's. `near`/`far` come from the scene's own bounds.\n scene.perspective({\n fov: 45,\n orbit: cameraOrbit(this.orbit),\n elevation: this.elevation,\n // Clamped here rather than in the mapper: `zoom` and the limits are\n // separate props, so either can be tweened and the clamp still holds.\n distance: Math.min(Math.max(this.zoom, camera.minZoom), camera.maxZoom),\n })\n\n // The backdrop is the node's own 2D fill — a `Canvas3D` composites its 3D\n // pass over its fill layers, and there is no 3D background pass to reach\n // for. `transparentBackground` is therefore the *absence* of a fill rather\n // than a clear colour, which is what the props mapper already emits.\n\n // A soft ambient fill plus a key light and a weaker back light, so the\n // underside of a surface stays readable when the camera orbits beneath it.\n // A light's own parameters and its *placement* are separate arguments now —\n // placement is a transform, and a transform is not a property of the lamp.\n scene\n .light({ type: \"ambient\", intensity: 0.4 })\n .light({ type: \"directional\", intensity: 0.6 }, { position: [10, 20, 10] })\n .light(\n { type: \"directional\", intensity: 0.4 },\n { position: [-10, -20, -10] }\n )\n\n if (grid.showGrid) this.addGrid(g3, grid)\n if (grid.showAxes) this.addAxes(g3, grid.axesSize)\n\n const domain = this.domain\n const segments = Math.max(1, Math.round(this.segments))\n const maxHeight = this.maxHeight\n equations.forEach((equation, index) => {\n this.addSurface(g3, equation, index, domain, segments, maxHeight)\n })\n\n return scene.draw(g3)\n }\n\n /**\n * One `z = f(x, y)` surface, keyed by equation id rather than by position in\n * the op list — see {@link Graph3D.equations}.\n */\n private addSurface(\n g3: Graphics3D,\n equation: EquationResolved,\n index: number,\n domain: number,\n segments: number,\n maxHeight: number\n ): void {\n // Fully faded out (hidden, or the far end of a cross-fade): emit nothing.\n // Safe to skip precisely because identity is the `key`, not the slot.\n if (equation.opacity <= 0.001) return\n\n const span = domain * 2\n const { sample, opacity } = equation\n const solid = opacity >= 1\n\n g3.mesh(\n Geo.parametric({\n segments,\n // The maths plane is (x, y) and the result is height, so maths y becomes\n // the renderer's z and the result becomes its y.\n vertex: (u, v) => {\n const x = u * span - domain\n const z = v * span - domain\n return { x, y: sanitizeHeight(sample(x, z), maxHeight), z }\n },\n computeNormals: true, // without this the surface is flat-lit\n // Exactly the three things the callback above closes over. Without it the\n // renderer re-runs `vertex` over the whole grid every frame — ~6.6k calls\n // per surface at the default `segments: 80`, plus normals and a buffer\n // re-upload — and a frame is drawn for all sorts of reasons that leave the\n // surface untouched: the camera orbiting, a gizmo drag on an unrelated\n // node repainting at pointer rate, a tween elsewhere in the scene.\n //\n // A morph still re-evaluates as it must: `blendEquation` hands out a fresh\n // closure per frame mid-tween, and tweening `domain` or `maxHeight` moves\n // the number directly.\n revision: surfaceRevision(sample, domain, maxHeight),\n }),\n Mat.standard({\n color: equation.color,\n faces: \"both\", // a surface is infinitely thin; show both faces\n roughness: 0.4,\n metalness: 0.1,\n // No `transparent`: the renderer derives blending from the opacity, so\n // the flag and the value it was meant to accompany cannot disagree.\n opacity,\n // The fix for surfaces tearing holes in each other mid-fade. A\n // translucent mesh that still writes depth occludes everything drawn\n // after it, so a surface at 5% opacity renders as a near-invisible\n // cut-out through the surfaces behind it — and it resolves only once the\n // fade lands. Depth is written by fully opaque surfaces only; the rest\n // blend, which is what asking for an opacity below 1 means.\n depthWrite: solid,\n }),\n {\n key: `equation:${equation.id}`,\n // The renderer sorts the transparent pass back-to-front by centroid\n // distance — and every surface here is centred on the origin, so their\n // sort keys are identical and the order is arbitrary, which flickers as\n // the camera orbits. An explicit order makes it stable.\n renderOrder: index,\n }\n )\n }\n\n /**\n * The ground grid, as two line ops — the centre cross and everything else —\n * because a line op carries a single colour and the centre lines are drawn\n * differently from the rest.\n */\n private addGrid(g3: Graphics3D, grid: GridResolved): void {\n const divisions = Math.max(1, grid.divisions)\n const half = grid.size / 2\n const step = grid.size / divisions\n\n const centre: Vector3Input[] = []\n const lines: Vector3Input[] = []\n\n for (let i = 0; i <= divisions; i++) {\n const offset = -half + i * step\n // A line is a centre line when it passes through the origin. With an odd\n // division count no line does, which is the conventional behaviour.\n const isCentre = Math.abs(offset) < step / 1000\n const target = isCentre ? centre : lines\n target.push([-half, 0, offset], [half, 0, offset]) // along X\n target.push([offset, 0, -half], [offset, 0, half]) // along Z\n }\n\n if (lines.length > 0) {\n // No key: a conditionally-emitted op used to shift every later op's cache\n // slot, and the reconciler keys a drawable by its content now.\n g3.line({\n points: lines,\n segments: true,\n stroke: { fill: grid.colorGrid },\n })\n }\n if (centre.length > 0) {\n g3.line({\n points: centre,\n segments: true,\n stroke: { fill: grid.colorCenterLine },\n })\n }\n }\n\n /** X/Y/Z axes in red/green/blue, the conventional colouring. */\n private addAxes(g3: Graphics3D, length: number): void {\n const axes: Array<[Vector3Input, string]> = [\n [[length, 0, 0], \"#ff4d4d\"],\n [[0, length, 0], \"#4ade80\"],\n [[0, 0, length], \"#60a5fa\"],\n ]\n\n for (const [end, color] of axes) {\n g3.line({ points: [[0, 0, 0], end], stroke: { fill: color } })\n }\n }\n}\n\n// --- Mappers & tweens ------------------------------------------------------\n\n/** Mapper for {@link Graph3D.equations}. */\nfunction resolveEquations(\n input: Graph3DEquation[] | undefined\n): EquationResolved[] {\n if (!input) return []\n const out: EquationResolved[] = []\n for (const equation of input) {\n const sample = compileExpressionCached(equation.expression)\n if (!sample) continue // unparseable or empty — drop it, keep the rest\n out.push({\n id: String(equation.id),\n sample,\n color: resolveColor3D(equation.color),\n opacity: equation.visible === false ? 0 : (equation.opacity ?? 1),\n })\n }\n return out\n}\n\n/**\n * Mapper for {@link Graph3D.camera}.\n *\n * Merges onto `previous` rather than onto the bare defaults, which is what makes\n * a partial write mean \"change this much and leave the rest\". It matters most on\n * a `to` command, whose target states only the fields that command touches: a\n * tween that raises `maxZoom` must not silently hand the framing back to\n * {@link DEFAULT_POSITION} on its way.\n */\nfunction resolveCamera(\n input: Graph3DCamera | undefined,\n previous?: CameraResolved\n): CameraResolved {\n const base = previous ?? {\n initialPosition: DEFAULT_POSITION,\n minZoom: 0.1,\n maxZoom: Number.POSITIVE_INFINITY,\n }\n return {\n initialPosition: input?.initialPosition ?? base.initialPosition,\n minZoom: input?.minZoom ?? base.minZoom,\n maxZoom: input?.maxZoom ?? base.maxZoom,\n }\n}\n\n/** Tween for {@link Graph3D.camera}. */\nfunction lerpCamera(\n from: CameraResolved,\n to: CameraResolved,\n t: number\n): CameraResolved {\n return {\n initialPosition: {\n x: lerpNumber(from.initialPosition.x, to.initialPosition.x, t),\n y: lerpNumber(from.initialPosition.y, to.initialPosition.y, t),\n z: lerpNumber(from.initialPosition.z, to.initialPosition.z, t),\n },\n // `maxZoom` defaults to Infinity, which a plain lerp turns into NaN.\n minZoom: lerpFinite(from.minZoom, to.minZoom, t),\n maxZoom: lerpFinite(from.maxZoom, to.maxZoom, t),\n }\n}\n\n/** The grid a node with nothing said about it draws. */\nconst DEFAULT_GRID: GridResolved = {\n showGrid: true,\n size: 20,\n divisions: 20,\n colorCenterLine: resolveColor3D(\"#4b5563\"),\n colorGrid: resolveColor3D(\"#1f2937\"),\n showAxes: true,\n axesSize: 10,\n}\n\n/** Mapper for {@link Graph3D.grid}. Merges onto `previous` — see {@link resolveCamera}. */\nfunction resolveGrid(\n input: Graph3DGrid | undefined,\n previous?: GridResolved\n): GridResolved {\n const base = previous ?? DEFAULT_GRID\n return {\n showGrid: input?.showGrid ?? base.showGrid,\n size: input?.size ?? base.size,\n divisions: input?.divisions ?? base.divisions,\n colorCenterLine:\n input?.colorCenterLine === undefined\n ? base.colorCenterLine\n : resolveColor3D(input.colorCenterLine),\n colorGrid:\n input?.colorGrid === undefined\n ? base.colorGrid\n : resolveColor3D(input.colorGrid),\n showAxes: input?.showAxes ?? base.showAxes,\n axesSize: input?.axesSize ?? base.axesSize,\n }\n}\n\n/** Tween for {@link Graph3D.grid}. */\nfunction lerpGrid(from: GridResolved, to: GridResolved, t: number): GridResolved {\n return {\n showGrid: snapFlag(from.showGrid, to.showGrid, t),\n size: lerpNumber(from.size, to.size, t),\n divisions: lerpCount(from.divisions, to.divisions, t),\n colorCenterLine: lerpColor3D(from.colorCenterLine, to.colorCenterLine, t),\n colorGrid: lerpColor3D(from.colorGrid, to.colorGrid, t),\n showAxes: snapFlag(from.showAxes, to.showAxes, t),\n axesSize: lerpNumber(from.axesSize, to.axesSize, t),\n }\n}\n\n/** A camera placement in spherical terms. */\ninterface Orbit3D {\n orbit: number\n elevation: number\n distance: number\n}\n\n/** Decomposes a raw camera position into spherical terms. */\nfunction orbitOf(position: { x: number; y: number; z: number }): Orbit3D {\n const distance = Math.hypot(position.x, position.y, position.z) || 1\n const sin = Math.min(1, Math.max(-1, position.y / distance))\n return {\n orbit: Math.atan2(position.z, position.x) * (180 / Math.PI),\n elevation: Math.asin(sin) * (180 / Math.PI),\n distance,\n }\n}\n", "/**\n * The mappers, tweens and small maths the {@link Graph3D} node is built from.\n *\n * These exist because a `@property` is only as good as the `mapper`/`tween` it\n * is declared with: the mapper turns a loose authored value (a CSS colour, a\n * partly-filled options bag) into one canonical internal shape, and the tween is\n * what makes `node.to({ … })` *interpolate* that shape instead of holding the\n * old value and snapping at the end. Core's own attributes are built the same\n * way — see `cornerRadiusOps` behind `Rect.cornerRadius`.\n *\n * The colour, number and camera helpers this used to define now live in\n * `nodes/view3d-kit`, because the Protein node needs the same six and none of\n * them were ever about graphs. The equation list's shape and its tween moved the\n * same way, into `nodes/graph-kit`, once the 2D graph needed both. They are\n * re-exported here so this stays the one import a graph3d module reaches for.\n */\n\nimport { clamp } from \"@motionscript/sdk\"\n\nimport {\n lerpGraphEquations,\n type GraphEquationResolved,\n} from \"../kit/equations\"\n\nexport {\n lerpColor3D,\n lerpCount,\n lerpFinite,\n cameraOrbit,\n resolveColor3D,\n snapFlag,\n type Orbit,\n} from \"@motionscript/sdk/component\"\n\nimport type { CompiledExpression } from \"./expression\"\n\n// --- Sampling --------------------------------------------------------------\n\n/**\n * Makes a sampled height safe to put in a vertex buffer.\n *\n * `NaN` (from `sqrt` of a negative, `log` of zero) poisons a whole triangle and\n * leaves a hole; an asymptote (`1/x`, `tan`) produces values large enough to\n * stretch the mesh off screen and wreck the camera framing. Both are flattened\n * rather than dropped, so the surface stays continuous.\n */\nexport function sanitizeHeight(value: number, maxHeight: number): number {\n if (!Number.isFinite(value)) return 0\n return clamp(value, -maxHeight, maxHeight)\n}\n\n// --- Equations -------------------------------------------------------------\n\n/**\n * One surface after {@link Graph3D}'s mapper has run: compiled, resolved and\n * fully defaulted, so the per-frame builder never re-derives anything and the\n * tween never tests for an absent field.\n */\nexport type EquationResolved = GraphEquationResolved<CompiledExpression>\n\n/**\n * Tween for the equation list — the shared match-by-id walk, with the one thing\n * a surface does differently: mid-morph its *height* is sampled from both\n * functions and blended, so it deforms into the new one rather than popping.\n *\n * The blend deliberately returns a **fresh closure** per frame, which is what\n * {@link surfaceRevision} reads as \"re-evaluate this mesh\".\n */\nexport function lerpEquations(\n from: EquationResolved[],\n to: EquationResolved[],\n t: number\n): EquationResolved[] {\n return lerpGraphEquations(from, to, t, blendSurface)\n}\n\n/** How two surfaces' heights blend mid-tween. */\nfunction blendSurface(\n a: CompiledExpression,\n b: CompiledExpression,\n t: number\n): CompiledExpression {\n return (x, y) => {\n const start = a(x, y)\n return start + (b(x, y) - start) * t\n }\n}\n\n/**\n * A small integer standing for a compiled expression's *identity*.\n *\n * `Geo.parametric`'s `revision` is a number, and what a surface's vertex callback\n * actually depends on includes a function — so the function has to be reduced to\n * something numeric. Identity is the right comparison here rather than the source\n * text: `compileExpressionCached` memoises by source, so two equal sources are\n * already `===`, and a mid-morph blend (see {@link blendEquation}) deliberately\n * builds a *fresh* closure each frame, which is exactly when the surface must be\n * re-evaluated.\n *\n * A `WeakMap` so retiring an expression doesn't pin it, and ids are never reused\n * — a recycled id could make a changed surface look unchanged.\n */\nconst expressionIds = new WeakMap<CompiledExpression, number>()\nlet nextExpressionId = 1\n\nfunction expressionId(sample: CompiledExpression): number {\n let id = expressionIds.get(sample)\n if (id === undefined) {\n id = nextExpressionId++\n expressionIds.set(sample, id)\n }\n return id\n}\n\n/**\n * The `revision` for one surface: a value that changes exactly when its vertex\n * callback would return something different.\n *\n * The callback in `Graph3D.addSurface` closes over three things — the compiled\n * expression, the domain (as `span`/`domain`) and `maxHeight` — so those three\n * are what this covers, and nothing else. `segments` is deliberately absent: it\n * is plain data on the descriptor that the renderer already compares itself.\n *\n * Mixed by FNV-1a over the three values' text rather than by arithmetic, because\n * `domain` and `maxHeight` are tweenable floats and there is no cheap numeric\n * combination of two floats and an int that doesn't collide somewhere. A\n * collision here would show as a surface that refuses to update, which is a\n * miserable bug to find; the string is a few dozen characters per surface per\n * frame, against the ~6.6k vertex evaluations it exists to avoid.\n */\nexport function surfaceRevision(\n sample: CompiledExpression,\n domain: number,\n maxHeight: number\n): number {\n const key = `${expressionId(sample)}|${domain}|${maxHeight}`\n let hash = 0x811c9dc5\n for (let i = 0; i < key.length; i++) {\n hash ^= key.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return hash >>> 0\n}\n", "import { Graph2D } from \"./graph2d\";\nimport { Graph3D } from \"./graph3d\";\n\n/**\n * Every node type this package publishes.\n *\n * A host registers these by handing the array to an engine —\n * `new Engine(platform, { nodes: NODES })` — which reads each class's `@node()`\n * key. The decorator only *declares* that key: nothing is registered by the mere\n * act of importing a module, so a registry holds exactly what its owner asked\n * for and two engines in one process can differ about it.\n *\n * Listing class **values** is also what survives bundling. This package declares\n * `sideEffects: false`, which licenses a bundler to drop a module nothing\n * imports a value from, and a document names a node type by string — so\n * `import \"./graph2d\"` for its side effect is exactly what gets shaken out, where an\n * array of bindings is a data dependency that cannot be.\n */\nexport const NODES = [Graph2D, Graph3D];\n"],
5
+ "mappings": "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",
6
+ "names": ["blankSample", "arena", "arenaMark", "takeSample", "slot", "fresh", "sampleBudget", "curves", "sampleCurve", "f", "map", "clip", "budget", "pen", "CurvePen", "tLo", "tHi", "seeds", "seedCount", "step", "share", "spent", "write", "t", "x", "y", "ok", "clampCoord", "at", "edgeOf", "from", "to", "inside", "outsideT", "i", "offFrame", "midT", "mid", "settled", "emit", "a", "m", "b", "floored", "diverges", "edge", "refine", "depth", "flat", "ends", "previous", "next", "widthPx", "wanted", "dx", "dy", "length", "first", "second", "turn", "straddles", "sample", "value", "segment", "clipSegment", "start", "end", "samePoint", "point", "ax", "ay", "bx", "by", "t0", "t1", "edges", "q", "r", "runsKeyOf", "map", "clip", "CurveRunsCache", "id", "sample", "key", "hit", "budget", "runs", "sampleCurve", "ids", "curveError", "source", "expressionError", "CURVE_VARIABLES", "node", "Clip", "Fills", "Graphics2D", "Node2D", "clamp", "lerpNumber", "property", "niceStep", "MIN_SPAN", "MAX_SPAN", "resolvePlane", "view", "width", "height", "w", "h", "xSpan", "clampSpan", "scaleX", "ySpan", "scaleY", "span", "planeBox", "plane", "inflate", "rect", "margin", "toPx", "x", "toPy", "y", "panPlane", "dx", "dy", "zoomPlane", "factorX", "factorY", "anchorX", "anchorY", "uniform", "MAX_TICKS", "MAX_INDEX", "decadeOf", "value", "tickLadder", "min", "max", "pxPerUnit", "minSpacingPx", "step", "divisions", "minorDivisions", "multiplesWithin", "DIGIT_EM", "LABEL_MARGIN", "BASE_LABEL_PITCH", "labelLadder", "fontSize", "base", "ladder", "needed", "labelWidthEm", "widestLabel", "widest", "length", "formatTick", "magnitude", "first", "last", "count", "out", "i", "MAX_DIGITS", "MAX_FRACTION", "decade", "digits", "trimExponent", "decimals", "text", "precision", "mantissa", "exponent", "PLANE_SOURCE", "MINOR_WEIGHT", "MAX_INDEX", "planeVisible", "style", "clamp01", "planeUniforms", "plane", "xTicks", "yTicks", "ruled", "majorHalf", "withAlpha", "rgba", "pitchOf", "phaseOf", "step", "scale", "pitch", "center", "halfBox", "signedScale", "index", "remainder", "anchor", "color", "opacity", "value", "resolveColor3D", "lerpColor3D", "lerpCount", "snapFlag", "snapValue", "curves", "compileCurveCached", "source", "trimmed", "hit", "compiled", "compileExpressionCached", "CURVE_VARIABLES", "curve", "x", "lerpCurves", "from", "to", "t", "lerpGraphEquations", "blendCurve", "a", "b", "start", "DEFAULT_GRID", "resolveColor3D", "CLIP_MARGIN", "LABEL_PITCH_Y", "LABEL_GAP", "LABEL_BOX", "Graph2D", "Node2D", "CurveRunsCache", "width", "height", "Clip", "ctx", "plane", "resolvePlane", "grid", "box", "planeBox", "xTicks", "labelLadder", "yTicks", "tickLadder", "style", "clamp01", "planeVisible", "Graphics2D", "Fills", "PLANE_SOURCE", "planeUniforms", "labels", "gap", "xBox", "labelWidthEm", "yBox", "font", "axisY", "clamp", "toPy", "rowY", "x", "formatTick", "toPx", "axisX", "leftOf", "columnX", "rowHasZero", "y", "equations", "weight", "clip", "inflate", "key", "runsKeyOf", "live", "misses", "equation", "budget", "sampleBudget", "stroke", "runs", "run", "__decorate", "property", "resolveEquations", "lerpCurves", "resolveGrid", "lerpGrid", "lerpColor3D", "node", "resolveEquations", "input", "out", "equation", "sample", "compileCurveCached", "resolveColor3D", "resolveGrid", "previous", "base", "DEFAULT_GRID", "colorOr", "fallback", "lerpGrid", "from", "to", "t", "snapFlag", "lerpColor3D", "lerpNumber", "snapValue", "clamp01", "value", "clamp", "compileExpression", "source", "SURFACE_VARIABLES", "expressionError", "compileExpressionCached", "command", "easeInOut", "node", "Canvas3D", "Fills", "Geo", "Graphics3D", "Mat", "Scene3D", "lerpNumber", "property", "orbitProps", "clamp", "lerpColor3D", "lerpCount", "lerpFinite", "cameraOrbit", "resolveColor3D", "snapFlag", "sanitizeHeight", "value", "maxHeight", "clamp", "lerpEquations", "from", "to", "t", "lerpGraphEquations", "blendSurface", "a", "b", "x", "y", "start", "expressionIds", "nextExpressionId", "expressionId", "sample", "id", "surfaceRevision", "domain", "key", "hash", "DEFAULT_POSITION", "Graph3D", "Canvas3D", "props", "Fills", "args", "orbitProps", "easeInOut", "seed", "orbitOf", "scene", "Scene3D", "g3", "Graphics3D", "camera", "grid", "equations", "cameraOrbit", "domain", "segments", "maxHeight", "equation", "index", "span", "sample", "opacity", "solid", "Geo", "u", "v", "x", "z", "sanitizeHeight", "surfaceRevision", "Mat", "divisions", "half", "step", "centre", "lines", "i", "offset", "target", "length", "axes", "end", "color", "__decorate", "property", "resolveEquations", "lerpEquations", "resolveCamera", "lerpCamera", "resolveGrid", "lerpGrid", "resolveColor3D", "lerpColor3D", "snapFlag", "command", "node", "resolveEquations", "input", "out", "equation", "sample", "compileExpressionCached", "resolveColor3D", "resolveCamera", "previous", "base", "DEFAULT_POSITION", "lerpCamera", "from", "to", "t", "lerpNumber", "lerpFinite", "DEFAULT_GRID", "resolveGrid", "lerpGrid", "snapFlag", "lerpCount", "lerpColor3D", "orbitOf", "position", "distance", "sin", "NODES", "Graph2D", "Graph3D"]
7
+ }
@@ -0,0 +1,2 @@
1
+ import{a as o,b as r,c as e,d as f,e as m,f as p,g as t,h as x,i as a}from"./chunks/chunk-37IWXGPH.js";export{r as CURVE_VARIABLES,m as CURVE_VOCABULARY,f as EXPRESSION_VOCABULARY,o as SURFACE_VARIABLES,p as compileExpression,x as compileExpressionCached,t as expressionError,e as expressionVocabulary,a as lerpGraphEquations};
2
+ //# sourceMappingURL=kit.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@motionscript/plot",
3
+ "version": "0.1.0-alpha.3",
4
+ "exports": {
5
+ ".": "dist/browser/index.js",
6
+ "./kit": "dist/browser/kit.js"
7
+ },
8
+ "peers": [
9
+ "@motionscript/sdk"
10
+ ],
11
+ "assets": {}
12
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The plot **kit**: what these components share, and what a copied one does
3
+ * not copy.
4
+ *
5
+ * `ms add` writes a component's own files into your project and rewrites its
6
+ * reach for these to `@motionscript/plot`, so the shared parts stay a real
7
+ * dependency. That is not a size decision. The engine keys signal machinery to
8
+ * class identity — `@property` installs against the class it was applied with,
9
+ * and the renderer asks `instanceof` about what it is drawing — so a second copy
10
+ * of a shared class is two classes answering one name, and whichever one your
11
+ * component did not get draws nothing at all, with no error.
12
+ */
13
+ export * from "./kit/index.js";
14
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,OAAO,CAAC"}
package/dist/engine.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The plot **kit**: what these components share, and what a copied one does
3
+ * not copy.
4
+ *
5
+ * `ms add` writes a component's own files into your project and rewrites its
6
+ * reach for these to `@motionscript/plot`, so the shared parts stay a real
7
+ * dependency. That is not a size decision. The engine keys signal machinery to
8
+ * class identity — `@property` installs against the class it was applied with,
9
+ * and the renderer asks `instanceof` about what it is drawing — so a second copy
10
+ * of a shared class is two classes answering one name, and whichever one your
11
+ * component did not get draws nothing at all, with no error.
12
+ */
13
+ export * from "./kit/index.js";
14
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,OAAO,CAAC","sourcesContent":["/**\n * The plot **kit**: what these components share, and what a copied one does\n * not copy.\n *\n * `ms add` writes a component's own files into your project and rewrites its\n * reach for these to `@motionscript/plot`, so the shared parts stay a real\n * dependency. That is not a size decision. The engine keys signal machinery to\n * class identity — `@property` installs against the class it was applied with,\n * and the renderer asks `instanceof` about what it is drawing — so a second copy\n * of a shared class is two classes answering one name, and whichever one your\n * component did not get draws nothing at all, with no error.\n */\nexport * from \"./kit\";\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * One frame's worth of sampled curves, kept for the next one.
3
+ *
4
+ * `renderSelf` runs every frame; between two frames the thing that has normally
5
+ * changed is nothing at all — the playhead moved through a stretch where this
6
+ * node holds still, or some other node redrew and took the whole scene with it.
7
+ * Re-running the sampler for a picture whose curves have not moved is the single
8
+ * largest avoidable cost in the node.
9
+ *
10
+ * ## Returning the *same arrays* is half the point
11
+ *
12
+ * `skia-render` keys its geometry cache by node and shape index and compares the
13
+ * new descriptor against the old with a shallow walk that short-circuits on
14
+ * reference identity. Handing back a fresh array of identical points would still
15
+ * miss, rebuild the wasm path, and pay for the walk that discovered it needn't
16
+ * have. Handing back the same array instance skips both.
17
+ *
18
+ * ## Two keys, of deliberately different kinds
19
+ *
20
+ * The **plane** is compared structurally: eight numbers, so a pan of a hundredth
21
+ * of a pixel is a miss and a redraw of an untouched frame is a hit.
22
+ *
23
+ * The **function** is compared by *identity*, which `compileCurveCached` is what
24
+ * makes meaningful — equal source is the identical closure, so an inspector
25
+ * keystroke that doesn't change the expression re-samples nothing. And a curve
26
+ * caught mid-tween is a fresh closure on every frame by design (`blendCurve`),
27
+ * so it correctly misses: it really is changing.
28
+ *
29
+ * Colour and opacity are deliberately *not* in the key. They live on the paint,
30
+ * not on the shape, so dragging a curve's colour must not throw its geometry
31
+ * away.
32
+ */
33
+ import type { Vector2 } from "@motionscript/sdk";
34
+ import { type CurveFunction, type PixelRect, type PlaneMap, type SampleBudget } from "./curve.js";
35
+ /** Everything about the view that decides where a curve's points come out. */
36
+ export declare function runsKeyOf(map: PlaneMap, clip: PixelRect): string;
37
+ /** The sampled runs of every curve on one node, held across frames. */
38
+ export declare class CurveRunsCache {
39
+ private readonly entries;
40
+ /**
41
+ * Whether {@link runs} would answer from memory rather than re-sample.
42
+ *
43
+ * Asked of every curve *before* any of them is sampled, so the frame's
44
+ * evaluation budget can be divided among the ones that actually have work to
45
+ * do — five cached curves and one that moved gives the whole budget to the
46
+ * one that moved.
47
+ */
48
+ holds(id: string, sample: CurveFunction, key: string): boolean;
49
+ /** This curve's polylines, sampled only if something that decides them moved. */
50
+ runs(id: string, sample: CurveFunction, key: string, map: PlaneMap, clip: PixelRect, budget?: SampleBudget): Vector2[][];
51
+ /**
52
+ * Forgets every curve not in `ids` — the ones deleted, switched off, or faded
53
+ * to nothing since the last frame.
54
+ *
55
+ * Without it a node whose equation list is edited a hundred times holds a
56
+ * hundred stale point lists, none of which anything will ever ask for again.
57
+ */
58
+ retain(ids: readonly string[]): void;
59
+ }
60
+ //# sourceMappingURL=curve-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"curve-cache.d.ts","sourceRoot":"","sources":["../../src/graph2d/curve-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAEhD,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,YAAY,EAClB,MAAM,SAAS,CAAA;AAEhB,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAEhE;AAQD,uEAAuE;AACvE,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAEnD;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAK9D,iFAAiF;IACjF,IAAI,CACF,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,SAAS,EACf,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,EAAE,EAAE;IAUd;;;;;;OAMG;IACH,MAAM,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;CAMrC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * One frame's worth of sampled curves, kept for the next one.
3
+ *
4
+ * `renderSelf` runs every frame; between two frames the thing that has normally
5
+ * changed is nothing at all — the playhead moved through a stretch where this
6
+ * node holds still, or some other node redrew and took the whole scene with it.
7
+ * Re-running the sampler for a picture whose curves have not moved is the single
8
+ * largest avoidable cost in the node.
9
+ *
10
+ * ## Returning the *same arrays* is half the point
11
+ *
12
+ * `skia-render` keys its geometry cache by node and shape index and compares the
13
+ * new descriptor against the old with a shallow walk that short-circuits on
14
+ * reference identity. Handing back a fresh array of identical points would still
15
+ * miss, rebuild the wasm path, and pay for the walk that discovered it needn't
16
+ * have. Handing back the same array instance skips both.
17
+ *
18
+ * ## Two keys, of deliberately different kinds
19
+ *
20
+ * The **plane** is compared structurally: eight numbers, so a pan of a hundredth
21
+ * of a pixel is a miss and a redraw of an untouched frame is a hit.
22
+ *
23
+ * The **function** is compared by *identity*, which `compileCurveCached` is what
24
+ * makes meaningful — equal source is the identical closure, so an inspector
25
+ * keystroke that doesn't change the expression re-samples nothing. And a curve
26
+ * caught mid-tween is a fresh closure on every frame by design (`blendCurve`),
27
+ * so it correctly misses: it really is changing.
28
+ *
29
+ * Colour and opacity are deliberately *not* in the key. They live on the paint,
30
+ * not on the shape, so dragging a curve's colour must not throw its geometry
31
+ * away.
32
+ */
33
+ import { sampleCurve, } from "./curve.js";
34
+ /** Everything about the view that decides where a curve's points come out. */
35
+ export function runsKeyOf(map, clip) {
36
+ return `${map.centerX}|${map.centerY}|${map.scaleX}|${map.scaleY}|${clip.left}|${clip.right}|${clip.bottom}|${clip.top}`;
37
+ }
38
+ /** The sampled runs of every curve on one node, held across frames. */
39
+ export class CurveRunsCache {
40
+ entries = new Map();
41
+ /**
42
+ * Whether {@link runs} would answer from memory rather than re-sample.
43
+ *
44
+ * Asked of every curve *before* any of them is sampled, so the frame's
45
+ * evaluation budget can be divided among the ones that actually have work to
46
+ * do — five cached curves and one that moved gives the whole budget to the
47
+ * one that moved.
48
+ */
49
+ holds(id, sample, key) {
50
+ const hit = this.entries.get(id);
51
+ return hit !== undefined && hit.key === key && hit.sample === sample;
52
+ }
53
+ /** This curve's polylines, sampled only if something that decides them moved. */
54
+ runs(id, sample, key, map, clip, budget) {
55
+ const hit = this.entries.get(id);
56
+ if (hit !== undefined && hit.key === key && hit.sample === sample) {
57
+ return hit.runs;
58
+ }
59
+ const runs = sampleCurve(sample, map, clip, budget);
60
+ this.entries.set(id, { key, sample, runs });
61
+ return runs;
62
+ }
63
+ /**
64
+ * Forgets every curve not in `ids` — the ones deleted, switched off, or faded
65
+ * to nothing since the last frame.
66
+ *
67
+ * Without it a node whose equation list is edited a hundred times holds a
68
+ * hundred stale point lists, none of which anything will ever ask for again.
69
+ */
70
+ retain(ids) {
71
+ if (this.entries.size === ids.length)
72
+ return;
73
+ for (const id of this.entries.keys()) {
74
+ if (!ids.includes(id))
75
+ this.entries.delete(id);
76
+ }
77
+ }
78
+ }
79
+ //# sourceMappingURL=curve-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"curve-cache.js","sourceRoot":"","sources":["../../src/graph2d/curve-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,EACL,WAAW,GAKZ,MAAM,SAAS,CAAA;AAEhB,8EAA8E;AAC9E,MAAM,UAAU,SAAS,CAAC,GAAa,EAAE,IAAe;IACtD,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAA;AAC1H,CAAC;AAQD,uEAAuE;AACvE,MAAM,OAAO,cAAc;IACR,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAA;IAEnD;;;;;;;OAOG;IACH,KAAK,CAAC,EAAU,EAAE,MAAqB,EAAE,GAAW;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAChC,OAAO,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAA;IACtE,CAAC;IAED,iFAAiF;IACjF,IAAI,CACF,EAAU,EACV,MAAqB,EACrB,GAAW,EACX,GAAa,EACb,IAAe,EACf,MAAqB;QAErB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAChC,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAClE,OAAO,GAAG,CAAC,IAAI,CAAA;QACjB,CAAC;QACD,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAA;QACnD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,GAAsB;QAC3B,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,GAAG,CAAC,MAAM;YAAE,OAAM;QAC5C,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YACrC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAChD,CAAC;IACH,CAAC;CACF","sourcesContent":["/**\n * One frame's worth of sampled curves, kept for the next one.\n *\n * `renderSelf` runs every frame; between two frames the thing that has normally\n * changed is nothing at all — the playhead moved through a stretch where this\n * node holds still, or some other node redrew and took the whole scene with it.\n * Re-running the sampler for a picture whose curves have not moved is the single\n * largest avoidable cost in the node.\n *\n * ## Returning the *same arrays* is half the point\n *\n * `skia-render` keys its geometry cache by node and shape index and compares the\n * new descriptor against the old with a shallow walk that short-circuits on\n * reference identity. Handing back a fresh array of identical points would still\n * miss, rebuild the wasm path, and pay for the walk that discovered it needn't\n * have. Handing back the same array instance skips both.\n *\n * ## Two keys, of deliberately different kinds\n *\n * The **plane** is compared structurally: eight numbers, so a pan of a hundredth\n * of a pixel is a miss and a redraw of an untouched frame is a hit.\n *\n * The **function** is compared by *identity*, which `compileCurveCached` is what\n * makes meaningful — equal source is the identical closure, so an inspector\n * keystroke that doesn't change the expression re-samples nothing. And a curve\n * caught mid-tween is a fresh closure on every frame by design (`blendCurve`),\n * so it correctly misses: it really is changing.\n *\n * Colour and opacity are deliberately *not* in the key. They live on the paint,\n * not on the shape, so dragging a curve's colour must not throw its geometry\n * away.\n */\n\nimport type { Vector2 } from \"@motionscript/sdk\"\n\nimport {\n sampleCurve,\n type CurveFunction,\n type PixelRect,\n type PlaneMap,\n type SampleBudget,\n} from \"./curve\"\n\n/** Everything about the view that decides where a curve's points come out. */\nexport function runsKeyOf(map: PlaneMap, clip: PixelRect): string {\n return `${map.centerX}|${map.centerY}|${map.scaleX}|${map.scaleY}|${clip.left}|${clip.right}|${clip.bottom}|${clip.top}`\n}\n\ninterface Entry {\n key: string\n sample: CurveFunction\n runs: Vector2[][]\n}\n\n/** The sampled runs of every curve on one node, held across frames. */\nexport class CurveRunsCache {\n private readonly entries = new Map<string, Entry>()\n\n /**\n * Whether {@link runs} would answer from memory rather than re-sample.\n *\n * Asked of every curve *before* any of them is sampled, so the frame's\n * evaluation budget can be divided among the ones that actually have work to\n * do — five cached curves and one that moved gives the whole budget to the\n * one that moved.\n */\n holds(id: string, sample: CurveFunction, key: string): boolean {\n const hit = this.entries.get(id)\n return hit !== undefined && hit.key === key && hit.sample === sample\n }\n\n /** This curve's polylines, sampled only if something that decides them moved. */\n runs(\n id: string,\n sample: CurveFunction,\n key: string,\n map: PlaneMap,\n clip: PixelRect,\n budget?: SampleBudget\n ): Vector2[][] {\n const hit = this.entries.get(id)\n if (hit !== undefined && hit.key === key && hit.sample === sample) {\n return hit.runs\n }\n const runs = sampleCurve(sample, map, clip, budget)\n this.entries.set(id, { key, sample, runs })\n return runs\n }\n\n /**\n * Forgets every curve not in `ids` — the ones deleted, switched off, or faded\n * to nothing since the last frame.\n *\n * Without it a node whose equation list is edited a hundred times holds a\n * hundred stale point lists, none of which anything will ever ask for again.\n */\n retain(ids: readonly string[]): void {\n if (this.entries.size === ids.length) return\n for (const id of this.entries.keys()) {\n if (!ids.includes(id)) this.entries.delete(id)\n }\n }\n}\n"]}