@simplex-viz/svg 0.1.1 → 0.1.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 (2) hide show
  1. package/README.md +278 -9
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,24 +1,90 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/ghackenberg/simplex-viz/main/docs/assets/logo-svg.png" width="640" alt="SimplexViz / svg — @simplex-viz/svg" />
3
+ </p>
4
+
1
5
  # @simplex-viz/svg
2
6
 
3
- Framework-agnostic SVG rendering engine and pluggable `SvgNodeView` architecture for `@simplex-viz/core`. Zero dependency on React—embeds cleanly into Vanilla TypeScript, Vue, Angular, Svelte, and Web Components.
7
+ Framework-agnostic SVG rendering engine and pluggable `SvgNodeView` architecture for `@simplex-viz/core`. Zero dependency on React—embeds cleanly into Vanilla TypeScript, Vue, Angular, Svelte, Solid, and Web Components.
4
8
 
5
9
  - **Live Demo:** <https://hackenberg.tech/simplex-viz/>
6
10
  - **Source & Monorepo:** <https://github.com/ghackenberg/simplex-viz/tree/main/packages/svg>
11
+ - **Companion Packages:** [`@simplex-viz/core`](https://www.npmjs.com/package/@simplex-viz/core) · [`@simplex-viz/react`](https://www.npmjs.com/package/@simplex-viz/react)
12
+
13
+ ## Visual Rendering Showcase
14
+
15
+ While `@simplex-viz/core` computes pure spatial coordinates, `@simplex-viz/svg` turns each frame into live DOM `<svg>` elements inside any browser container or UI framework. The preview below renders its built-in views together in a single viewport:
16
+
17
+ <p align="center">
18
+ <img src="https://raw.githubusercontent.com/ghackenberg/simplex-viz/main/docs/assets/preview-svg.png" width="100%" alt="@simplex-viz/svg rendering showcase showing GeometricShapeSvgView, MultiSectorGlyphSvgView, ClusterAnchorSvgView, and BadgeClusterSvgView" />
19
+ </p>
20
+
21
+ *Figure: Visual showcase of the four built-in `SvgNodeView` plugins rendered on an OKLab-tinted 60° simplex mesh. Content items mix `GeometricShapeSvgView` (circles, diamonds, hexagons, and stars that morph smoothly via 64-point paths) with `MultiSectorGlyphSvgView` (proportional donut glyphs whose sector arcs encode barycentric topic weights). Topic clusters alternate between compact `ClusterAnchorSvgView` vertex rings and auto-measured `BadgeClusterSvgView` cards sized via offscreen `getBBox()`.*
22
+
23
+ ## Features
24
+
25
+ - **Framework-agnostic SVG scene graph**: Keyed DOM reconciliation for background mesh triangles, highlighted edges, convex hull debug wireframes, cluster nodes, and content nodes.
26
+ - **Automatic offscreen `getBBox()` hull measurement**: Omit `measureConvexHull` on any `SvgNodeView` and `@simplex-viz/svg` automatically renders it in a hidden measurement bin, reads `getBBox()`, and caches a tight convex polygon for overlap-free layout.
27
+ - **Smooth 60 fps transitions**: Critically damped spring motion + specialized parametric `morph()` or automatic dual-layer crossfade when switching visual presets or mutating items.
28
+ - **Centralized root-SVG interactions**: Attaches pointer, click, right-click, wheel, drag-to-pan, and `ResizeObserver` listeners solely on the root `<svg>` element and delegates hit-testing to `@simplex-viz/core`'s exact convex geometry solver.
29
+ - **Four built-in view plugins**: Ready-to-use views for geometric shape morphing, multi-sector topic glyphs, vertex anchors, and dynamic text badges.
7
30
 
8
31
  ## Installation
9
32
 
33
+ Install `@simplex-viz/core` alongside `@simplex-viz/svg` in any browser or TypeScript project:
34
+
10
35
  ```bash
11
36
  npm install @simplex-viz/core @simplex-viz/svg
12
37
  ```
13
38
 
14
- ## Included `SvgNodeView` Plugins
39
+ ## Quickstart (Vanilla TypeScript)
15
40
 
16
- - `GeometricShapeSvgView` — Circle, rounded diamond, hexagon, and 5-point star with 64-point parametric SVG path morphing.
17
- - `MultiSectorGlyphSvgView` — Multi-colored topic weight donut glyphs representing exact barycentric weights.
18
- - `BadgeClusterSvgView` — Dynamic text badges with automatic offscreen `getBBox()` convex hull measurement.
19
- - `ClusterAnchorSvgView` — Calibrated double-ring vertex anchors with code & member count.
41
+ ### Step 1 — Define Your Domain Record & `VisualResolver`
20
42
 
21
- ## Quickstart (Vanilla TS)
43
+ Start by describing your data model (`Bookmark`) and mapping each item and topic cluster into visual descriptors for `GeometricShapeSvgView` and `ClusterAnchorSvgView`. `ctx.mixedColorHex` provides the item's barycentric OKLab color blend automatically:
44
+
45
+ ```ts
46
+ import type { VisualResolver } from '@simplex-viz/core';
47
+ import type {
48
+ ClusterAnchorVisual,
49
+ GeometricShapeVisual,
50
+ } from '@simplex-viz/svg';
51
+
52
+ interface Bookmark {
53
+ readonly url: string;
54
+ readonly title: string;
55
+ readonly topics: readonly string[];
56
+ readonly minutes: number;
57
+ }
58
+
59
+ const visualResolver: VisualResolver<
60
+ Bookmark,
61
+ GeometricShapeVisual,
62
+ ClusterAnchorVisual
63
+ > = {
64
+ resolveContentVisual: (bookmark, ctx) => ({
65
+ shape: bookmark.minutes > 10 ? 'star' : 'circle',
66
+ radius: 10,
67
+ fillHex: ctx.mixedColorHex,
68
+ strokeHex: '#07090E',
69
+ strokeWidth: 1.4,
70
+ }),
71
+ resolveClusterVisual: (cluster) => ({
72
+ code: cluster.code,
73
+ label: cluster.defaultLabel,
74
+ memberCount: cluster.members.length,
75
+ colorHex: cluster.colorHex,
76
+ radius: 22,
77
+ }),
78
+ getVisualCacheKey: (visual) =>
79
+ 'shape' in visual
80
+ ? `${visual.shape}:${visual.radius}:${visual.fillHex}`
81
+ : `${visual.code}:${visual.memberCount}:${visual.colorHex}`,
82
+ };
83
+ ```
84
+
85
+ ### Step 2 — Mount `SimplexSvgController`
86
+
87
+ Pass any container `HTMLElement` to `SimplexSvgController` along with `getId`, `getTags`, `visualResolver`, and the built-in views. The controller creates the root `<svg>`, attaches a `ResizeObserver`, computes the initial triangulation, and begins the 60 fps spring loop:
22
88
 
23
89
  ```ts
24
90
  import {
@@ -27,10 +93,213 @@ import {
27
93
  ClusterAnchorSvgView,
28
94
  } from '@simplex-viz/svg';
29
95
 
30
- const controller = new SimplexSvgController(containerEl, {
31
- items,
96
+ const container = document.getElementById('graph')!;
97
+
98
+ const controller = new SimplexSvgController(container, {
99
+ items: myBookmarks,
100
+ getId: (b) => b.url,
101
+ getTags: (b) => b.topics,
32
102
  visualResolver,
33
103
  contentView: new GeometricShapeSvgView(),
34
104
  clusterView: new ClusterAnchorSvgView(),
105
+ meshOptions: { innerPadding: 48, minSideLength: 140, maxSideLength: 280, maxClusters: 10 },
106
+ layoutOptions: { clusterGapPx: 14, itemGapPx: 5 },
107
+ onMetricsChange: (metrics) => {
108
+ console.log(`Clusters: ${metrics.clusterCount}, Overlaps: ${metrics.overlapCount}`);
109
+ },
35
110
  });
36
111
  ```
112
+
113
+ ### Step 3 — Wire Hover, Click & Wheel Interactions
114
+
115
+ Attach centralized pointer, click, context-menu, and wheel handlers via `setInteractions` (or pass `interactions` directly in the constructor). Listeners live exclusively on the root `<svg>`, while hit-testing uses exact convex hulls:
116
+
117
+ ```ts
118
+ controller.setInteractions({
119
+ onContentHover: (ev) => {
120
+ if (ev) console.log('Hovered item:', ev.item.title, ev.screenPosition);
121
+ },
122
+ onLeftClick: (target) => {
123
+ if (target.kind === 'content') {
124
+ console.log('Clicked:', target.item.title);
125
+ } else if (target.kind === 'cluster') {
126
+ controller.setFocusedClusterUid(target.cluster.clusterUid);
127
+ } else {
128
+ controller.setFocusedClusterUid(null);
129
+ }
130
+ },
131
+ onRightClick: (target) => {
132
+ console.log('Context menu target:', target.kind);
133
+ return true; // Suppress browser default context menu
134
+ },
135
+ onWheel: 'zoom-to-cursor',
136
+ });
137
+ ```
138
+
139
+ ### Step 4 — Apply Live Mutations & Clean Up
140
+
141
+ Mutate items or reset the camera smoothly at runtime without interrupting user navigation. Call `controller.destroy()` on unmount to stop animations and disconnect observers:
142
+
143
+ ```ts
144
+ controller.addItems({
145
+ url: 'https://example.com/oklab',
146
+ title: 'Perceptual Color Mixing',
147
+ topics: ['oklab', 'color-science'],
148
+ minutes: 12,
149
+ });
150
+ controller.removeItems('https://example.com/old');
151
+ controller.camera.reset();
152
+
153
+ // Call on component unmount / teardown:
154
+ // controller.destroy();
155
+ ```
156
+
157
+ ## Built-In `SvgNodeView` Plugins
158
+
159
+ ### 1. `GeometricShapeSvgView`
160
+ Renders `'circle' | 'diamond' | 'star' | 'hexagon'` with analytical convex hulls and **64-point parametric SVG path morphing** between shapes and colors. Pass a `GeometricShapeVisual` descriptor from `resolveContentVisual` to control polygon kind, radius, fill, and outline stroke:
161
+
162
+ ```ts
163
+ import type { GeometricShapeVisual } from '@simplex-viz/svg';
164
+
165
+ // Descriptor shape:
166
+ const visual: GeometricShapeVisual = {
167
+ shape: 'hexagon', // 'circle' | 'diamond' | 'star' | 'hexagon'
168
+ radius: 11,
169
+ fillHex: ctx.mixedColorHex,
170
+ strokeHex: '#07090E',
171
+ strokeWidth: 1.4,
172
+ };
173
+ ```
174
+
175
+ ### 2. `MultiSectorGlyphSvgView`
176
+ Renders a multi-colored donut glyph whose sector arc angles match an item's exact barycentric weights across its active simplex unit (vertex, edge, or triangle). Map `ctx.unitClusters` directly into `sectors` as shown below to reflect multi-topic membership visually:
177
+
178
+ ```ts
179
+ import type { MultiSectorGlyphVisual } from '@simplex-viz/svg';
180
+
181
+ const visual: MultiSectorGlyphVisual = {
182
+ outerRadius: 12,
183
+ innerRadius: 5,
184
+ sectors: ctx.unitClusters.map((uc) => ({
185
+ colorHex: uc.cluster.colorHex,
186
+ weight: uc.weight,
187
+ })),
188
+ centerColorHex: ctx.mixedColorHex,
189
+ };
190
+ ```
191
+
192
+ ### 3. `BadgeClusterSvgView`
193
+ Renders dynamic pill/card cluster badges sized to their title and metadata line. Because it intentionally omits `measureConvexHull`, `@simplex-viz/svg` measures the rendered `<svg>` elements via `getBBox()` so varying title lengths remain strictly overlap-free:
194
+
195
+ ```ts
196
+ import type { BadgeClusterVisual } from '@simplex-viz/svg';
197
+
198
+ const visual: BadgeClusterVisual = {
199
+ code: cluster.code,
200
+ title: cluster.defaultLabel,
201
+ metaLine: `${cluster.members.length} items`,
202
+ colorHex: cluster.colorHex,
203
+ };
204
+ ```
205
+
206
+ ### 4. `ClusterAnchorSvgView`
207
+ Renders calibrated double-ring vertex anchors showing the cluster code and member count. Supply a `ClusterAnchorVisual` descriptor when you want compact circular topic anchors on mesh vertices:
208
+
209
+ ```ts
210
+ import type { ClusterAnchorVisual } from '@simplex-viz/svg';
211
+
212
+ const visual: ClusterAnchorVisual = {
213
+ code: cluster.code,
214
+ label: cluster.defaultLabel,
215
+ memberCount: cluster.members.length,
216
+ colorHex: cluster.colorHex,
217
+ radius: 23,
218
+ };
219
+ ```
220
+
221
+ ## Writing Custom `SvgNodeView` Plugins
222
+
223
+ Implement `SvgNodeView<TVisual>` centered around local origin `(0, 0)`. `SvgRendererAdapter` handles world positioning `(x, y)`, solver scale `α`, opacity, and crossfading on the parent `<g>`.
224
+
225
+ First, define your visual descriptor and an optional analytical hull via `measureConvexHull` (if omitted, `@simplex-viz/svg` automatically measures the rendered SVG via offscreen `getBBox()`):
226
+
227
+ ```ts
228
+ import type { SvgNodeView } from '@simplex-viz/svg';
229
+ import { rectToConvexPolygon } from '@simplex-viz/core';
230
+
231
+ interface CardVisual {
232
+ readonly title: string;
233
+ readonly colorHex: string;
234
+ }
235
+
236
+ const getCardWidth = (title: string) => Math.max(80, title.length * 7 + 20);
237
+ ```
238
+
239
+ Next, populate `mountOrUpdate` to construct pure SVG nodes inside `groupEl`, reacting to `state.interaction.isHovered` for instant border emphasis:
240
+
241
+ ```ts
242
+ const SVG_NS = 'http://www.w3.org/2000/svg';
243
+
244
+ export const CustomCardSvgView: SvgNodeView<CardVisual> = {
245
+ // Optional: omit to measure automatically via offscreen getBBox()
246
+ measureConvexHull: (visual) =>
247
+ rectToConvexPolygon(getCardWidth(visual.title), 28, 6),
248
+
249
+ mountOrUpdate: (groupEl, visual, state) => {
250
+ const doc = groupEl.ownerDocument;
251
+ while (groupEl.firstChild) groupEl.removeChild(groupEl.firstChild);
252
+
253
+ const width = getCardWidth(visual.title);
254
+ const rect = doc.createElementNS(SVG_NS, 'rect');
255
+ rect.setAttribute('x', String(-width / 2));
256
+ rect.setAttribute('y', '-14');
257
+ rect.setAttribute('width', String(width));
258
+ rect.setAttribute('height', '28');
259
+ rect.setAttribute('rx', '6');
260
+ rect.setAttribute('fill', '#0d1320');
261
+ rect.setAttribute(
262
+ 'stroke',
263
+ state.interaction.isHovered ? '#f8fafc' : visual.colorHex,
264
+ );
265
+ rect.setAttribute('stroke-width', state.interaction.isHovered ? '2' : '1.2');
266
+
267
+ const text = doc.createElementNS(SVG_NS, 'text');
268
+ text.setAttribute('text-anchor', 'middle');
269
+ text.setAttribute('y', '4');
270
+ text.setAttribute('fill', '#f8fafc');
271
+ text.setAttribute('font-size', '11');
272
+ text.textContent = visual.title;
273
+
274
+ groupEl.appendChild(rect);
275
+ groupEl.appendChild(text);
276
+ },
277
+
278
+ // Optional: omit to use universal dual-layer crossfade
279
+ // morph: (groupEl, from, to, progress, state) => false,
280
+ };
281
+ ```
282
+
283
+ ## `SimplexSvgController` API Summary
284
+
285
+ | Method / Property | Description |
286
+ | --- | --- |
287
+ | `addItems(items)` | Appends one or more items and triggers an interruptible transition. |
288
+ | `removeItems(ids)` | Removes items by ID and smoothly fades out exiting nodes. |
289
+ | `setItems(items)` | Replaces the entire corpus while preserving matching clusters & items. |
290
+ | `setViews({ contentView, clusterView })` | Hot-swaps `SvgNodeView` plugins at runtime. |
291
+ | `setVisualResolver(resolver)` | Updates descriptor resolution and crossfades smoothly. |
292
+ | `setSizeMultiplier(multiplier)` | Uniformly scales node sizes (`0.35` – `3.0`) and re-solves zero overlap. |
293
+ | `setShowHulls(show)` | Toggles convex bounding hull wireframe overlays. |
294
+ | `setPinnedIds(ids)` / `setHighlightedIds(ids)` | Controls pinned and highlighted visual emphasis states. |
295
+ | `setFocusedClusterUid(uid \| null)` | Isolates a cluster and its members, dimming unrelated nodes. |
296
+ | `setInteractions(handlers)` | Replaces pointer, click, and wheel callbacks dynamically. |
297
+ | `camera` | Exposes `CameraController` (`zoomToCursor`, `panBy`, `reset`, `getState`, `setState`). |
298
+ | `getMetrics()` / `getClusters()` / `getItems()` | Reads live solver telemetry, clusters, and items. |
299
+ | `getSvgElement()` | Returns the managed root `<svg>` element. |
300
+ | `stepForTest(dtMs)` | Deterministically advances simulation time in unit/DOM tests. |
301
+ | `destroy()` | Cancels `requestAnimationFrame`, disconnects `ResizeObserver`, and removes the `<svg>`. |
302
+
303
+ ## License
304
+
305
+ MIT — see [LICENSE](https://github.com/ghackenberg/simplex-viz/blob/main/LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplex-viz/svg",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Framework-agnostic SVG rendering engine, offscreen getBBox() hull measurer, and pluggable SvgNodeView system for @simplex-viz/core.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "@simplex-viz/core": "^0.1.1"
23
+ "@simplex-viz/core": "^0.1.3"
24
24
  },
25
25
  "keywords": [
26
26
  "simplex",