@simplex-viz/svg 0.1.2 → 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 +83 -56
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,3 +1,7 @@
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
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.
@@ -6,7 +10,15 @@ Framework-agnostic SVG rendering engine and pluggable `SvgNodeView` architecture
6
10
  - **Source & Monorepo:** <https://github.com/ghackenberg/simplex-viz/tree/main/packages/svg>
7
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)
8
12
 
9
- ---
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()`.*
10
22
 
11
23
  ## Features
12
24
 
@@ -16,27 +28,26 @@ Framework-agnostic SVG rendering engine and pluggable `SvgNodeView` architecture
16
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.
17
29
  - **Four built-in view plugins**: Ready-to-use views for geometric shape morphing, multi-sector topic glyphs, vertex anchors, and dynamic text badges.
18
30
 
19
- ---
20
-
21
31
  ## Installation
22
32
 
33
+ Install `@simplex-viz/core` alongside `@simplex-viz/svg` in any browser or TypeScript project:
34
+
23
35
  ```bash
24
36
  npm install @simplex-viz/core @simplex-viz/svg
25
37
  ```
26
38
 
27
- ---
28
-
29
39
  ## Quickstart (Vanilla TypeScript)
30
40
 
41
+ ### Step 1 — Define Your Domain Record & `VisualResolver`
42
+
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
+
31
45
  ```ts
32
- import {
33
- SimplexSvgController,
34
- GeometricShapeSvgView,
35
- ClusterAnchorSvgView,
36
- type GeometricShapeVisual,
37
- type ClusterAnchorVisual,
38
- } from '@simplex-viz/svg';
39
46
  import type { VisualResolver } from '@simplex-viz/core';
47
+ import type {
48
+ ClusterAnchorVisual,
49
+ GeometricShapeVisual,
50
+ } from '@simplex-viz/svg';
40
51
 
41
52
  interface Bookmark {
42
53
  readonly url: string;
@@ -69,6 +80,18 @@ const visualResolver: VisualResolver<
69
80
  ? `${visual.shape}:${visual.radius}:${visual.fillHex}`
70
81
  : `${visual.code}:${visual.memberCount}:${visual.colorHex}`,
71
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:
88
+
89
+ ```ts
90
+ import {
91
+ SimplexSvgController,
92
+ GeometricShapeSvgView,
93
+ ClusterAnchorSvgView,
94
+ } from '@simplex-viz/svg';
72
95
 
73
96
  const container = document.getElementById('graph')!;
74
97
 
@@ -79,41 +102,45 @@ const controller = new SimplexSvgController(container, {
79
102
  visualResolver,
80
103
  contentView: new GeometricShapeSvgView(),
81
104
  clusterView: new ClusterAnchorSvgView(),
82
- meshOptions: {
83
- innerPadding: 48,
84
- minSideLength: 140,
85
- maxSideLength: 280,
86
- maxClusters: 10,
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}`);
87
109
  },
88
- layoutOptions: {
89
- clusterGapPx: 14,
90
- itemGapPx: 5,
110
+ });
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);
91
121
  },
92
- interactions: {
93
- onContentHover: (ev) => {
94
- if (ev) console.log('Hovered item:', ev.item.title, ev.screenPosition);
95
- },
96
- onLeftClick: (target) => {
97
- if (target.kind === 'content') {
98
- console.log('Clicked:', target.item.title);
99
- } else if (target.kind === 'cluster') {
100
- controller.setFocusedClusterUid(target.cluster.clusterUid);
101
- } else {
102
- controller.setFocusedClusterUid(null);
103
- }
104
- },
105
- onRightClick: (target) => {
106
- console.log('Context menu target:', target.kind);
107
- return true; // Suppress browser default context menu
108
- },
109
- onWheel: 'zoom-to-cursor',
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
+ }
110
130
  },
111
- onMetricsChange: (metrics) => {
112
- console.log(`Clusters: ${metrics.clusterCount}, Overlaps: ${metrics.overlapCount}`);
131
+ onRightClick: (target) => {
132
+ console.log('Context menu target:', target.kind);
133
+ return true; // Suppress browser default context menu
113
134
  },
135
+ onWheel: 'zoom-to-cursor',
114
136
  });
137
+ ```
115
138
 
116
- // Live mutations & camera controls
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
117
144
  controller.addItems({
118
145
  url: 'https://example.com/oklab',
119
146
  title: 'Perceptual Color Mixing',
@@ -123,16 +150,14 @@ controller.addItems({
123
150
  controller.removeItems('https://example.com/old');
124
151
  controller.camera.reset();
125
152
 
126
- // Clean up DOM, ResizeObserver, and animation loop on unmount
153
+ // Call on component unmount / teardown:
127
154
  // controller.destroy();
128
155
  ```
