dsviz 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -2
- package/dist/camera.d.ts +25 -0
- package/dist/camera.js +27 -0
- package/dist/camera.js.map +7 -0
- package/dist/connector.d.ts +65 -0
- package/dist/connector.js +108 -0
- package/dist/connector.js.map +7 -0
- package/dist/describe.d.ts +44 -0
- package/dist/describe.js +131 -0
- package/dist/describe.js.map +7 -0
- package/dist/ds/array.d.ts +51 -0
- package/dist/ds/array.js +62 -0
- package/dist/ds/array.js.map +7 -0
- package/dist/ds/common.d.ts +9 -0
- package/dist/ds/common.js +10 -0
- package/dist/ds/common.js.map +7 -0
- package/dist/ds/graph.d.ts +39 -0
- package/dist/ds/graph.js +57 -0
- package/dist/ds/graph.js.map +7 -0
- package/dist/ds/grid.d.ts +30 -0
- package/dist/ds/grid.js +41 -0
- package/dist/ds/grid.js.map +7 -0
- package/dist/ds/index.d.ts +8 -0
- package/dist/ds/index.js +9 -0
- package/dist/ds/index.js.map +7 -0
- package/dist/ds/linkedList.d.ts +46 -0
- package/dist/ds/linkedList.js +68 -0
- package/dist/ds/linkedList.js.map +7 -0
- package/dist/ds/pointer.d.ts +23 -0
- package/dist/ds/pointer.js +35 -0
- package/dist/ds/pointer.js.map +7 -0
- package/dist/ds/stackQueue.d.ts +5 -0
- package/dist/ds/stackQueue.js +49 -0
- package/dist/ds/stackQueue.js.map +7 -0
- package/dist/ds/tree.d.ts +53 -0
- package/dist/ds/tree.js +87 -0
- package/dist/ds/tree.js.map +7 -0
- package/dist/easing.d.ts +14 -0
- package/dist/easing.js +21 -0
- package/dist/easing.js.map +7 -0
- package/dist/fx.d.ts +21 -0
- package/dist/fx.js +27 -0
- package/dist/fx.js.map +7 -0
- package/dist/geom/index.d.ts +2 -0
- package/dist/geom/index.js +3 -0
- package/dist/geom/index.js.map +7 -0
- package/dist/geom/math.d.ts +34 -0
- package/dist/geom/math.js +74 -0
- package/dist/geom/math.js.map +7 -0
- package/dist/geom/shapes.d.ts +69 -0
- package/dist/geom/shapes.js +105 -0
- package/dist/geom/shapes.js.map +7 -0
- package/dist/handle.d.ts +30 -0
- package/dist/handle.js +13 -0
- package/dist/handle.js.map +7 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +7 -0
- package/dist/mathtext.d.ts +29 -0
- package/dist/mathtext.js +142 -0
- package/dist/mathtext.js.map +7 -0
- package/dist/mem/index.d.ts +2 -0
- package/dist/mem/index.js +3 -0
- package/dist/mem/index.js.map +7 -0
- package/dist/mem/memory.d.ts +47 -0
- package/dist/mem/memory.js +41 -0
- package/dist/mem/memory.js.map +7 -0
- package/dist/mem/stackHeap.d.ts +48 -0
- package/dist/mem/stackHeap.js +76 -0
- package/dist/mem/stackHeap.js.map +7 -0
- package/dist/path.d.ts +23 -0
- package/dist/path.js +49 -0
- package/dist/path.js.map +7 -0
- package/dist/player.d.ts +49 -0
- package/dist/player.js +100 -0
- package/dist/player.js.map +7 -0
- package/dist/plot/index.d.ts +2 -0
- package/dist/plot/index.js +3 -0
- package/dist/plot/index.js.map +7 -0
- package/dist/plot/marks.d.ts +120 -0
- package/dist/plot/marks.js +200 -0
- package/dist/plot/marks.js.map +7 -0
- package/dist/plot/plane.d.ts +45 -0
- package/dist/plot/plane.js +41 -0
- package/dist/plot/plane.js.map +7 -0
- package/dist/prob/bars.d.ts +67 -0
- package/dist/prob/bars.js +82 -0
- package/dist/prob/bars.js.map +7 -0
- package/dist/prob/index.d.ts +3 -0
- package/dist/prob/index.js +4 -0
- package/dist/prob/index.js.map +7 -0
- package/dist/prob/tree.d.ts +39 -0
- package/dist/prob/tree.js +57 -0
- package/dist/prob/tree.js.map +7 -0
- package/dist/prob/venn.d.ts +38 -0
- package/dist/prob/venn.js +89 -0
- package/dist/prob/venn.js.map +7 -0
- package/dist/react/Diagram.d.ts +47 -0
- package/dist/react/Diagram.js +41 -0
- package/dist/react/Diagram.js.map +7 -0
- package/dist/react/MathText.d.ts +9 -0
- package/dist/react/MathText.js +15 -0
- package/dist/react/MathText.js.map +7 -0
- package/dist/react/controls-styles.d.ts +9 -0
- package/dist/react/controls-styles.js +109 -0
- package/dist/react/controls-styles.js.map +7 -0
- package/dist/react/controls.d.ts +84 -0
- package/dist/react/controls.js +146 -0
- package/dist/react/controls.js.map +7 -0
- package/dist/react/hooks.d.ts +48 -0
- package/dist/react/hooks.js +67 -0
- package/dist/react/hooks.js.map +7 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +7 -0
- package/dist/render.d.ts +61 -0
- package/dist/render.js +294 -0
- package/dist/render.js.map +7 -0
- package/dist/scene.d.ts +138 -0
- package/dist/scene.js +94 -0
- package/dist/scene.js.map +7 -0
- package/dist/sched/gantt.d.ts +49 -0
- package/dist/sched/gantt.js +65 -0
- package/dist/sched/gantt.js.map +7 -0
- package/dist/sched/index.d.ts +2 -0
- package/dist/sched/index.js +3 -0
- package/dist/sched/index.js.map +7 -0
- package/dist/sched/simulate.d.ts +55 -0
- package/dist/sched/simulate.js +78 -0
- package/dist/sched/simulate.js.map +7 -0
- package/dist/static.d.ts +8 -0
- package/dist/static.js +26 -0
- package/dist/static.js.map +7 -0
- package/dist/styles.d.ts +3 -0
- package/dist/styles.js +241 -0
- package/dist/styles.js.map +7 -0
- package/dist/sys/architecture.d.ts +104 -0
- package/dist/sys/architecture.js +167 -0
- package/dist/sys/architecture.js.map +7 -0
- package/dist/sys/index.d.ts +1 -0
- package/dist/sys/index.js +2 -0
- package/dist/sys/index.js.map +7 -0
- package/dist/themes.d.ts +13 -0
- package/dist/themes.js +8 -0
- package/dist/themes.js.map +7 -0
- package/dist/uml/classDiagram.d.ts +72 -0
- package/dist/uml/classDiagram.js +84 -0
- package/dist/uml/classDiagram.js.map +7 -0
- package/dist/uml/index.d.ts +3 -0
- package/dist/uml/index.js +4 -0
- package/dist/uml/index.js.map +7 -0
- package/dist/uml/sequence.d.ts +58 -0
- package/dist/uml/sequence.js +121 -0
- package/dist/uml/sequence.js.map +7 -0
- package/dist/uml/stateMachine.d.ts +40 -0
- package/dist/uml/stateMachine.js +89 -0
- package/dist/uml/stateMachine.js.map +7 -0
- package/dist/vec.d.ts +21 -0
- package/dist/vec.js +24 -0
- package/dist/vec.js.map +7 -0
- package/package.json +93 -4
- package/src/camera.ts +50 -0
- package/src/connector.ts +182 -0
- package/src/describe.ts +181 -0
- package/src/ds/array.ts +108 -0
- package/src/ds/common.ts +16 -0
- package/src/ds/graph.ts +104 -0
- package/src/ds/grid.ts +70 -0
- package/src/ds/index.ts +8 -0
- package/src/ds/linkedList.ts +117 -0
- package/src/ds/pointer.ts +55 -0
- package/src/ds/stackQueue.ts +59 -0
- package/src/ds/tree.ts +139 -0
- package/src/easing.ts +24 -0
- package/src/fx.ts +46 -0
- package/src/geom/index.ts +2 -0
- package/src/geom/math.ts +85 -0
- package/src/geom/shapes.ts +158 -0
- package/src/handle.ts +40 -0
- package/src/index.ts +37 -0
- package/src/mathtext.ts +139 -0
- package/src/mem/index.ts +2 -0
- package/src/mem/memory.ts +85 -0
- package/src/mem/stackHeap.ts +127 -0
- package/src/path.ts +68 -0
- package/src/player.ts +153 -0
- package/src/plot/index.ts +2 -0
- package/src/plot/marks.ts +298 -0
- package/src/plot/plane.ts +82 -0
- package/src/prob/bars.ts +132 -0
- package/src/prob/index.ts +3 -0
- package/src/prob/tree.ts +94 -0
- package/src/prob/venn.ts +144 -0
- package/src/react/Diagram.tsx +97 -0
- package/src/react/MathText.tsx +21 -0
- package/src/react/controls-styles.ts +111 -0
- package/src/react/controls.tsx +297 -0
- package/src/react/hooks.ts +112 -0
- package/src/react/index.ts +4 -0
- package/src/render.ts +445 -0
- package/src/scene.ts +238 -0
- package/src/sched/gantt.ts +110 -0
- package/src/sched/index.ts +2 -0
- package/src/sched/simulate.ts +140 -0
- package/src/static.ts +35 -0
- package/src/styles.ts +272 -0
- package/src/sys/architecture.ts +257 -0
- package/src/sys/index.ts +1 -0
- package/src/themes.ts +26 -0
- package/src/uml/classDiagram.ts +145 -0
- package/src/uml/index.ts +3 -0
- package/src/uml/sequence.ts +218 -0
- package/src/uml/stateMachine.ts +133 -0
- package/src/vec.ts +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jesse Williamson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsviz
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TypeScript library for interactive, animated SVG diagrams for teaching: data structures and algorithms,
|
|
4
|
+
plots, probability, geometry, CPU scheduling and memory. You describe the picture as data; dsviz draws it
|
|
5
|
+
and animates every change. Framework-free core, thin React layer.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
state ──▶ layout functions ──▶ Scene (plain data, stable ids) ──▶ <Diagram> / createView / toSVG
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use it in a project
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install dsviz
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { array, combine, pointer } from 'dsviz';
|
|
19
|
+
import { Diagram } from 'dsviz/react';
|
|
20
|
+
|
|
21
|
+
const arr = array([3, 8, 12, 17], { tones: { 2: 'active' } });
|
|
22
|
+
<Diagram scene={combine(arr, pointer({ id: 'i', at: arr.top(2), label: 'i' }))} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Families: data structures at the top level (`array`, `pointer`, `linkedList`, `binaryTree`, `graph`, `grid`,
|
|
26
|
+
`stack`, `queue`) and the namespaces `plot`, `prob`, `geom`, `sched`, `mem`. Colours are CSS variables
|
|
27
|
+
(`--dv-*`) you can override; dark mode via `[data-theme="dark"]`.
|
|
28
|
+
|
|
29
|
+
## This repo
|
|
30
|
+
|
|
31
|
+
- `npm run dev` — the docs and examples site (`site/`)
|
|
32
|
+
- `npm test` — unit tests
|
|
33
|
+
- `npm run build` — library build into `dist/`
|
|
34
|
+
- `npm run build:site` — static site into `site-dist/` (deployed to GitHub Pages by CI)
|
|
35
|
+
|
|
36
|
+
See `CLAUDE.md` for design notes.
|
package/dist/camera.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the camera (the viewBox) should be.
|
|
3
|
+
*
|
|
4
|
+
* Rule of thumb for teaching diagrams: the camera should almost never move.
|
|
5
|
+
* Whatever moves on screen is what the learner reads as "the change", so a
|
|
6
|
+
* camera that refits on every step makes everything appear to move at once.
|
|
7
|
+
*
|
|
8
|
+
* - fixed You pass a viewBox. Use this for plots and anything draggable.
|
|
9
|
+
* - fitViewBox The union of every frame you'll ever show (e.g. all of an
|
|
10
|
+
* algorithm's steps), computed up front, then fixed.
|
|
11
|
+
* - 'grow' The default for `viewBox: 'auto'`: grow only when something
|
|
12
|
+
* would fall outside, and shrink only when the content is much
|
|
13
|
+
* smaller than the frame, never on small wobbles.
|
|
14
|
+
* - 'fit' Refit to the scene on every render (the old behaviour).
|
|
15
|
+
*/
|
|
16
|
+
import { Part } from './scene';
|
|
17
|
+
export type ViewBox = [number, number, number, number];
|
|
18
|
+
/** A viewBox around everything in all the given parts (e.g. one scene per frame). */
|
|
19
|
+
export declare function fitViewBox(parts: Part | readonly Part[], pad?: number | [number, number, number, number]): ViewBox;
|
|
20
|
+
/**
|
|
21
|
+
* The next camera, given the current one and the padded box around the new content.
|
|
22
|
+
* 'grow': stay put while the content fits; grow to include it when it doesn't;
|
|
23
|
+
* shrink back to it only when it fills less than half the frame.
|
|
24
|
+
*/
|
|
25
|
+
export declare function nextCamera(current: ViewBox | null, content: ViewBox, mode?: 'grow' | 'fit'): ViewBox;
|
package/dist/camera.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { bounds, combine } from "./scene.js";
|
|
2
|
+
function fitViewBox(parts, pad = 24) {
|
|
3
|
+
const b = bounds(combine(parts));
|
|
4
|
+
const [t, r, bt, l] = typeof pad === "number" ? [pad, pad, pad, pad] : pad;
|
|
5
|
+
return snap([b.x - l, b.y - t, b.w + l + r, b.h + t + bt]);
|
|
6
|
+
}
|
|
7
|
+
const snap = ([x, y, w, h]) => {
|
|
8
|
+
const x0 = Math.floor(x), y0 = Math.floor(y);
|
|
9
|
+
return [x0, y0, Math.max(1, Math.ceil(x + w) - x0), Math.max(1, Math.ceil(y + h) - y0)];
|
|
10
|
+
};
|
|
11
|
+
const contains = (a, b) => b[0] >= a[0] && b[1] >= a[1] && b[0] + b[2] <= a[0] + a[2] && b[1] + b[3] <= a[1] + a[3];
|
|
12
|
+
const union = (a, b) => {
|
|
13
|
+
const x0 = Math.min(a[0], b[0]), y0 = Math.min(a[1], b[1]);
|
|
14
|
+
return [x0, y0, Math.max(a[0] + a[2], b[0] + b[2]) - x0, Math.max(a[1] + a[3], b[1] + b[3]) - y0];
|
|
15
|
+
};
|
|
16
|
+
function nextCamera(current, content, mode = "grow") {
|
|
17
|
+
const c = snap(content);
|
|
18
|
+
if (!current || mode === "fit") return c;
|
|
19
|
+
if (!contains(current, c)) return union(current, c);
|
|
20
|
+
const fill = c[2] * c[3] / (current[2] * current[3]);
|
|
21
|
+
return fill < 0.5 ? c : current;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
fitViewBox,
|
|
25
|
+
nextCamera
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=camera.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/camera.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Where the camera (the viewBox) should be.\n *\n * Rule of thumb for teaching diagrams: the camera should almost never move.\n * Whatever moves on screen is what the learner reads as \"the change\", so a\n * camera that refits on every step makes everything appear to move at once.\n *\n * - fixed You pass a viewBox. Use this for plots and anything draggable.\n * - fitViewBox The union of every frame you'll ever show (e.g. all of an\n * algorithm's steps), computed up front, then fixed.\n * - 'grow' The default for `viewBox: 'auto'`: grow only when something\n * would fall outside, and shrink only when the content is much\n * smaller than the frame, never on small wobbles.\n * - 'fit' Refit to the scene on every render (the old behaviour).\n */\nimport { Part, bounds, combine } from './scene';\n\nexport type ViewBox = [number, number, number, number];\n\n/** A viewBox around everything in all the given parts (e.g. one scene per frame). */\nexport function fitViewBox(parts: Part | readonly Part[], pad: number | [number, number, number, number] = 24): ViewBox {\n const b = bounds(combine(parts as Part));\n const [t, r, bt, l] = typeof pad === 'number' ? [pad, pad, pad, pad] : pad;\n return snap([b.x - l, b.y - t, b.w + l + r, b.h + t + bt]);\n}\n\n/** Round outward to whole units, so tiny bounds changes can't make the camera twitch. */\nconst snap = ([x, y, w, h]: number[]): ViewBox => {\n const x0 = Math.floor(x), y0 = Math.floor(y);\n return [x0, y0, Math.max(1, Math.ceil(x + w) - x0), Math.max(1, Math.ceil(y + h) - y0)];\n};\n\nconst contains = (a: ViewBox, b: ViewBox) => b[0] >= a[0] && b[1] >= a[1] && b[0] + b[2] <= a[0] + a[2] && b[1] + b[3] <= a[1] + a[3];\nconst union = (a: ViewBox, b: ViewBox): ViewBox => {\n const x0 = Math.min(a[0], b[0]), y0 = Math.min(a[1], b[1]);\n return [x0, y0, Math.max(a[0] + a[2], b[0] + b[2]) - x0, Math.max(a[1] + a[3], b[1] + b[3]) - y0];\n};\n\n/**\n * The next camera, given the current one and the padded box around the new content.\n * 'grow': stay put while the content fits; grow to include it when it doesn't;\n * shrink back to it only when it fills less than half the frame.\n */\nexport function nextCamera(current: ViewBox | null, content: ViewBox, mode: 'grow' | 'fit' = 'grow'): ViewBox {\n const c = snap(content);\n if (!current || mode === 'fit') return c;\n if (!contains(current, c)) return union(current, c);\n const fill = (c[2] * c[3]) / (current[2] * current[3]);\n return fill < 0.5 ? c : current;\n}\n"],
|
|
5
|
+
"mappings": "AAeA,SAAe,QAAQ,eAAe;AAK/B,SAAS,WAAW,OAA+B,MAAiD,IAAa;AACtH,QAAM,IAAI,OAAO,QAAQ,KAAa,CAAC;AACvC,QAAM,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,OAAO,QAAQ,WAAW,CAAC,KAAK,KAAK,KAAK,GAAG,IAAI;AACvE,SAAO,KAAK,CAAC,EAAE,IAAI,GAAG,EAAE,IAAI,GAAG,EAAE,IAAI,IAAI,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC;AAC3D;AAGA,MAAM,OAAO,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,MAAyB;AAChD,QAAM,KAAK,KAAK,MAAM,CAAC,GAAG,KAAK,KAAK,MAAM,CAAC;AAC3C,SAAO,CAAC,IAAI,IAAI,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;AACxF;AAEA,MAAM,WAAW,CAAC,GAAY,MAAe,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC;AACpI,MAAM,QAAQ,CAAC,GAAY,MAAwB;AACjD,QAAM,KAAK,KAAK,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;AACzD,SAAO,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;AAClG;AAOO,SAAS,WAAW,SAAyB,SAAkB,OAAuB,QAAiB;AAC5G,QAAM,IAAI,KAAK,OAAO;AACtB,MAAI,CAAC,WAAW,SAAS,MAAO,QAAO;AACvC,MAAI,CAAC,SAAS,SAAS,CAAC,EAAG,QAAO,MAAM,SAAS,CAAC;AAClD,QAAM,OAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAM,QAAQ,CAAC,IAAI,QAAQ,CAAC;AACpD,SAAO,OAAO,MAAM,IAAI;AAC1B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connectors: a line between two boxes (or points) with a decoration at either
|
|
3
|
+
* end. This is what UML relationships and architecture links are made of.
|
|
4
|
+
*
|
|
5
|
+
* ─────▷ 'triangle' hollow triangle (inheritance, realisation)
|
|
6
|
+
* ─────▶ 'arrow' solid triangle (a message, a request)
|
|
7
|
+
* ─────> 'open' open chevron (association, dependency, async)
|
|
8
|
+
* ◇───── 'diamond' hollow diamond (aggregation, at the owner)
|
|
9
|
+
* ◆───── 'diamond-filled' solid diamond (composition, at the owner)
|
|
10
|
+
*
|
|
11
|
+
* Each end marker is its own small closed path with a role (`end-solid`,
|
|
12
|
+
* `end-hollow`, `end-open`), so the stylesheet decides how it looks. The line
|
|
13
|
+
* stops where the marker begins, so it never shows through a hollow head.
|
|
14
|
+
*/
|
|
15
|
+
import { Box, Scene, Tone } from './scene';
|
|
16
|
+
import { Vec } from './vec';
|
|
17
|
+
export type ConnectorEnd = 'none' | 'arrow' | 'open' | 'triangle' | 'diamond' | 'diamond-filled';
|
|
18
|
+
/** A box given by its CENTRE and size, like a rect node. */
|
|
19
|
+
export interface Frame {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
w: number;
|
|
23
|
+
h: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Where the ray from a box's centre toward `toward` leaves the box.
|
|
27
|
+
* The ray is centre + t·d. It hits the vertical sides at t = (w/2)/|dx| and the
|
|
28
|
+
* horizontal sides at t = (h/2)/|dy|; whichever comes first is the exit.
|
|
29
|
+
*/
|
|
30
|
+
export declare function rectEdge(f: Frame, toward: Vec): Vec;
|
|
31
|
+
/** A centred Frame from a top-left Box. */
|
|
32
|
+
export declare const frameOf: (b: Box) => Frame;
|
|
33
|
+
export interface ConnectorOptions {
|
|
34
|
+
id: string;
|
|
35
|
+
from: Vec | Frame;
|
|
36
|
+
to: Vec | Frame;
|
|
37
|
+
/** 'straight' (default) or 'elbow': right-angled, leaving and entering through the facing sides. */
|
|
38
|
+
route?: 'straight' | 'elbow';
|
|
39
|
+
start?: ConnectorEnd;
|
|
40
|
+
end?: ConnectorEnd;
|
|
41
|
+
dashed?: boolean;
|
|
42
|
+
tone?: Tone;
|
|
43
|
+
width?: number;
|
|
44
|
+
/** Text at the middle of the line. */
|
|
45
|
+
label?: string;
|
|
46
|
+
/** Small text near each end (UML multiplicities like '1' and '0..*'). */
|
|
47
|
+
startLabel?: string;
|
|
48
|
+
endLabel?: string;
|
|
49
|
+
/** Points to pass through on the way (overrides `route`), e.g. a self-message loop. */
|
|
50
|
+
via?: readonly Vec[];
|
|
51
|
+
/** Gap left between the line and a point endpoint (boxes already stop at their edge). */
|
|
52
|
+
gap?: number;
|
|
53
|
+
opacity?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ConnectorScene extends Scene {
|
|
56
|
+
/** The route actually drawn, before trimming for the end markers. */
|
|
57
|
+
points: Vec[];
|
|
58
|
+
/** A point a fraction t (0..1) of the way along the route, by length. */
|
|
59
|
+
along(t: number): Vec;
|
|
60
|
+
}
|
|
61
|
+
/** The route between the two ends, as a polyline. */
|
|
62
|
+
export declare function route(from: Vec | Frame, to: Vec | Frame, kind?: 'straight' | 'elbow', gap?: number): Vec[];
|
|
63
|
+
/** Point a fraction t along a polyline, measured by length. */
|
|
64
|
+
export declare function pointAlong(pts: readonly Vec[], t: number): Vec;
|
|
65
|
+
export declare function connector(o: ConnectorOptions): ConnectorScene;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { path, text } from "./scene.js";
|
|
2
|
+
import { V2, vadd, vdist, vnorm, vperp, vscale, vsub } from "./vec.js";
|
|
3
|
+
const isFrame = (v) => "w" in v && "h" in v;
|
|
4
|
+
function rectEdge(f, toward) {
|
|
5
|
+
const dx = toward.x - f.x, dy = toward.y - f.y;
|
|
6
|
+
if (dx === 0 && dy === 0) return V2(f.x, f.y);
|
|
7
|
+
const tx = dx === 0 ? Infinity : f.w / 2 / Math.abs(dx);
|
|
8
|
+
const ty = dy === 0 ? Infinity : f.h / 2 / Math.abs(dy);
|
|
9
|
+
const t = Math.min(tx, ty);
|
|
10
|
+
return V2(f.x + dx * t, f.y + dy * t);
|
|
11
|
+
}
|
|
12
|
+
const frameOf = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2, w: b.w, h: b.h });
|
|
13
|
+
const MARKERS = {
|
|
14
|
+
arrow: { len: 11, half: 5, trim: 9 },
|
|
15
|
+
open: { len: 10, half: 6, trim: 0 },
|
|
16
|
+
triangle: { len: 14, half: 8, trim: 14 },
|
|
17
|
+
diamond: { len: 22, half: 7, trim: 22 },
|
|
18
|
+
"diamond-filled": { len: 22, half: 7, trim: 22 }
|
|
19
|
+
};
|
|
20
|
+
function route(from, to, kind = "straight", gap = 0) {
|
|
21
|
+
const c0 = V2(from.x, from.y), c1 = V2(to.x, to.y);
|
|
22
|
+
if (kind === "elbow" && isFrame(from) && isFrame(to)) {
|
|
23
|
+
const dx = c1.x - c0.x, dy = c1.y - c0.y;
|
|
24
|
+
if (Math.abs(dy) * 1.2 >= Math.abs(dx)) {
|
|
25
|
+
const s2 = Math.sign(dy) || 1;
|
|
26
|
+
const a3 = V2(c0.x, c0.y + s2 * from.h / 2), b3 = V2(c1.x, c1.y - s2 * to.h / 2);
|
|
27
|
+
if (Math.abs(a3.x - b3.x) < 1) return [a3, b3];
|
|
28
|
+
const my = (a3.y + b3.y) / 2;
|
|
29
|
+
return [a3, V2(a3.x, my), V2(b3.x, my), b3];
|
|
30
|
+
}
|
|
31
|
+
const s = Math.sign(dx) || 1;
|
|
32
|
+
const a2 = V2(c0.x + s * from.w / 2, c0.y), b2 = V2(c1.x - s * to.w / 2, c1.y);
|
|
33
|
+
if (Math.abs(a2.y - b2.y) < 1) return [a2, b2];
|
|
34
|
+
const mx = (a2.x + b2.x) / 2;
|
|
35
|
+
return [a2, V2(mx, a2.y), V2(mx, b2.y), b2];
|
|
36
|
+
}
|
|
37
|
+
const a = isFrame(from) ? rectEdge(from, c1) : vadd(c0, vscale(vnorm(vsub(c1, c0)), gap));
|
|
38
|
+
const b = isFrame(to) ? rectEdge(to, c0) : vadd(c1, vscale(vnorm(vsub(c0, c1)), gap));
|
|
39
|
+
return [a, b];
|
|
40
|
+
}
|
|
41
|
+
function pointAlong(pts, t) {
|
|
42
|
+
const lens = pts.slice(1).map((p, i) => vdist(pts[i], p));
|
|
43
|
+
let d = Math.max(0, Math.min(1, t)) * lens.reduce((s, l) => s + l, 0);
|
|
44
|
+
for (let i = 0; i < lens.length; i++) {
|
|
45
|
+
if (d <= lens[i] || i === lens.length - 1) {
|
|
46
|
+
const k = lens[i] ? d / lens[i] : 0;
|
|
47
|
+
return V2(pts[i].x + (pts[i + 1].x - pts[i].x) * k, pts[i].y + (pts[i + 1].y - pts[i].y) * k);
|
|
48
|
+
}
|
|
49
|
+
d -= lens[i];
|
|
50
|
+
}
|
|
51
|
+
return pts[0];
|
|
52
|
+
}
|
|
53
|
+
function marker(kind, tip, dir) {
|
|
54
|
+
const { len, half } = MARKERS[kind];
|
|
55
|
+
const back = (k) => vsub(tip, vscale(dir, k));
|
|
56
|
+
const side = vperp(dir);
|
|
57
|
+
const at = (k, s) => vadd(back(k), vscale(side, s));
|
|
58
|
+
switch (kind) {
|
|
59
|
+
case "arrow":
|
|
60
|
+
return { pts: [tip, at(len, half), at(len, -half)], closed: true, role: "end-solid" };
|
|
61
|
+
case "open":
|
|
62
|
+
return { pts: [at(len, half), tip, at(len, -half)], closed: false, role: "end-open" };
|
|
63
|
+
case "triangle":
|
|
64
|
+
return { pts: [tip, at(len, half), at(len, -half)], closed: true, role: "end-hollow" };
|
|
65
|
+
case "diamond":
|
|
66
|
+
case "diamond-filled":
|
|
67
|
+
return { pts: [tip, at(len / 2, half), back(len), at(len / 2, -half)], closed: true, role: kind === "diamond" ? "end-hollow" : "end-solid" };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function connector(o) {
|
|
71
|
+
const pts = o.via?.length ? [V2(o.from.x, o.from.y), ...o.via.map((p) => V2(p.x, p.y)), V2(o.to.x, o.to.y)] : route(o.from, o.to, o.route, o.gap ?? 0);
|
|
72
|
+
const start = o.start ?? "none", end = o.end ?? "arrow";
|
|
73
|
+
const n = pts.length;
|
|
74
|
+
const dirEnd = vnorm(vsub(pts[n - 1], pts[n - 2]));
|
|
75
|
+
const dirStart = vnorm(vsub(pts[0], pts[1]));
|
|
76
|
+
const line = pts.map((p) => V2(p.x, p.y));
|
|
77
|
+
if (end !== "none") line[n - 1] = vsub(pts[n - 1], vscale(dirEnd, MARKERS[end].trim));
|
|
78
|
+
if (start !== "none") line[0] = vsub(pts[0], vscale(dirStart, MARKERS[start].trim));
|
|
79
|
+
const common = { tone: o.tone, opacity: o.opacity };
|
|
80
|
+
const nodes = [
|
|
81
|
+
path({ id: `${o.id}:line`, points: line.map((p) => [p.x, p.y]), role: "connector", dashed: o.dashed, width: o.width ?? 1.5, ...common })
|
|
82
|
+
];
|
|
83
|
+
for (const [k, kind, tip, dir] of [["start", start, pts[0], dirStart], ["end", end, pts[n - 1], dirEnd]]) {
|
|
84
|
+
if (kind === "none") continue;
|
|
85
|
+
const m = marker(kind, tip, dir);
|
|
86
|
+
nodes.push(path({ id: `${o.id}:${k}`, points: m.pts.map((p) => [p.x, p.y]), closed: m.closed, role: m.role, width: o.width ?? 1.5, ...common }));
|
|
87
|
+
}
|
|
88
|
+
if (o.label) {
|
|
89
|
+
const m = pointAlong(pts, 0.5);
|
|
90
|
+
nodes.push(text({ id: `${o.id}:label`, x: m.x, y: m.y, text: o.label, size: 12, role: "edge-label", ...common }));
|
|
91
|
+
}
|
|
92
|
+
const endLabel = (k, s, tip, dir) => {
|
|
93
|
+
if (!s) return;
|
|
94
|
+
const p = vadd(vsub(tip, vscale(dir, 20)), vscale(vperp(dir), 11));
|
|
95
|
+
nodes.push(text({ id: `${o.id}:${k}-label`, x: p.x, y: p.y, text: s, size: 11, role: "end-label", ...common }));
|
|
96
|
+
};
|
|
97
|
+
endLabel("start", o.startLabel, pts[0], dirStart);
|
|
98
|
+
endLabel("end", o.endLabel, pts[n - 1], dirEnd);
|
|
99
|
+
return { nodes, points: pts, along: (t) => pointAlong(pts, t) };
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
connector,
|
|
103
|
+
frameOf,
|
|
104
|
+
pointAlong,
|
|
105
|
+
rectEdge,
|
|
106
|
+
route
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=connector.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/connector.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Connectors: a line between two boxes (or points) with a decoration at either\n * end. This is what UML relationships and architecture links are made of.\n *\n * \u2500\u2500\u2500\u2500\u2500\u25B7 'triangle' hollow triangle (inheritance, realisation)\n * \u2500\u2500\u2500\u2500\u2500\u25B6 'arrow' solid triangle (a message, a request)\n * \u2500\u2500\u2500\u2500\u2500> 'open' open chevron (association, dependency, async)\n * \u25C7\u2500\u2500\u2500\u2500\u2500 'diamond' hollow diamond (aggregation, at the owner)\n * \u25C6\u2500\u2500\u2500\u2500\u2500 'diamond-filled' solid diamond (composition, at the owner)\n *\n * Each end marker is its own small closed path with a role (`end-solid`,\n * `end-hollow`, `end-open`), so the stylesheet decides how it looks. The line\n * stops where the marker begins, so it never shows through a hollow head.\n */\nimport { Box, Scene, SceneNode, Tone, path, text } from './scene';\nimport { V2, Vec, vadd, vdist, vnorm, vperp, vscale, vsub } from './vec';\n\nexport type ConnectorEnd = 'none' | 'arrow' | 'open' | 'triangle' | 'diamond' | 'diamond-filled';\n\n/** A box given by its CENTRE and size, like a rect node. */\nexport interface Frame {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\nconst isFrame = (v: Vec | Frame): v is Frame => 'w' in v && 'h' in v;\n\n/**\n * Where the ray from a box's centre toward `toward` leaves the box.\n * The ray is centre + t\u00B7d. It hits the vertical sides at t = (w/2)/|dx| and the\n * horizontal sides at t = (h/2)/|dy|; whichever comes first is the exit.\n */\nexport function rectEdge(f: Frame, toward: Vec): Vec {\n const dx = toward.x - f.x, dy = toward.y - f.y;\n if (dx === 0 && dy === 0) return V2(f.x, f.y);\n const tx = dx === 0 ? Infinity : f.w / 2 / Math.abs(dx);\n const ty = dy === 0 ? Infinity : f.h / 2 / Math.abs(dy);\n const t = Math.min(tx, ty);\n return V2(f.x + dx * t, f.y + dy * t);\n}\n\n/** A centred Frame from a top-left Box. */\nexport const frameOf = (b: Box): Frame => ({ x: b.x + b.w / 2, y: b.y + b.h / 2, w: b.w, h: b.h });\n\nexport interface ConnectorOptions {\n id: string;\n from: Vec | Frame;\n to: Vec | Frame;\n /** 'straight' (default) or 'elbow': right-angled, leaving and entering through the facing sides. */\n route?: 'straight' | 'elbow';\n start?: ConnectorEnd;\n end?: ConnectorEnd;\n dashed?: boolean;\n tone?: Tone;\n width?: number;\n /** Text at the middle of the line. */\n label?: string;\n /** Small text near each end (UML multiplicities like '1' and '0..*'). */\n startLabel?: string;\n endLabel?: string;\n /** Points to pass through on the way (overrides `route`), e.g. a self-message loop. */\n via?: readonly Vec[];\n /** Gap left between the line and a point endpoint (boxes already stop at their edge). */\n gap?: number;\n opacity?: number;\n}\n\nexport interface ConnectorScene extends Scene {\n /** The route actually drawn, before trimming for the end markers. */\n points: Vec[];\n /** A point a fraction t (0..1) of the way along the route, by length. */\n along(t: number): Vec;\n}\n\n/** Marker length (how far back along the line it reaches) and half-width. */\nconst MARKERS: Record<Exclude<ConnectorEnd, 'none'>, { len: number; half: number; trim: number }> = {\n arrow: { len: 11, half: 5, trim: 9 },\n open: { len: 10, half: 6, trim: 0 },\n triangle: { len: 14, half: 8, trim: 14 },\n diamond: { len: 22, half: 7, trim: 22 },\n 'diamond-filled': { len: 22, half: 7, trim: 22 },\n};\n\n/** The route between the two ends, as a polyline. */\nexport function route(from: Vec | Frame, to: Vec | Frame, kind: 'straight' | 'elbow' = 'straight', gap = 0): Vec[] {\n const c0 = V2(from.x, from.y), c1 = V2(to.x, to.y);\n if (kind === 'elbow' && isFrame(from) && isFrame(to)) {\n const dx = c1.x - c0.x, dy = c1.y - c0.y;\n // Leave through the sides that face each other. Mostly-vertical pairs (a\n // class and its parent) go top/bottom, so inheritance trees read top-down.\n if (Math.abs(dy) * 1.2 >= Math.abs(dx)) {\n const s = Math.sign(dy) || 1;\n const a = V2(c0.x, c0.y + (s * from.h) / 2), b = V2(c1.x, c1.y - (s * to.h) / 2);\n if (Math.abs(a.x - b.x) < 1) return [a, b];\n const my = (a.y + b.y) / 2;\n return [a, V2(a.x, my), V2(b.x, my), b];\n }\n const s = Math.sign(dx) || 1;\n const a = V2(c0.x + (s * from.w) / 2, c0.y), b = V2(c1.x - (s * to.w) / 2, c1.y);\n if (Math.abs(a.y - b.y) < 1) return [a, b];\n const mx = (a.x + b.x) / 2;\n return [a, V2(mx, a.y), V2(mx, b.y), b];\n }\n const a = isFrame(from) ? rectEdge(from, c1) : vadd(c0, vscale(vnorm(vsub(c1, c0)), gap));\n const b = isFrame(to) ? rectEdge(to, c0) : vadd(c1, vscale(vnorm(vsub(c0, c1)), gap));\n return [a, b];\n}\n\n/** Point a fraction t along a polyline, measured by length. */\nexport function pointAlong(pts: readonly Vec[], t: number): Vec {\n const lens = pts.slice(1).map((p, i) => vdist(pts[i], p));\n let d = Math.max(0, Math.min(1, t)) * lens.reduce((s, l) => s + l, 0);\n for (let i = 0; i < lens.length; i++) {\n if (d <= lens[i] || i === lens.length - 1) {\n const k = lens[i] ? d / lens[i] : 0;\n return V2(pts[i].x + (pts[i + 1].x - pts[i].x) * k, pts[i].y + (pts[i + 1].y - pts[i].y) * k);\n }\n d -= lens[i];\n }\n return pts[0];\n}\n\n/**\n * The marker at `tip`, pointing along `dir` (unit vector, pointing OUT of the line).\n * Returns the outline and how much of the line it covers.\n */\nfunction marker(kind: Exclude<ConnectorEnd, 'none'>, tip: Vec, dir: Vec): { pts: Vec[]; closed: boolean; role: string } {\n const { len, half } = MARKERS[kind];\n const back = (k: number) => vsub(tip, vscale(dir, k));\n const side = vperp(dir);\n const at = (k: number, s: number) => vadd(back(k), vscale(side, s));\n switch (kind) {\n case 'arrow':\n return { pts: [tip, at(len, half), at(len, -half)], closed: true, role: 'end-solid' };\n case 'open':\n return { pts: [at(len, half), tip, at(len, -half)], closed: false, role: 'end-open' };\n case 'triangle':\n return { pts: [tip, at(len, half), at(len, -half)], closed: true, role: 'end-hollow' };\n case 'diamond':\n case 'diamond-filled':\n return { pts: [tip, at(len / 2, half), back(len), at(len / 2, -half)], closed: true, role: kind === 'diamond' ? 'end-hollow' : 'end-solid' };\n }\n}\n\nexport function connector(o: ConnectorOptions): ConnectorScene {\n const pts = o.via?.length\n ? [V2(o.from.x, o.from.y), ...o.via.map((p) => V2(p.x, p.y)), V2(o.to.x, o.to.y)]\n : route(o.from, o.to, o.route, o.gap ?? 0);\n const start = o.start ?? 'none', end = o.end ?? 'arrow';\n const n = pts.length;\n const dirEnd = vnorm(vsub(pts[n - 1], pts[n - 2]));\n const dirStart = vnorm(vsub(pts[0], pts[1]));\n // trim the line so it stops where each marker begins\n const line = pts.map((p) => V2(p.x, p.y));\n if (end !== 'none') line[n - 1] = vsub(pts[n - 1], vscale(dirEnd, MARKERS[end].trim));\n if (start !== 'none') line[0] = vsub(pts[0], vscale(dirStart, MARKERS[start].trim));\n\n const common = { tone: o.tone, opacity: o.opacity };\n const nodes: SceneNode[] = [\n path({ id: `${o.id}:line`, points: line.map((p) => [p.x, p.y] as const), role: 'connector', dashed: o.dashed, width: o.width ?? 1.5, ...common }),\n ];\n for (const [k, kind, tip, dir] of [['start', start, pts[0], dirStart], ['end', end, pts[n - 1], dirEnd]] as const) {\n if (kind === 'none') continue;\n const m = marker(kind, tip, dir);\n nodes.push(path({ id: `${o.id}:${k}`, points: m.pts.map((p) => [p.x, p.y] as const), closed: m.closed, role: m.role, width: o.width ?? 1.5, ...common }));\n }\n if (o.label) {\n const m = pointAlong(pts, 0.5);\n nodes.push(text({ id: `${o.id}:label`, x: m.x, y: m.y, text: o.label, size: 12, role: 'edge-label', ...common }));\n }\n // multiplicities sit just off the line, a little way in from each end\n const endLabel = (k: string, s: string | undefined, tip: Vec, dir: Vec) => {\n if (!s) return;\n const p = vadd(vsub(tip, vscale(dir, 20)), vscale(vperp(dir), 11));\n nodes.push(text({ id: `${o.id}:${k}-label`, x: p.x, y: p.y, text: s, size: 11, role: 'end-label', ...common }));\n };\n endLabel('start', o.startLabel, pts[0], dirStart);\n endLabel('end', o.endLabel, pts[n - 1], dirEnd);\n return { nodes, points: pts, along: (t) => pointAlong(pts, t) };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["s", "a", "b"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a node into SVG, split into two halves:
|
|
3
|
+
*
|
|
4
|
+
* - numericProps(node): the numbers that can be tweened (x, y, r, opacity…)
|
|
5
|
+
* - describe(node, props): which SVG tag + attributes to draw for those numbers
|
|
6
|
+
*
|
|
7
|
+
* The DOM renderer calls describe() every animation frame with interpolated
|
|
8
|
+
* props; the string renderer calls it once with the final props. Sharing this
|
|
9
|
+
* one function means both renderers can never disagree about what a node looks like.
|
|
10
|
+
*/
|
|
11
|
+
import { NumericProps, SceneNode } from './scene';
|
|
12
|
+
import { Geometry } from './themes';
|
|
13
|
+
import { Vec } from './vec';
|
|
14
|
+
/** Path points live in the flat numeric bag as p0, p1, … (x0 y0 x1 y1 …) so the tween engine needs no special case. */
|
|
15
|
+
export declare function numericProps(n: SceneNode): NumericProps;
|
|
16
|
+
export type SvgTag = 'circle' | 'rect' | 'text' | 'path';
|
|
17
|
+
export interface Description {
|
|
18
|
+
tag: SvgTag;
|
|
19
|
+
attrs: Record<string, string>;
|
|
20
|
+
text?: string;
|
|
21
|
+
/** Inner SVG markup instead of plain text (maths labels become <tspan>s). */
|
|
22
|
+
html?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const tagFor: (kind: SceneNode['kind']) => SvgTag;
|
|
25
|
+
/** Read the flat bag back into points. */
|
|
26
|
+
export declare function pointsOf(p: NumericProps, count: number): Array<[number, number]>;
|
|
27
|
+
export declare function classFor(n: SceneNode): string;
|
|
28
|
+
export declare function describe(n: SceneNode, p: NumericProps, g?: Geometry): Description;
|
|
29
|
+
/** Control point of a bent line: midpoint pushed `bend` px sideways. */
|
|
30
|
+
export declare function bendControl(a: Vec, b: Vec, bend: number): Vec;
|
|
31
|
+
/**
|
|
32
|
+
* Path data for a (possibly curved) line plus its arrowhead, as ONE path so a
|
|
33
|
+
* single element can be tweened.
|
|
34
|
+
*
|
|
35
|
+
* - 'chevron': an open "V" — stroke only.
|
|
36
|
+
* - 'triangle': a closed triangle that CSS fills (class dv-head-solid).
|
|
37
|
+
* Filling a path fills EVERY sub-path, and an open curve "M a Q c b" would
|
|
38
|
+
* fill the lens between the curve and its chord. So the line part retraces
|
|
39
|
+
* itself back to the start: a shape that goes there and back encloses zero
|
|
40
|
+
* area, so only the triangle gets filled. (A straight line has zero area anyway.)
|
|
41
|
+
*/
|
|
42
|
+
export declare function arrowPath(a: Vec, b: Vec, bend: number, head: boolean, width: number, style?: 'chevron' | 'triangle', headScale?: number): string;
|
|
43
|
+
/** Point at parameter t on the (possibly curved) line — handy for edge labels. */
|
|
44
|
+
export declare function pointOnArrow(a: Vec, b: Vec, bend: number, t: number): Vec;
|
package/dist/describe.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { geometry } from "./themes.js";
|
|
2
|
+
import { pathData } from "./path.js";
|
|
3
|
+
import { mathTspans, parseMath } from "./mathtext.js";
|
|
4
|
+
import { V2, vadd, vnorm, vperp, vscale, vsub } from "./vec.js";
|
|
5
|
+
function numericProps(n) {
|
|
6
|
+
const opacity = n.opacity ?? 1;
|
|
7
|
+
switch (n.kind) {
|
|
8
|
+
case "path": {
|
|
9
|
+
const o = { width: n.width ?? 1.5, opacity };
|
|
10
|
+
n.points.forEach(([x, y], i) => (o[`p${2 * i}`] = x, o[`p${2 * i + 1}`] = y));
|
|
11
|
+
return o;
|
|
12
|
+
}
|
|
13
|
+
case "circle":
|
|
14
|
+
return { x: n.x, y: n.y, r: n.r, opacity };
|
|
15
|
+
case "rect":
|
|
16
|
+
return { x: n.x, y: n.y, w: n.w, h: n.h, rx: n.rx ?? 0, opacity };
|
|
17
|
+
case "text":
|
|
18
|
+
return { x: n.x, y: n.y, size: n.size ?? 16, opacity };
|
|
19
|
+
case "arrow":
|
|
20
|
+
return { x1: n.x1, y1: n.y1, x2: n.x2, y2: n.y2, bend: n.bend ?? 0, width: n.width ?? 2, opacity };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const tagFor = (kind) => kind === "arrow" ? "path" : kind;
|
|
24
|
+
function pointsOf(p, count) {
|
|
25
|
+
return Array.from({ length: count }, (_, i) => [p[`p${2 * i}`], p[`p${2 * i + 1}`]]);
|
|
26
|
+
}
|
|
27
|
+
function classFor(n) {
|
|
28
|
+
const c = ["dv", `dv-${n.kind}`, `dv-tone-${n.tone ?? "default"}`];
|
|
29
|
+
if (n.role) c.push(`dv-role-${n.role}`);
|
|
30
|
+
if (n.kind === "path" && !n.closed) c.push("dv-open");
|
|
31
|
+
if (n.dashed) c.push("dv-dashed");
|
|
32
|
+
if (n.draggable) c.push("dv-draggable");
|
|
33
|
+
if (n.interactive || n.draggable) c.push("dv-interactive");
|
|
34
|
+
return c.join(" ");
|
|
35
|
+
}
|
|
36
|
+
const f = (v) => String(Math.round(v * 100) / 100);
|
|
37
|
+
function describe(n, p, g = geometry) {
|
|
38
|
+
const solidHead = n.kind === "arrow" && (n.head ?? true) && g.head === "triangle";
|
|
39
|
+
const attrs = {
|
|
40
|
+
class: classFor(n) + (solidHead ? " dv-head-solid" : ""),
|
|
41
|
+
"data-id": n.id,
|
|
42
|
+
opacity: f(p.opacity),
|
|
43
|
+
// draggable things are reachable with Tab and movable with the arrow keys
|
|
44
|
+
...n.draggable ? { tabindex: "0" } : {}
|
|
45
|
+
};
|
|
46
|
+
switch (n.kind) {
|
|
47
|
+
case "circle":
|
|
48
|
+
return { tag: "circle", attrs: { ...attrs, cx: f(p.x), cy: f(p.y), r: f(Math.max(0, p.r)) } };
|
|
49
|
+
case "rect":
|
|
50
|
+
return {
|
|
51
|
+
tag: "rect",
|
|
52
|
+
attrs: {
|
|
53
|
+
...attrs,
|
|
54
|
+
x: f(p.x - p.w / 2),
|
|
55
|
+
y: f(p.y - p.h / 2),
|
|
56
|
+
width: f(Math.max(0, p.w)),
|
|
57
|
+
height: f(Math.max(0, p.h)),
|
|
58
|
+
rx: f(p.rx * g.rxScale)
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
case "text":
|
|
62
|
+
return {
|
|
63
|
+
tag: "text",
|
|
64
|
+
// role 'math' is typeset: italic variables, upright numbers, real superscripts
|
|
65
|
+
...n.role === "math" ? { html: mathTspans(parseMath(n.text), p.size) } : { text: n.text },
|
|
66
|
+
attrs: {
|
|
67
|
+
...attrs,
|
|
68
|
+
x: f(p.x),
|
|
69
|
+
y: f(p.y),
|
|
70
|
+
"font-size": f(p.size),
|
|
71
|
+
"text-anchor": n.anchor ?? "middle",
|
|
72
|
+
"dominant-baseline": "central",
|
|
73
|
+
...n.weight != null ? { "font-weight": String(n.weight) } : {}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
case "path":
|
|
77
|
+
return {
|
|
78
|
+
tag: "path",
|
|
79
|
+
attrs: { ...attrs, d: pathData(pointsOf(p, n.points.length), !!n.closed, !!n.smooth), "stroke-width": f(p.width) }
|
|
80
|
+
};
|
|
81
|
+
case "arrow": {
|
|
82
|
+
const w = p.width * g.strokeScale;
|
|
83
|
+
return {
|
|
84
|
+
tag: "path",
|
|
85
|
+
attrs: {
|
|
86
|
+
...attrs,
|
|
87
|
+
d: arrowPath(V2(p.x1, p.y1), V2(p.x2, p.y2), p.bend, n.head ?? true, w, g.head, g.headScale),
|
|
88
|
+
"stroke-width": f(w)
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function bendControl(a, b, bend) {
|
|
95
|
+
const mid = V2((a.x + b.x) / 2, (a.y + b.y) / 2);
|
|
96
|
+
return vadd(mid, vscale(vperp(vnorm(vsub(b, a))), bend));
|
|
97
|
+
}
|
|
98
|
+
function arrowPath(a, b, bend, head, width, style = "chevron", headScale = 1) {
|
|
99
|
+
const curved = Math.abs(bend) > 0.5;
|
|
100
|
+
const c = curved ? bendControl(a, b, bend) : null;
|
|
101
|
+
const dir = vnorm(vsub(b, c ?? a));
|
|
102
|
+
const hasHead = head && (dir.x !== 0 || dir.y !== 0);
|
|
103
|
+
const size = (6 + width * 1.5) * headScale;
|
|
104
|
+
const solid = hasHead && style === "triangle";
|
|
105
|
+
const end = solid ? vsub(b, vscale(dir, size * 0.8)) : b;
|
|
106
|
+
let d = c ? `M${f(a.x)} ${f(a.y)}Q${f(c.x)} ${f(c.y)} ${f(end.x)} ${f(end.y)}` : `M${f(a.x)} ${f(a.y)}L${f(end.x)} ${f(end.y)}`;
|
|
107
|
+
if (solid && c) d += `Q${f(c.x)} ${f(c.y)} ${f(a.x)} ${f(a.y)}`;
|
|
108
|
+
if (!hasHead) return d;
|
|
109
|
+
const back = vsub(b, vscale(dir, size));
|
|
110
|
+
const spread = solid ? 0.55 : 0.6;
|
|
111
|
+
const side = vscale(vperp(dir), size * spread);
|
|
112
|
+
const l = vadd(back, side), r = vsub(back, side);
|
|
113
|
+
d += solid ? `M${f(b.x)} ${f(b.y)}L${f(l.x)} ${f(l.y)}L${f(r.x)} ${f(r.y)}Z` : `M${f(l.x)} ${f(l.y)}L${f(b.x)} ${f(b.y)}L${f(r.x)} ${f(r.y)}`;
|
|
114
|
+
return d;
|
|
115
|
+
}
|
|
116
|
+
function pointOnArrow(a, b, bend, t) {
|
|
117
|
+
const c = bendControl(a, b, bend);
|
|
118
|
+
const u = 1 - t;
|
|
119
|
+
return V2(u * u * a.x + 2 * u * t * c.x + t * t * b.x, u * u * a.y + 2 * u * t * c.y + t * t * b.y);
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
arrowPath,
|
|
123
|
+
bendControl,
|
|
124
|
+
classFor,
|
|
125
|
+
describe,
|
|
126
|
+
numericProps,
|
|
127
|
+
pointOnArrow,
|
|
128
|
+
pointsOf,
|
|
129
|
+
tagFor
|
|
130
|
+
};
|
|
131
|
+
//# sourceMappingURL=describe.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/describe.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Turning a node into SVG, split into two halves:\n *\n * - numericProps(node): the numbers that can be tweened (x, y, r, opacity\u2026)\n * - describe(node, props): which SVG tag + attributes to draw for those numbers\n *\n * The DOM renderer calls describe() every animation frame with interpolated\n * props; the string renderer calls it once with the final props. Sharing this\n * one function means both renderers can never disagree about what a node looks like.\n */\nimport { NumericProps, SceneNode } from './scene';\nimport { Geometry, geometry } from './themes';\nimport { pathData } from './path';\nimport { mathTspans, parseMath } from './mathtext';\nimport { V2, Vec, vadd, vnorm, vperp, vscale, vsub } from './vec';\n\n/** Path points live in the flat numeric bag as p0, p1, \u2026 (x0 y0 x1 y1 \u2026) so the tween engine needs no special case. */\n\nexport function numericProps(n: SceneNode): NumericProps {\n const opacity = n.opacity ?? 1;\n switch (n.kind) {\n case 'path': {\n const o: NumericProps = { width: n.width ?? 1.5, opacity };\n n.points.forEach(([x, y], i) => ((o[`p${2 * i}`] = x), (o[`p${2 * i + 1}`] = y)));\n return o;\n }\n case 'circle':\n return { x: n.x, y: n.y, r: n.r, opacity };\n case 'rect':\n return { x: n.x, y: n.y, w: n.w, h: n.h, rx: n.rx ?? 0, opacity };\n case 'text':\n return { x: n.x, y: n.y, size: n.size ?? 16, opacity };\n case 'arrow':\n return { x1: n.x1, y1: n.y1, x2: n.x2, y2: n.y2, bend: n.bend ?? 0, width: n.width ?? 2, opacity };\n }\n}\n\nexport type SvgTag = 'circle' | 'rect' | 'text' | 'path';\n\nexport interface Description {\n tag: SvgTag;\n attrs: Record<string, string>;\n text?: string;\n /** Inner SVG markup instead of plain text (maths labels become <tspan>s). */\n html?: string;\n}\n\nexport const tagFor = (kind: SceneNode['kind']): SvgTag => (kind === 'arrow' ? 'path' : kind);\n\n/** Read the flat bag back into points. */\nexport function pointsOf(p: NumericProps, count: number): Array<[number, number]> {\n return Array.from({ length: count }, (_, i) => [p[`p${2 * i}`], p[`p${2 * i + 1}`]] as [number, number]);\n}\n\nexport function classFor(n: SceneNode): string {\n const c = ['dv', `dv-${n.kind}`, `dv-tone-${n.tone ?? 'default'}`];\n if (n.role) c.push(`dv-role-${n.role}`);\n if (n.kind === 'path' && !n.closed) c.push('dv-open');\n if (n.dashed) c.push('dv-dashed');\n if (n.draggable) c.push('dv-draggable');\n if (n.interactive || n.draggable) c.push('dv-interactive');\n return c.join(' ');\n}\n\n/** Round to 2 decimals so the DOM / SVG strings stay readable. */\nconst f = (v: number) => String(Math.round(v * 100) / 100);\n\nexport function describe(n: SceneNode, p: NumericProps, g: Geometry = geometry): Description {\n const solidHead = n.kind === 'arrow' && (n.head ?? true) && g.head === 'triangle';\n const attrs: Record<string, string> = {\n class: classFor(n) + (solidHead ? ' dv-head-solid' : ''),\n 'data-id': n.id,\n opacity: f(p.opacity),\n // draggable things are reachable with Tab and movable with the arrow keys\n ...(n.draggable ? { tabindex: '0' } : {}),\n };\n switch (n.kind) {\n case 'circle':\n return { tag: 'circle', attrs: { ...attrs, cx: f(p.x), cy: f(p.y), r: f(Math.max(0, p.r)) } };\n case 'rect':\n return {\n tag: 'rect',\n attrs: {\n ...attrs,\n x: f(p.x - p.w / 2),\n y: f(p.y - p.h / 2),\n width: f(Math.max(0, p.w)),\n height: f(Math.max(0, p.h)),\n rx: f(p.rx * g.rxScale),\n },\n };\n case 'text':\n return {\n tag: 'text',\n // role 'math' is typeset: italic variables, upright numbers, real superscripts\n ...(n.role === 'math' ? { html: mathTspans(parseMath(n.text), p.size) } : { text: n.text }),\n attrs: {\n ...attrs,\n x: f(p.x),\n y: f(p.y),\n 'font-size': f(p.size),\n 'text-anchor': n.anchor ?? 'middle',\n 'dominant-baseline': 'central',\n ...(n.weight != null ? { 'font-weight': String(n.weight) } : {}),\n },\n };\n case 'path':\n return {\n tag: 'path',\n attrs: { ...attrs, d: pathData(pointsOf(p, n.points.length), !!n.closed, !!n.smooth), 'stroke-width': f(p.width) },\n };\n case 'arrow': {\n const w = p.width * g.strokeScale;\n return {\n tag: 'path',\n attrs: {\n ...attrs,\n d: arrowPath(V2(p.x1, p.y1), V2(p.x2, p.y2), p.bend, n.head ?? true, w, g.head, g.headScale),\n 'stroke-width': f(w),\n },\n };\n }\n }\n}\n\n/** Control point of a bent line: midpoint pushed `bend` px sideways. */\nexport function bendControl(a: Vec, b: Vec, bend: number): Vec {\n const mid = V2((a.x + b.x) / 2, (a.y + b.y) / 2);\n return vadd(mid, vscale(vperp(vnorm(vsub(b, a))), bend));\n}\n\n/**\n * Path data for a (possibly curved) line plus its arrowhead, as ONE path so a\n * single element can be tweened.\n *\n * - 'chevron': an open \"V\" \u2014 stroke only.\n * - 'triangle': a closed triangle that CSS fills (class dv-head-solid).\n * Filling a path fills EVERY sub-path, and an open curve \"M a Q c b\" would\n * fill the lens between the curve and its chord. So the line part retraces\n * itself back to the start: a shape that goes there and back encloses zero\n * area, so only the triangle gets filled. (A straight line has zero area anyway.)\n */\nexport function arrowPath(\n a: Vec,\n b: Vec,\n bend: number,\n head: boolean,\n width: number,\n style: 'chevron' | 'triangle' = 'chevron',\n headScale = 1,\n): string {\n const curved = Math.abs(bend) > 0.5;\n const c = curved ? bendControl(a, b, bend) : null;\n // A quadratic curve arrives along (end - control), a straight line along (end - start).\n const dir = vnorm(vsub(b, c ?? a));\n const hasHead = head && (dir.x !== 0 || dir.y !== 0);\n const size = (6 + width * 1.5) * headScale;\n const solid = hasHead && style === 'triangle';\n // A solid head covers the last bit of the line, so stop the line at its base.\n const end = solid ? vsub(b, vscale(dir, size * 0.8)) : b;\n\n let d = c ? `M${f(a.x)} ${f(a.y)}Q${f(c.x)} ${f(c.y)} ${f(end.x)} ${f(end.y)}` : `M${f(a.x)} ${f(a.y)}L${f(end.x)} ${f(end.y)}`;\n if (solid && c) d += `Q${f(c.x)} ${f(c.y)} ${f(a.x)} ${f(a.y)}`; // retrace: zero fill area\n if (!hasHead) return d;\n\n const back = vsub(b, vscale(dir, size));\n const spread = solid ? 0.55 : 0.6;\n const side = vscale(vperp(dir), size * spread);\n const l = vadd(back, side), r = vsub(back, side);\n d += solid\n ? `M${f(b.x)} ${f(b.y)}L${f(l.x)} ${f(l.y)}L${f(r.x)} ${f(r.y)}Z`\n : `M${f(l.x)} ${f(l.y)}L${f(b.x)} ${f(b.y)}L${f(r.x)} ${f(r.y)}`;\n return d;\n}\n\n/** Point at parameter t on the (possibly curved) line \u2014 handy for edge labels. */\nexport function pointOnArrow(a: Vec, b: Vec, bend: number, t: number): Vec {\n const c = bendControl(a, b, bend);\n const u = 1 - t;\n return V2(u * u * a.x + 2 * u * t * c.x + t * t * b.x, u * u * a.y + 2 * u * t * c.y + t * t * b.y);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arrays: a row of cells, with index labels underneath.
|
|
3
|
+
*
|
|
4
|
+
* The important design decision is WHAT A CELL'S ID IS:
|
|
5
|
+
* - the cell box + value text are identified by the element's `key`
|
|
6
|
+
* (default: its index). Give elements stable keys and a swap becomes two
|
|
7
|
+
* cells sliding past each other instead of two numbers changing in place.
|
|
8
|
+
* - the index labels (0, 1, 2…) are identified by POSITION, because index 3
|
|
9
|
+
* is always index 3 no matter what value sits in it.
|
|
10
|
+
*/
|
|
11
|
+
import { Part, Scene, Tone } from '../scene';
|
|
12
|
+
import { Vec } from '../vec';
|
|
13
|
+
import { Key, ToneSpec } from './common';
|
|
14
|
+
export interface ArrayOptions<T = unknown> {
|
|
15
|
+
id?: string;
|
|
16
|
+
/** Top-left corner of the first cell. */
|
|
17
|
+
x?: number;
|
|
18
|
+
y?: number;
|
|
19
|
+
/** Cell size in px. Default 48. */
|
|
20
|
+
cell?: number;
|
|
21
|
+
gap?: number;
|
|
22
|
+
/** Stable identity per element (must be unique). Default: the index. */
|
|
23
|
+
keys?: Key[];
|
|
24
|
+
/** Tone per INDEX: `{ 2: 'active' }` or `(i, v) => …`. */
|
|
25
|
+
tones?: ToneSpec<number, T>;
|
|
26
|
+
showIndices?: boolean;
|
|
27
|
+
/** Text to show instead of String(value). */
|
|
28
|
+
format?: (v: T, i: number) => string;
|
|
29
|
+
/** Lay the cells out top-to-bottom instead of left-to-right. */
|
|
30
|
+
vertical?: boolean;
|
|
31
|
+
/** Where new cells fly in from / old cells fly out to (offset in px). */
|
|
32
|
+
enterOffset?: Vec;
|
|
33
|
+
exitOffset?: Vec;
|
|
34
|
+
interactive?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ArrayDiagram extends Scene {
|
|
37
|
+
length: number;
|
|
38
|
+
cell: number;
|
|
39
|
+
center(i: number): Vec;
|
|
40
|
+
top(i: number): Vec;
|
|
41
|
+
bottom(i: number): Vec;
|
|
42
|
+
left(i: number): Vec;
|
|
43
|
+
right(i: number): Vec;
|
|
44
|
+
/** A highlight band behind cells lo..hi (inclusive). Put it BEFORE the array in combine(). */
|
|
45
|
+
window(lo: number, hi: number, o?: {
|
|
46
|
+
id?: string;
|
|
47
|
+
tone?: Tone;
|
|
48
|
+
pad?: number;
|
|
49
|
+
}): Part;
|
|
50
|
+
}
|
|
51
|
+
export declare function array<T>(values: readonly T[], o?: ArrayOptions<T>): ArrayDiagram;
|