vizcraft 0.2.2 → 1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # vizcraft
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [`73ee1fb`](https://github.com/ChipiKaf/vizcraft/commit/73ee1fbd204bf1ba0447c764eba2c1b9d6981ee5) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Added new shapes, connection ports, path based edges, Edge marker types, Multi-position edge labels, Containers and The overlay builder
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`c5ffe75`](https://github.com/ChipiKaf/vizcraft/commit/c5ffe7546a2e2148618db057c24aea01ecf097e0) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Add improved overlay api
14
+
3
15
  ## 0.2.2
4
16
 
5
17
  ### Patch Changes
package/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chipili Kafwilo
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
@@ -101,15 +101,54 @@ b.node('n1')
101
101
  .at(x, y) // Absolute position
102
102
  // OR
103
103
  .cell(col, row) // Grid position
104
- .circle(radius) // Shape definition
104
+ .circle(radius) // Circle shape
105
+ .rect(w, h, [rx]) // Rectangle (optional corner radius)
106
+ .diamond(w, h) // Diamond shape
107
+ .cylinder(w, h, [arcHeight]) // Cylinder (database symbol)
108
+ .hexagon(r, [orientation]) // Hexagon ('pointy' or 'flat')
109
+ .ellipse(rx, ry) // Ellipse / oval
110
+ .arc(r, start, end, [closed]) // Arc / pie slice
111
+ .blockArrow(len, bodyW, headW, headLen, [dir]) // Block arrow
112
+ .callout(w, h, [opts]) // Speech bubble / callout
113
+ .cloud(w, h) // Cloud / thought bubble
114
+ .cross(size, [barWidth]) // Cross / plus sign
115
+ .cube(w, h, [depth]) // 3D isometric cube
116
+ .path(d, w, h) // Custom SVG path
117
+ .document(w, h, [wave]) // Document (wavy bottom)
118
+ .note(w, h, [foldSize]) // Note (folded corner)
119
+ .parallelogram(w, h, [skew]) // Parallelogram (I/O)
120
+ .star(points, outerR, [innerR]) // Star / badge
121
+ .trapezoid(topW, bottomW, h) // Trapezoid
122
+ .triangle(w, h, [direction]) // Triangle
105
123
  .label('Text', { dy: 5 }) // Label with offset
106
124
  .class('css-class') // Custom CSS class
107
125
  .data({ ... }) // Attach custom data
126
+ .port('out', { x: 50, y: 0 }) // Named connection port
127
+ .container(config?) // Mark as container / group node
128
+ .parent('containerId') // Make child of a container
108
129
  ```
109
130
 
131
+ ### Container / Group Nodes
132
+
133
+ Group related nodes into visual containers (swimlanes, sub-processes, etc.).
134
+
135
+ ```typescript
136
+ b.node('lane')
137
+ .at(250, 170)
138
+ .rect(460, 300)
139
+ .label('Process Phase')
140
+ .container({ headerHeight: 36 });
141
+
142
+ b.node('step1').at(150, 220).rect(100, 50).parent('lane');
143
+ b.node('step2').at(350, 220).rect(100, 50).parent('lane');
144
+ ```
145
+
146
+ Container children are nested inside the container `<g>` in the SVG and follow the container when moved at runtime.
147
+
110
148
  ### Edges
111
149
 
112
150
  Edges connect nodes and can be styled, directed, or animated.
151
+ All edges are rendered as `<path>` elements supporting three routing modes.
113
152
 
114
153
  ```typescript
115
154
  b.edge('n1', 'n2')
@@ -117,8 +156,65 @@ b.edge('n1', 'n2')
117
156
  .straight() // (Default) Straight line
118
157
  .label('Connection')
119
158
  .animate('flow'); // Add animation
159
+
160
+ // Curved edge
161
+ b.edge('a', 'b').curved().arrow();
162
+
163
+ // Orthogonal (right-angle) edge
164
+ b.edge('a', 'c').orthogonal().arrow();
165
+
166
+ // Waypoints — intermediate points the edge passes through
167
+ b.edge('x', 'y').curved().via(150, 50).via(200, 100).arrow();
168
+
169
+ // Per-edge styling (overrides CSS defaults)
170
+ b.edge('a', 'b').stroke('#ff0000', 3).fill('none').opacity(0.8);
171
+
172
+ // Multi-position edge labels (start / mid / end)
173
+ b.edge('a', 'b')
174
+ .label('1', { position: 'start' })
175
+ .label('*', { position: 'end' })
176
+ .arrow();
177
+
178
+ // Edge markers / arrowhead types
179
+ b.edge('a', 'b').markerEnd('arrowOpen'); // Open arrow (inheritance)
180
+ b.edge('a', 'b').markerStart('diamond').markerEnd('arrow'); // UML composition
181
+ b.edge('a', 'b').markerStart('diamondOpen').markerEnd('arrow'); // UML aggregation
182
+ b.edge('a', 'b').arrow('both'); // Bidirectional arrows
183
+ b.edge('a', 'b').markerStart('circleOpen').markerEnd('arrow'); // Association
184
+ b.edge('a', 'b').markerEnd('bar'); // ER cardinality
185
+
186
+ // Connection ports — edges attach to specific points on nodes
187
+ b.node('srv')
188
+ .at(100, 100)
189
+ .rect(80, 60)
190
+ .port('out-1', { x: 40, y: -15 })
191
+ .port('out-2', { x: 40, y: 15 });
192
+ b.node('db').at(400, 100).cylinder(80, 60).port('in', { x: -40, y: 0 });
193
+ b.edge('srv', 'db').fromPort('out-1').toPort('in').arrow();
194
+
195
+ // Default ports (no .port() needed) — every shape has built-in ports
196
+ b.edge('a', 'b').fromPort('right').toPort('left').arrow();
120
197
  ```
121
198
 
199
+ | Method | Description |
200
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
201
+ | `.straight()` | Direct line (default). With waypoints → polyline. |
202
+ | `.curved()` | Smooth bezier curve. With waypoints → Catmull-Rom spline. |
203
+ | `.orthogonal()` | Right-angle elbows. |
204
+ | `.routing(mode)` | Set mode programmatically. |
205
+ | `.via(x, y)` | Add an intermediate waypoint (chainable). |
206
+ | `.label(text, opts?)` | Add a text label. Chain multiple calls for multi-position labels. `opts.position` can be `'start'`, `'mid'` (default), or `'end'`. |
207
+ | `.arrow([enabled])` | Shorthand for arrow markers. `true`/no-arg → markerEnd arrow. `'both'` → both ends. `'start'`/`'end'` → specific end. `false` → none. |
208
+ | `.markerEnd(type)` | Set marker type at the target end. See `EdgeMarkerType`. |
209
+ | `.markerStart(type)` | Set marker type at the source end. See `EdgeMarkerType`. |
210
+ | `.fromPort(portId)` | Connect from a specific named port on the source node. |
211
+ | `.toPort(portId)` | Connect to a specific named port on the target node. |
212
+ | `.stroke(color, width?)` | Set stroke color and optional width. |
213
+ | `.fill(color)` | Set fill color. |
214
+ | `.opacity(value)` | Set opacity (0–1). |
215
+
216
+ **`EdgeMarkerType`** values: `'none'`, `'arrow'`, `'arrowOpen'`, `'diamond'`, `'diamondOpen'`, `'circle'`, `'circleOpen'`, `'square'`, `'bar'`, `'halfArrow'`.
217
+
122
218
  ### Animations
123
219
 
124
220
  See the full Animations guide [docs here](https://vizcraft-docs.vercel.app/docs/animations).
@@ -265,13 +361,19 @@ VizCraft generates standard SVG elements with predictable classes, making it eas
265
361
  fill: #ff6b6b;
266
362
  }
267
363
 
268
- /* Edge styling */
364
+ /* Edge styling (CSS defaults) */
269
365
  .viz-edge {
270
366
  stroke: #ccc;
271
367
  stroke-width: 2;
272
368
  }
273
369
  ```
274
370
 
371
+ Edges can also be styled **per-edge** via the builder (inline SVG attributes override CSS):
372
+
373
+ ```ts
374
+ b.edge('a', 'b').stroke('#e74c3c', 3).fill('none').opacity(0.8);
375
+ ```
376
+
275
377
  ## 🤝 Contributing
276
378
 
277
379
  Contributions are welcome! This is a monorepo managed with Turbo.
@@ -38,6 +38,8 @@ export declare class AnimationBuilder {
38
38
  extendAdapter(cb: ExtendAdapter): this;
39
39
  /** Select a node by id (compiles to target `node:<id>`). */
40
40
  node(id: string): this;
41
+ /** Select an overlay by key (compiles to target `overlay:<key>`). */
42
+ overlay(key: string): this;
41
43
  /**
42
44
  * Select an edge.
43
45
  *
@@ -30,6 +30,11 @@ export class AnimationBuilder {
30
30
  this.currentTarget = toTarget('node', id);
31
31
  return this;
32
32
  }
33
+ /** Select an overlay by key (compiles to target `overlay:<key>`). */
34
+ overlay(key) {
35
+ this.currentTarget = toTarget('overlay', key);
36
+ return this;
37
+ }
33
38
  edge(a, b, c) {
34
39
  const id = b === undefined ? a : (c ?? `${a}->${b}`);
35
40
  this.currentTarget = toTarget('edge', id);
@@ -56,7 +61,7 @@ export class AnimationBuilder {
56
61
  */
57
62
  to(props, opts) {
58
63
  if (!this.currentTarget) {
59
- throw new Error('AnimationBuilder.to(): no target selected (call node(...) or edge(...))');
64
+ throw new Error('AnimationBuilder.to(): no target selected (call node(...), edge(...), or overlay(...))');
60
65
  }
61
66
  const duration = Math.max(0, opts.duration);
62
67
  const easing = opts.easing;
@@ -1,4 +1,4 @@
1
- export type AnimationTarget = (`node:${string}` | `edge:${string}`) | (string & {});
1
+ export type AnimationTarget = (`node:${string}` | `edge:${string}` | `overlay:${string}`) | (string & {});
2
2
  export type CoreAnimProperty = 'x' | 'y' | 'opacity' | 'scale' | 'rotation' | 'strokeDashoffset';
3
3
  export type AnimProperty = CoreAnimProperty | (string & {});
4
4
  export type Ease = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
@@ -1,13 +1,21 @@
1
+ import { OVERLAY_RUNTIME_DIRTY } from '../types';
1
2
  import { createRegistryAdapter } from './registryAdapter';
2
3
  export function createVizCraftAdapter(scene, requestRender) {
3
4
  const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));
4
5
  const edgesById = new Map(scene.edges.map((e) => [e.id, e]));
6
+ const overlays = scene.overlays ?? [];
7
+ const overlaysByKey = new Map();
8
+ for (const spec of overlays) {
9
+ const key = spec.key ?? spec.id;
10
+ overlaysByKey.set(key, spec);
11
+ }
5
12
  const adapter = createRegistryAdapter({
6
13
  flush: requestRender,
7
14
  });
8
15
  // register node/edge target resolvers and props using ergonomic handles
9
16
  const node = adapter.kind('node', (id) => nodesById.get(id));
10
17
  const edge = adapter.kind('edge', (id) => edgesById.get(id));
18
+ const overlay = adapter.kind('overlay', (key) => overlaysByKey.get(key));
11
19
  node
12
20
  .prop('x', {
13
21
  get: (el) => {
@@ -75,5 +83,64 @@ export function createVizCraftAdapter(scene, requestRender) {
75
83
  e.runtime.strokeDashoffset = v;
76
84
  },
77
85
  });
78
- return adapter;
86
+ // Overlay params: allow animating arbitrary numeric fields on `spec.params`.
87
+ //
88
+ // This intentionally uses a generic reader/writer so users can animate
89
+ // custom overlays without needing adapter extensions.
90
+ const overlayParamReader = (el, prop) => {
91
+ const spec = el;
92
+ const params = spec.params;
93
+ const v = params?.[prop];
94
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
95
+ };
96
+ const overlayParamWriter = (el, prop, value) => {
97
+ const spec = el;
98
+ const existing = spec.params;
99
+ const params = existing && typeof existing === 'object' && !Array.isArray(existing)
100
+ ? existing
101
+ : {};
102
+ params[prop] = value;
103
+ // Ensure we keep reference stable in case params was undefined or non-object.
104
+ spec.params = params;
105
+ // Mark dirty so patchRuntime can avoid re-rendering unaffected overlays.
106
+ spec[OVERLAY_RUNTIME_DIRTY] = true;
107
+ };
108
+ const resolveOverlayFromTarget = (target) => {
109
+ const t = String(target);
110
+ if (!t.startsWith('overlay:'))
111
+ return undefined;
112
+ const key = t.slice('overlay:'.length);
113
+ return overlaysByKey.get(key);
114
+ };
115
+ // Register core overlay props we know are numeric today.
116
+ // Users can still register more via adapter extensions if they prefer explicitness.
117
+ overlay.prop('progress', {
118
+ get: (el) => overlayParamReader(el, 'progress'),
119
+ set: (el, v) => overlayParamWriter(el, 'progress', v),
120
+ });
121
+ // Make overlays fully generic: any numeric `spec.params[prop]` is animatable.
122
+ //
123
+ // `createRegistryAdapter` requires per-prop registration; for overlays we provide
124
+ // a fallback so custom overlays don't need adapter extensions.
125
+ const baseGet = adapter.get;
126
+ const baseSet = adapter.set;
127
+ return {
128
+ ...adapter,
129
+ get(target, prop) {
130
+ const v = baseGet(target, prop);
131
+ if (v !== undefined)
132
+ return v;
133
+ const spec = resolveOverlayFromTarget(target);
134
+ if (!spec)
135
+ return undefined;
136
+ return overlayParamReader(spec, String(prop));
137
+ },
138
+ set(target, prop, value) {
139
+ baseSet(target, prop, value);
140
+ const spec = resolveOverlayFromTarget(target);
141
+ if (!spec)
142
+ return;
143
+ overlayParamWriter(spec, String(prop), value);
144
+ },
145
+ };
79
146
  }
package/dist/builder.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, AnimationConfig, VizGridConfig } from './types';
1
+ import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, AnimationConfig, OverlayId, OverlayParams, VizGridConfig, ContainerConfig, EdgeRouting, EdgeMarkerType } from './types';
2
+ import { OverlayBuilder } from './overlayBuilder';
2
3
  import type { AnimationSpec } from './anim/spec';
3
4
  import { type AnimationBuilder, type AnimatableProps, type TweenOptions } from './anim/animationBuilder';
4
5
  import { type PlaybackController } from './anim/playback';
@@ -13,6 +14,9 @@ interface VizBuilder {
13
14
  * The compiled spec is also stored on the built scene as `scene.animationSpecs`.
14
15
  */
15
16
  animate(cb: (anim: AnimationBuilder) => unknown): AnimationSpec;
17
+ /** Fluent overlay authoring (compiles to overlay specs and stores on the built scene). */
18
+ overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
19
+ overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
16
20
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
17
21
  node(id: string): NodeBuilder;
18
22
  edge(from: string, to: string, id?: string): EdgeBuilder;
@@ -53,6 +57,28 @@ interface NodeBuilder {
53
57
  circle(r: number): NodeBuilder;
54
58
  rect(w: number, h: number, rx?: number): NodeBuilder;
55
59
  diamond(w: number, h: number): NodeBuilder;
60
+ cylinder(w: number, h: number, arcHeight?: number): NodeBuilder;
61
+ hexagon(r: number, orientation?: 'pointy' | 'flat'): NodeBuilder;
62
+ ellipse(rx: number, ry: number): NodeBuilder;
63
+ arc(r: number, startAngle: number, endAngle: number, closed?: boolean): NodeBuilder;
64
+ blockArrow(length: number, bodyWidth: number, headWidth: number, headLength: number, direction?: 'right' | 'left' | 'up' | 'down'): NodeBuilder;
65
+ callout(w: number, h: number, opts?: {
66
+ rx?: number;
67
+ pointerSide?: 'bottom' | 'top' | 'left' | 'right';
68
+ pointerHeight?: number;
69
+ pointerWidth?: number;
70
+ pointerPosition?: number;
71
+ }): NodeBuilder;
72
+ cloud(w: number, h: number): NodeBuilder;
73
+ cross(size: number, barWidth?: number): NodeBuilder;
74
+ cube(w: number, h: number, depth?: number): NodeBuilder;
75
+ path(d: string, w: number, h: number): NodeBuilder;
76
+ document(w: number, h: number, waveHeight?: number): NodeBuilder;
77
+ note(w: number, h: number, foldSize?: number): NodeBuilder;
78
+ parallelogram(w: number, h: number, skew?: number): NodeBuilder;
79
+ star(points: number, outerR: number, innerR?: number): NodeBuilder;
80
+ trapezoid(topW: number, bottomW: number, h: number): NodeBuilder;
81
+ triangle(w: number, h: number, direction?: 'up' | 'down' | 'left' | 'right'): NodeBuilder;
56
82
  label(text: string, opts?: Partial<NodeLabel>): NodeBuilder;
57
83
  fill(color: string): NodeBuilder;
58
84
  stroke(color: string, width?: number): NodeBuilder;
@@ -64,18 +90,58 @@ interface NodeBuilder {
64
90
  animateTo(props: AnimatableProps, opts: TweenOptions): NodeBuilder;
65
91
  data(payload: unknown): NodeBuilder;
66
92
  onClick(handler: (id: string, node: VizNode) => void): NodeBuilder;
93
+ /**
94
+ * Define a named connection port on the node.
95
+ * @param id Unique port id (e.g. `'top'`, `'out-1'`)
96
+ * @param offset Position relative to the node center `{ x, y }`
97
+ * @param direction Optional outgoing tangent in degrees (0=right, 90=down, 180=left, 270=up)
98
+ */
99
+ port(id: string, offset: {
100
+ x: number;
101
+ y: number;
102
+ }, direction?: number): NodeBuilder;
103
+ container(config?: ContainerConfig): NodeBuilder;
104
+ parent(parentId: string): NodeBuilder;
67
105
  done(): VizBuilder;
68
106
  node(id: string): NodeBuilder;
69
107
  edge(from: string, to: string, id?: string): EdgeBuilder;
108
+ overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
109
+ overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
70
110
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
71
111
  build(): VizScene;
72
112
  svg(): string;
73
113
  }
74
114
  interface EdgeBuilder {
75
115
  straight(): EdgeBuilder;
116
+ curved(): EdgeBuilder;
117
+ orthogonal(): EdgeBuilder;
118
+ routing(mode: EdgeRouting): EdgeBuilder;
119
+ via(x: number, y: number): EdgeBuilder;
76
120
  label(text: string, opts?: Partial<EdgeLabel>): EdgeBuilder;
77
- arrow(enabled?: boolean): EdgeBuilder;
121
+ /**
122
+ * Set arrow markers. Convenience method.
123
+ * - `arrow(true)` or `arrow()` sets markerEnd to 'arrow'
124
+ * - `arrow(false)` sets markerEnd to 'none'
125
+ * - `arrow('both')` sets both markerStart and markerEnd to 'arrow'
126
+ * - `arrow('start')` sets markerStart to 'arrow'
127
+ * - `arrow('end')` sets markerEnd to 'arrow'
128
+ */
129
+ arrow(enabled?: boolean | 'both' | 'start' | 'end'): EdgeBuilder;
130
+ /** Set the marker type at the end (target) of the edge. */
131
+ markerEnd(type: EdgeMarkerType): EdgeBuilder;
132
+ /** Set the marker type at the start (source) of the edge. */
133
+ markerStart(type: EdgeMarkerType): EdgeBuilder;
134
+ /** Connect the edge to a specific port on the source node. */
135
+ fromPort(portId: string): EdgeBuilder;
136
+ /** Connect the edge to a specific port on the target node. */
137
+ toPort(portId: string): EdgeBuilder;
78
138
  connect(anchor: 'center' | 'boundary'): EdgeBuilder;
139
+ /** Sets the fill color of the edge path. */
140
+ fill(color: string): EdgeBuilder;
141
+ /** Sets the stroke color and optional width of the edge path. */
142
+ stroke(color: string, width?: number): EdgeBuilder;
143
+ /** Sets the opacity of the edge. */
144
+ opacity(value: number): EdgeBuilder;
79
145
  class(name: string): EdgeBuilder;
80
146
  hitArea(px: number): EdgeBuilder;
81
147
  animate(type: string, config?: AnimationConfig): EdgeBuilder;
@@ -87,6 +153,8 @@ interface EdgeBuilder {
87
153
  done(): VizBuilder;
88
154
  node(id: string): NodeBuilder;
89
155
  edge(from: string, to: string, id?: string): EdgeBuilder;
156
+ overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
157
+ overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
90
158
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
91
159
  build(): VizScene;
92
160
  svg(): string;