@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.
- package/README.md +83 -56
- 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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
112
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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(
|
|
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 =
|
|
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.
|
|
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.
|
|
23
|
+
"@simplex-viz/core": "^0.1.3"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"simplex",
|