129
156
 
130
- ---
131
-
132
157
  ## Built-In `SvgNodeView` Plugins
133
158
 
134
159
  ### 1. `GeometricShapeSvgView`
135
- Renders `'circle' | 'diamond' | 'star' | 'hexagon'` with analytical convex hulls and **64-point parametric SVG path morphing** between shapes and colors.
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:
136
161
 
137
162
  ```ts
138
163
  import type { GeometricShapeVisual } from '@simplex-viz/svg';
@@ -148,7 +173,7 @@ const visual: GeometricShapeVisual = {
148
173
  ```
149
174
 
150
175
  ### 2. `MultiSectorGlyphSvgView`
151
- 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).
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:
152
177
 
153
178
  ```ts
154
179
  import type { MultiSectorGlyphVisual } from '@simplex-viz/svg';
@@ -165,7 +190,7 @@ const visual: MultiSectorGlyphVisual = {
165
190
  ```
166
191
 
167
192
  ### 3. `BadgeClusterSvgView`
168
- Renders dynamic pill/card cluster badges sized to their title and metadata line. Intentionally omits `measureConvexHull` so `@simplex-viz/svg` measures the rendered SVG element via `getBBox()`.
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:
169
194
 
170
195
  ```ts
171
196
  import type { BadgeClusterVisual } from '@simplex-viz/svg';
@@ -179,7 +204,7 @@ const visual: BadgeClusterVisual = {
179
204
  ```
180
205
 
181
206
  ### 4. `ClusterAnchorSvgView`
182
- Renders calibrated double-ring vertex anchors showing the cluster code and member count.
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:
183
208
 
184
209
  ```ts
185
210
  import type { ClusterAnchorVisual } from '@simplex-viz/svg';
@@ -193,12 +218,12 @@ const visual: ClusterAnchorVisual = {
193
218
  };
194
219
  ```
195
220
 
196
- ---
197
-
198
221
  ## Writing Custom `SvgNodeView` Plugins
199
222
 
200
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>`.
201
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
+
202
227
  ```ts
203
228
  import type { SvgNodeView } from '@simplex-viz/svg';
204
229
  import { rectToConvexPolygon } from '@simplex-viz/core';
@@ -208,18 +233,24 @@ interface CardVisual {
208
233
  readonly colorHex: string;
209
234
  }
210
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
211
242
  const SVG_NS = 'http://www.w3.org/2000/svg';
212
243
 
213
244
  export const CustomCardSvgView: SvgNodeView<CardVisual> = {
214
245
  // Optional: omit to measure automatically via offscreen getBBox()
215
246
  measureConvexHull: (visual) =>
216
- rectToConvexPolygon(Math.max(80, visual.title.length * 7 + 20), 28, 6),
247
+ rectToConvexPolygon(getCardWidth(visual.title), 28, 6),
217
248
 
218
249
  mountOrUpdate: (groupEl, visual, state) => {
219
250
  const doc = groupEl.ownerDocument;
220
251
  while (groupEl.firstChild) groupEl.removeChild(groupEl.firstChild);
221
252
 
222
- const width = Math.max(80, visual.title.length * 7 + 20);
253
+ const width = getCardWidth(visual.title);
223
254
  const rect = doc.createElementNS(SVG_NS, 'rect');
224
255
  rect.setAttribute('x', String(-width / 2));
225
256
  rect.setAttribute('y', '-14');
@@ -249,8 +280,6 @@ export const CustomCardSvgView: SvgNodeView<CardVisual> = {
249
280
  };
250
281
  ```
251
282
 
252
- ---
253
-
254
283
  ## `SimplexSvgController` API Summary
255
284
 
256
285
  | Method / Property | Description |
@@ -271,8 +300,6 @@ export const CustomCardSvgView: SvgNodeView<CardVisual> = {
271
300
  | `stepForTest(dtMs)` | Deterministically advances simulation time in unit/DOM tests. |
272
301
  | `destroy()` | Cancels `requestAnimationFrame`, disconnects `ResizeObserver`, and removes the `<svg>`. |
273
302
 
274
- ---
275
-
276
303
  ## License
277
304
 
278
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.2",
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.2"
23
+ "@simplex-viz/core": "^0.1.3"
24
24
  },
25
25
  "keywords": [
26
26
  "simplex",