vizcraft 1.2.0 → 1.3.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,11 @@
1
1
  # vizcraft
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`3f55212`](https://github.com/ChipiKaf/vizcraft/commit/3f55212e56557994710d65a99fe339c6826cb2a7) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Add new visual styling options for nodes and edges: a hand-drawn "sketch" rendering mode, configurable drop-shadow support for nodes, and node stroke dasharray (dashed strokes)
8
+
3
9
  ## 1.2.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -196,6 +196,16 @@ b.node('n1')
196
196
  .image(href, w, h, opts?) // Embed an <image> inside the node
197
197
  .icon(id, opts?) // Embed an icon from the icon registry (see registerIcon)
198
198
  .svgContent(svg, opts) // Embed inline SVG content inside the node
199
+ .fill('#f0f0f0') // Fill color
200
+ .stroke('#333', 2) // Stroke color and optional width
201
+ .opacity(0.8) // Opacity
202
+ .dashed() // Dashed border (8, 4)
203
+ .dotted() // Dotted border (2, 4)
204
+ .dash('12, 3, 3, 3') // Custom dash pattern
205
+ .shadow() // Drop shadow (default: dx=2 dy=2 blur=4)
206
+ .shadow({ dx: 4, dy: 4, blur: 10, color: 'rgba(0,0,0,0.35)' }) // Custom shadow
207
+ .sketch() // Sketch / hand-drawn look (SVG turbulence filter)
208
+ .sketch({ seed: 42 }) // Sketch with explicit seed for deterministic jitter
199
209
  .class('css-class') // Custom CSS class
200
210
  .data({ ... }) // Attach custom data
201
211
  .port('out', { x: 50, y: 0 }) // Named connection port
@@ -262,6 +272,9 @@ b.edge('a', 'b').dashed().stroke('#6c7086'); // dashed line
262
272
  b.edge('a', 'b').dotted(); // dotted line
263
273
  b.edge('a', 'b').dash('12, 3, 3, 3').stroke('#cba6f7'); // custom pattern
264
274
 
275
+ // Sketch / hand-drawn edges
276
+ b.edge('a', 'b').sketch(); // sketchy look
277
+
265
278
  // Multi-position edge labels (start / mid / end)
266
279
  b.edge('a', 'b')
267
280
  .label('1', { position: 'start' })
package/dist/builder.d.ts CHANGED
@@ -37,6 +37,8 @@ export interface VizBuilder extends VizSceneMutator {
37
37
  x: number;
38
38
  y: number;
39
39
  }): VizBuilder;
40
+ /** Enable global sketch / hand-drawn rendering for all nodes and edges. */
41
+ sketch(enabled?: boolean, seed?: number): VizBuilder;
40
42
  /**
41
43
  * Fluent, data-only animation authoring. Compiles immediately to an `AnimationSpec`.
42
44
  * The compiled spec is also stored on the built scene as `scene.animationSpecs`.
@@ -209,6 +211,27 @@ interface NodeBuilder {
209
211
  fill(color: string): NodeBuilder;
210
212
  stroke(color: string, width?: number): NodeBuilder;
211
213
  opacity(value: number): NodeBuilder;
214
+ /** Apply a dashed stroke pattern (`8, 4`). */
215
+ dashed(): NodeBuilder;
216
+ /** Apply a dotted stroke pattern (`2, 4`). */
217
+ dotted(): NodeBuilder;
218
+ /** Apply a custom SVG `stroke-dasharray` value, or a preset name (`'dashed'`, `'dotted'`, `'dash-dot'`). */
219
+ dash(pattern: 'solid' | 'dashed' | 'dotted' | 'dash-dot' | string): NodeBuilder;
220
+ /**
221
+ * Add a drop shadow behind the node shape.
222
+ *
223
+ * Call with no arguments for a sensible default, or pass a config object.
224
+ */
225
+ shadow(config?: {
226
+ dx?: number;
227
+ dy?: number;
228
+ blur?: number;
229
+ color?: string;
230
+ }): NodeBuilder;
231
+ /** Render this node with a hand-drawn / sketchy appearance. */
232
+ sketch(config?: {
233
+ seed?: number;
234
+ }): NodeBuilder;
212
235
  class(name: string): NodeBuilder;
213
236
  zIndex(value: number): NodeBuilder;
214
237
  animate(type: string, config?: AnimationConfig): NodeBuilder;
@@ -283,6 +306,8 @@ interface EdgeBuilder {
283
306
  dotted(): EdgeBuilder;
284
307
  /** Apply a custom SVG `stroke-dasharray` value, or a preset name (`'dashed'`, `'dotted'`, `'dash-dot'`). */
285
308
  dash(pattern: 'solid' | 'dashed' | 'dotted' | 'dash-dot' | string): EdgeBuilder;
309
+ /** Render this edge with a hand-drawn / sketchy appearance. */
310
+ sketch(): EdgeBuilder;
286
311
  class(name: string): EdgeBuilder;
287
312
  hitArea(px: number): EdgeBuilder;
288
313
  animate(type: string, config?: AnimationConfig): EdgeBuilder;
package/dist/builder.js CHANGED
@@ -37,6 +37,74 @@ function markerIdFor(markerType, stroke, position = 'end') {
37
37
  function arrowMarkerIdFor(stroke) {
38
38
  return markerIdFor('arrow', stroke);
39
39
  }
40
+ const SHADOW_DEFAULTS = {
41
+ dx: 2,
42
+ dy: 2,
43
+ blur: 4,
44
+ color: 'rgba(0,0,0,0.2)',
45
+ };
46
+ /** Apply defaults to a partial shadow config. */
47
+ function resolveShadow(shadow) {
48
+ return {
49
+ dx: shadow.dx ?? SHADOW_DEFAULTS.dx,
50
+ dy: shadow.dy ?? SHADOW_DEFAULTS.dy,
51
+ blur: shadow.blur ?? SHADOW_DEFAULTS.blur,
52
+ color: shadow.color ?? SHADOW_DEFAULTS.color,
53
+ };
54
+ }
55
+ /** Deterministic filter id keyed by config tuple for dedup. */
56
+ function shadowFilterId(cfg) {
57
+ const colorSuffix = cfg.color.replace(/[^a-zA-Z0-9]/g, '_');
58
+ return `viz-shadow-${cfg.dx}-${cfg.dy}-${cfg.blur}-${colorSuffix}`;
59
+ }
60
+ /** SVG markup for a drop-shadow `<filter>`. */
61
+ function shadowFilterSvg(id, cfg) {
62
+ return `<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%"><feDropShadow dx="${cfg.dx}" dy="${cfg.dy}" stdDeviation="${cfg.blur}" flood-color="${escapeXmlAttr(cfg.color)}" flood-opacity="1"/></filter>`;
63
+ }
64
+ /** Deterministic filter id for sketch displacement keyed by seed. */
65
+ function sketchFilterId(seed) {
66
+ return `viz-sketch-${seed}`;
67
+ }
68
+ /** Simple seeded float in [0, 1) derived from a seed via xorshift-like mix. */
69
+ function sketchRand(seed, salt) {
70
+ let s = ((seed ^ (salt * 2654435761)) >>> 0) | 1;
71
+ s ^= s << 13;
72
+ s ^= s >>> 17;
73
+ s ^= s << 5;
74
+ return (s >>> 0) / 4294967296;
75
+ }
76
+ /** Lerp a value between min and max using a seeded random. */
77
+ function sketchLerp(seed, salt, min, max) {
78
+ return min + sketchRand(seed, salt) * (max - min);
79
+ }
80
+ /** SVG markup for a sketch `<filter>` using dual-pass displacement for a hand-drawn double-stroke look. */
81
+ function sketchFilterSvg(id, seed) {
82
+ const s2 = seed + 37;
83
+ // Derive unique per-seed parameters
84
+ const freq2 = sketchLerp(seed, 1, 0.009, 0.015).toFixed(4);
85
+ const scale1 = sketchLerp(seed, 2, 2.5, 4).toFixed(1);
86
+ const scale2 = sketchLerp(seed, 3, 3, 5).toFixed(1);
87
+ const dx = sketchLerp(seed, 4, 0.3, 1.6).toFixed(2);
88
+ const dy = sketchLerp(seed, 5, 0.2, 1.3).toFixed(2);
89
+ return (`<filter id="${id}" filterUnits="userSpaceOnUse" x="-10000" y="-10000" width="20000" height="20000">` +
90
+ `<feTurbulence type="fractalNoise" baseFrequency="0.008" numOctaves="2" seed="${seed}" result="n1"/>` +
91
+ `<feTurbulence type="fractalNoise" baseFrequency="${freq2}" numOctaves="2" seed="${s2}" result="n2"/>` +
92
+ `<feDisplacementMap in="SourceGraphic" in2="n1" scale="${scale1}" xChannelSelector="R" yChannelSelector="G" result="s1"/>` +
93
+ `<feDisplacementMap in="SourceGraphic" in2="n2" scale="${scale2}" xChannelSelector="G" yChannelSelector="R" result="s2"/>` +
94
+ `<feOffset in="s2" dx="${dx}" dy="${dy}" result="s2off"/>` +
95
+ '<feComposite in="s1" in2="s2off" operator="over"/>' +
96
+ '</filter>');
97
+ }
98
+ /** Resolve the effective sketch seed for a node, falling back to the hash of its id. */
99
+ function resolveSketchSeed(nodeStyle, id) {
100
+ if (nodeStyle?.sketchSeed !== undefined)
101
+ return nodeStyle.sketchSeed;
102
+ let h = 0;
103
+ for (let i = 0; i < id.length; i++) {
104
+ h = (Math.imul(31, h) + id.charCodeAt(i)) | 0;
105
+ }
106
+ return Math.abs(h);
107
+ }
40
108
  /**
41
109
  * Generate SVG markup for a single marker definition.
42
110
  * @param markerType The type of marker
@@ -251,6 +319,14 @@ function applyNodeOptions(nb, opts) {
251
319
  }
252
320
  if (opts.opacity !== undefined)
253
321
  nb.opacity(opts.opacity);
322
+ if (opts.dash)
323
+ nb.dash(opts.dash);
324
+ if (opts.shadow !== undefined && opts.shadow !== false) {
325
+ nb.shadow(opts.shadow === true ? {} : opts.shadow);
326
+ }
327
+ if (opts.sketch !== undefined && opts.sketch !== false) {
328
+ nb.sketch(opts.sketch === true ? {} : opts.sketch);
329
+ }
254
330
  if (opts.className)
255
331
  nb.class(opts.className);
256
332
  if (opts.zIndex !== undefined)
@@ -339,6 +415,8 @@ function applyEdgeOptions(eb, opts) {
339
415
  eb.opacity(opts.opacity);
340
416
  if (opts.dash)
341
417
  eb.dash(opts.dash);
418
+ if (opts.sketch)
419
+ eb.sketch();
342
420
  if (opts.className)
343
421
  eb.class(opts.className);
344
422
  // Anchor & ports
@@ -422,6 +500,7 @@ class VizBuilderImpl {
422
500
  _nodeOrder = [];
423
501
  _edgeOrder = [];
424
502
  _gridConfig = null;
503
+ _sketch = null;
425
504
  _animationSpecs = [];
426
505
  _mountedContainer = null;
427
506
  _panZoomController;
@@ -656,6 +735,10 @@ class VizBuilderImpl {
656
735
  this._gridConfig = { cols, rows, padding };
657
736
  return this;
658
737
  }
738
+ sketch(enabled = true, seed) {
739
+ this._sketch = enabled ? { enabled: true, seed } : null;
740
+ return this;
741
+ }
659
742
  overlay(arg1, arg2, arg3) {
660
743
  if (typeof arg1 === 'function') {
661
744
  const overlay = new OverlayBuilder();
@@ -740,6 +823,9 @@ class VizBuilderImpl {
740
823
  if (scene.animationSpecs) {
741
824
  this._animationSpecs = [...scene.animationSpecs];
742
825
  }
826
+ this._sketch = scene.sketch
827
+ ? { ...scene.sketch, enabled: scene.sketch.enabled ?? true }
828
+ : null;
743
829
  return this;
744
830
  }
745
831
  /**
@@ -764,6 +850,7 @@ class VizBuilderImpl {
764
850
  edges,
765
851
  overlays: this._overlays,
766
852
  animationSpecs: this._animationSpecs.length > 0 ? [...this._animationSpecs] : undefined,
853
+ sketch: this._sketch ?? undefined,
767
854
  };
768
855
  this._dispatchEvent('build', { scene });
769
856
  return scene;
@@ -1100,6 +1187,49 @@ class VizBuilderImpl {
1100
1187
  }
1101
1188
  defs.appendChild(markerEl);
1102
1189
  });
1190
+ const neededShadows = new Map();
1191
+ nodes.forEach((n) => {
1192
+ if (n.style?.shadow) {
1193
+ const cfg = resolveShadow(n.style.shadow);
1194
+ const fid = shadowFilterId(cfg);
1195
+ if (!neededShadows.has(fid)) {
1196
+ neededShadows.set(fid, cfg);
1197
+ }
1198
+ }
1199
+ });
1200
+ neededShadows.forEach((cfg, fid) => {
1201
+ const tmp = document.createElementNS(svgNS, 'svg');
1202
+ tmp.innerHTML = shadowFilterSvg(fid, cfg);
1203
+ const filterEl = tmp.querySelector('filter');
1204
+ if (filterEl) {
1205
+ defs.appendChild(filterEl);
1206
+ }
1207
+ });
1208
+ const neededSketchSeeds = new Set();
1209
+ const globalSketch = scene.sketch?.enabled;
1210
+ nodes.forEach((n) => {
1211
+ if (n.style?.sketch || globalSketch) {
1212
+ neededSketchSeeds.add(resolveSketchSeed(n.style, n.id));
1213
+ }
1214
+ });
1215
+ edges.forEach((e) => {
1216
+ if (e.style?.sketch || globalSketch) {
1217
+ let h = 0;
1218
+ for (let i = 0; i < e.id.length; i++) {
1219
+ h = (Math.imul(31, h) + e.id.charCodeAt(i)) | 0;
1220
+ }
1221
+ neededSketchSeeds.add(Math.abs(h));
1222
+ }
1223
+ });
1224
+ neededSketchSeeds.forEach((seed) => {
1225
+ const fid = sketchFilterId(seed);
1226
+ const tmp = document.createElementNS(svgNS, 'svg');
1227
+ tmp.innerHTML = sketchFilterSvg(fid, seed);
1228
+ const filterEl = tmp.querySelector('filter');
1229
+ if (filterEl) {
1230
+ defs.appendChild(filterEl);
1231
+ }
1232
+ });
1103
1233
  svg.appendChild(defs);
1104
1234
  // Layers
1105
1235
  const viewport = document.createElementNS(svgNS, 'g');
@@ -1162,6 +1292,9 @@ class VizBuilderImpl {
1162
1292
  }
1163
1293
  // Compute Classes & Styles
1164
1294
  let classes = `viz-edge-group ${edge.className || ''}`;
1295
+ const edgeSketched = edge.style?.sketch || scene.sketch?.enabled;
1296
+ if (edgeSketched)
1297
+ classes += ' viz-sketch';
1165
1298
  // Reset styles
1166
1299
  group.removeAttribute('style');
1167
1300
  if (edge.animations) {
@@ -1281,6 +1414,17 @@ class VizBuilderImpl {
1281
1414
  else {
1282
1415
  line.style.removeProperty('stroke-dasharray');
1283
1416
  }
1417
+ if (edgeSketched) {
1418
+ let h = 0;
1419
+ for (let i = 0; i < edge.id.length; i++) {
1420
+ h = (Math.imul(31, h) + edge.id.charCodeAt(i)) | 0;
1421
+ }
1422
+ const seed = Math.abs(h);
1423
+ line.setAttribute('filter', `url(#${sketchFilterId(seed)})`);
1424
+ }
1425
+ else {
1426
+ line.removeAttribute('filter');
1427
+ }
1284
1428
  const oldHit = group.querySelector('[data-viz-role="edge-hit"]') ||
1285
1429
  group.querySelector('.viz-edge-hit');
1286
1430
  if (oldHit)
@@ -1311,6 +1455,10 @@ class VizBuilderImpl {
1311
1455
  const labelClass = `viz-edge-label ${lbl.className || ''}`;
1312
1456
  const edgeLabelSvg = renderSvgText(pos.x, pos.y, lbl.rich ?? lbl.text, {
1313
1457
  className: labelClass,
1458
+ fill: lbl.fill,
1459
+ fontSize: lbl.fontSize,
1460
+ fontWeight: lbl.fontWeight,
1461
+ fontFamily: lbl.fontFamily,
1314
1462
  textAnchor: 'middle',
1315
1463
  dominantBaseline: 'middle',
1316
1464
  maxWidth: lbl.maxWidth,
@@ -1370,6 +1518,9 @@ class VizBuilderImpl {
1370
1518
  const isContainer = !!node.container;
1371
1519
  // Calculate Anim Classes
1372
1520
  let classes = `viz-node-group${isContainer ? ' viz-container' : ''} ${node.className || ''}`;
1521
+ const nodeSketched = node.style?.sketch || scene.sketch?.enabled;
1522
+ if (nodeSketched)
1523
+ classes += ' viz-sketch';
1373
1524
  group.removeAttribute('style');
1374
1525
  if (node.animations) {
1375
1526
  node.animations.forEach((spec) => {
@@ -1426,12 +1577,25 @@ class VizBuilderImpl {
1426
1577
  group.prepend(shape);
1427
1578
  }
1428
1579
  applyShapeGeometry(shape, node.shape, { x, y });
1580
+ const resolvedNodeDash = resolveDasharray(node.style?.strokeDasharray);
1581
+ const nodeShadowFilter = node.style?.shadow
1582
+ ? `url(#${shadowFilterId(resolveShadow(node.style.shadow))})`
1583
+ : undefined;
1429
1584
  setSvgAttributes(shape, {
1430
1585
  fill: node.style?.fill ?? 'none',
1431
1586
  stroke: node.style?.stroke ?? '#111',
1432
1587
  'stroke-width': node.style?.strokeWidth ?? 2,
1433
1588
  opacity: node.runtime?.opacity ?? node.style?.opacity,
1589
+ 'stroke-dasharray': resolvedNodeDash || undefined,
1590
+ filter: nodeShadowFilter,
1434
1591
  });
1592
+ if (nodeSketched) {
1593
+ const seed = resolveSketchSeed(node.style, node.id);
1594
+ group.setAttribute('filter', `url(#${sketchFilterId(seed)})`);
1595
+ }
1596
+ else {
1597
+ group.removeAttribute('filter');
1598
+ }
1435
1599
  // Embedded media (rendered alongside the base shape)
1436
1600
  const existingImg = group.querySelector(':scope > [data-viz-role="node-image"], :scope > .viz-node-image');
1437
1601
  if (existingImg)
@@ -1558,6 +1722,7 @@ class VizBuilderImpl {
1558
1722
  fill: node.label.fill,
1559
1723
  fontSize: node.label.fontSize,
1560
1724
  fontWeight: node.label.fontWeight,
1725
+ fontFamily: node.label.fontFamily,
1561
1726
  textAnchor: node.label.textAnchor || 'middle',
1562
1727
  dominantBaseline: node.label.dominantBaseline || 'middle',
1563
1728
  maxWidth: node.label.maxWidth,
@@ -1744,6 +1909,38 @@ class VizBuilderImpl {
1744
1909
  }
1745
1910
  }
1746
1911
  });
1912
+ const exportShadows = new Map();
1913
+ exportNodes.forEach((n) => {
1914
+ if (n.style?.shadow) {
1915
+ const cfg = resolveShadow(n.style.shadow);
1916
+ const fid = shadowFilterId(cfg);
1917
+ if (!exportShadows.has(fid)) {
1918
+ exportShadows.set(fid, cfg);
1919
+ }
1920
+ }
1921
+ });
1922
+ exportShadows.forEach((cfg, fid) => {
1923
+ svgContent += shadowFilterSvg(fid, cfg);
1924
+ });
1925
+ const exportSketchSeeds = new Set();
1926
+ const globalSketchExport = exportScene.sketch?.enabled;
1927
+ exportNodes.forEach((n) => {
1928
+ if (n.style?.sketch || globalSketchExport) {
1929
+ exportSketchSeeds.add(resolveSketchSeed(n.style, n.id));
1930
+ }
1931
+ });
1932
+ exportEdges.forEach((e) => {
1933
+ if (e.style?.sketch || globalSketchExport) {
1934
+ let h = 0;
1935
+ for (let i = 0; i < e.id.length; i++) {
1936
+ h = (Math.imul(31, h) + e.id.charCodeAt(i)) | 0;
1937
+ }
1938
+ exportSketchSeeds.add(Math.abs(h));
1939
+ }
1940
+ });
1941
+ exportSketchSeeds.forEach((seed) => {
1942
+ svgContent += sketchFilterSvg(sketchFilterId(seed), seed);
1943
+ });
1747
1944
  svgContent += `
1748
1945
  </defs>`;
1749
1946
  // Render Edges
@@ -1825,7 +2022,9 @@ class VizBuilderImpl {
1825
2022
  lineRuntimeStyle += `stroke-dashoffset: ${edge.runtime.strokeDashoffset}; `;
1826
2023
  lineRuntimeAttrs += ` stroke-dashoffset="${edge.runtime.strokeDashoffset}"`;
1827
2024
  }
1828
- svgContent += `<g data-id="${edge.id}" data-viz-role="edge-group" class="viz-edge-group ${edge.className || ''} ${animClasses}" style="${animStyleStr}${runtimeStyle}">`;
2025
+ const edgeSketched = edge.style?.sketch || globalSketchExport;
2026
+ const sketchClass = edgeSketched ? ' viz-sketch' : '';
2027
+ svgContent += `<g data-id="${edge.id}" data-viz-role="edge-group" class="viz-edge-group${sketchClass} ${edge.className || ''} ${animClasses}" style="${animStyleStr}${runtimeStyle}">`;
1829
2028
  let edgeInlineStyle = lineRuntimeStyle;
1830
2029
  if (edge.style?.stroke !== undefined)
1831
2030
  edgeInlineStyle += `stroke: ${edge.style.stroke}; `;
@@ -1841,7 +2040,15 @@ class VizBuilderImpl {
1841
2040
  if (resolved)
1842
2041
  edgeInlineStyle += `stroke-dasharray: ${resolved}; `;
1843
2042
  }
1844
- svgContent += `<path d="${edgePath.d}" class="viz-edge" data-viz-role="edge-line" ${markerEnd} ${markerStart} style="${edgeInlineStyle}"${lineRuntimeAttrs} />`;
2043
+ let edgeSketchFilterAttr = '';
2044
+ if (edgeSketched) {
2045
+ let h = 0;
2046
+ for (let i = 0; i < edge.id.length; i++) {
2047
+ h = (Math.imul(31, h) + edge.id.charCodeAt(i)) | 0;
2048
+ }
2049
+ edgeSketchFilterAttr = ` filter="url(#${sketchFilterId(Math.abs(h))})"`;
2050
+ }
2051
+ svgContent += `<path d="${edgePath.d}" class="viz-edge" data-viz-role="edge-line" ${markerEnd} ${markerStart} style="${edgeInlineStyle}"${lineRuntimeAttrs}${edgeSketchFilterAttr} />`;
1845
2052
  // Edge Labels (multi-position)
1846
2053
  const allLabels = collectEdgeLabels(edge);
1847
2054
  allLabels.forEach((lbl, idx) => {
@@ -1849,6 +2056,10 @@ class VizBuilderImpl {
1849
2056
  const labelClass = `viz-edge-label ${lbl.className || ''}`;
1850
2057
  const edgeLabelSvg = renderSvgText(pos.x, pos.y, lbl.rich ?? lbl.text, {
1851
2058
  className: labelClass,
2059
+ fill: lbl.fill,
2060
+ fontSize: lbl.fontSize,
2061
+ fontWeight: lbl.fontWeight,
2062
+ fontFamily: lbl.fontFamily,
1852
2063
  textAnchor: 'middle',
1853
2064
  dominantBaseline: 'middle',
1854
2065
  maxWidth: lbl.maxWidth,
@@ -1913,18 +2124,30 @@ class VizBuilderImpl {
1913
2124
  });
1914
2125
  }
1915
2126
  const isContainer = !!node.container;
1916
- const className = `viz-node-group${isContainer ? ' viz-container' : ''} ${node.className || ''} ${animClasses}`;
2127
+ const nodeSketched = node.style?.sketch || globalSketchExport;
2128
+ const className = `viz-node-group${isContainer ? ' viz-container' : ''}${nodeSketched ? ' viz-sketch' : ''} ${node.className || ''} ${animClasses}`;
1917
2129
  const scale = node.runtime?.scale;
1918
2130
  const rotation = node.runtime?.rotation;
2131
+ let groupFilterAttr = '';
2132
+ if (nodeSketched) {
2133
+ const seed = resolveSketchSeed(node.style, node.id);
2134
+ groupFilterAttr = ` filter="url(#${sketchFilterId(seed)})"`;
2135
+ }
1919
2136
  const transformAttr = scale !== undefined || rotation !== undefined
1920
2137
  ? ` transform="translate(${x} ${y}) rotate(${rotation ?? 0}) scale(${scale ?? 1}) translate(${-x} ${-y})"`
1921
2138
  : '';
1922
- content += `<g data-id="${node.id}" data-viz-role="node-group" class="${className}" style="${animStyleStr}"${transformAttr}>`;
2139
+ content += `<g data-id="${node.id}" data-viz-role="node-group" class="${className}" style="${animStyleStr}"${transformAttr}${groupFilterAttr}>`;
2140
+ const resolvedExportNodeDash = resolveDasharray(node.style?.strokeDasharray);
2141
+ const exportShadowFilter = node.style?.shadow
2142
+ ? `url(#${shadowFilterId(resolveShadow(node.style.shadow))})`
2143
+ : undefined;
1923
2144
  const shapeStyleAttrs = svgAttributeString({
1924
2145
  fill: node.style?.fill ?? 'none',
1925
2146
  stroke: node.style?.stroke ?? '#111',
1926
2147
  'stroke-width': node.style?.strokeWidth ?? 2,
1927
2148
  opacity: node.runtime?.opacity !== undefined ? undefined : node.style?.opacity,
2149
+ 'stroke-dasharray': resolvedExportNodeDash || undefined,
2150
+ filter: exportShadowFilter,
1928
2151
  });
1929
2152
  // Shape
1930
2153
  content += shapeSvgMarkup(shape, { x, y }, shapeStyleAttrs);
@@ -1997,6 +2220,7 @@ class VizBuilderImpl {
1997
2220
  fill: node.label.fill,
1998
2221
  fontSize: node.label.fontSize,
1999
2222
  fontWeight: node.label.fontWeight,
2223
+ fontFamily: node.label.fontFamily,
2000
2224
  textAnchor: node.label.textAnchor || 'middle',
2001
2225
  dominantBaseline: node.label.dominantBaseline || 'middle',
2002
2226
  maxWidth: node.label.maxWidth,
@@ -2297,6 +2521,42 @@ class NodeBuilderImpl {
2297
2521
  };
2298
2522
  return this;
2299
2523
  }
2524
+ dashed() {
2525
+ this.nodeDef.style = {
2526
+ ...(this.nodeDef.style || {}),
2527
+ strokeDasharray: 'dashed',
2528
+ };
2529
+ return this;
2530
+ }
2531
+ dotted() {
2532
+ this.nodeDef.style = {
2533
+ ...(this.nodeDef.style || {}),
2534
+ strokeDasharray: 'dotted',
2535
+ };
2536
+ return this;
2537
+ }
2538
+ dash(pattern) {
2539
+ this.nodeDef.style = {
2540
+ ...(this.nodeDef.style || {}),
2541
+ strokeDasharray: pattern,
2542
+ };
2543
+ return this;
2544
+ }
2545
+ shadow(config) {
2546
+ this.nodeDef.style = {
2547
+ ...(this.nodeDef.style || {}),
2548
+ shadow: config ?? {},
2549
+ };
2550
+ return this;
2551
+ }
2552
+ sketch(config) {
2553
+ this.nodeDef.style = {
2554
+ ...(this.nodeDef.style || {}),
2555
+ sketch: true,
2556
+ sketchSeed: config?.seed,
2557
+ };
2558
+ return this;
2559
+ }
2300
2560
  class(name) {
2301
2561
  if (this.nodeDef.className) {
2302
2562
  this.nodeDef.className += ` ${name}`;
@@ -2522,6 +2782,13 @@ class EdgeBuilderImpl {
2522
2782
  };
2523
2783
  return this;
2524
2784
  }
2785
+ sketch() {
2786
+ this.edgeDef.style = {
2787
+ ...(this.edgeDef.style || {}),
2788
+ sketch: true,
2789
+ };
2790
+ return this;
2791
+ }
2525
2792
  class(name) {
2526
2793
  if (this.edgeDef.className) {
2527
2794
  this.edgeDef.className += ` ${name}`;
@@ -3,6 +3,146 @@ import { computeEdgePath, computeEdgeEndpoints, computeSelfLoop, } from './edgeP
3
3
  import { resolveEdgeLabelPosition, collectEdgeLabels } from './edgeLabels';
4
4
  import { resolveDasharray } from './edgeStyles';
5
5
  const svgNS = 'http://www.w3.org/2000/svg';
6
+ const SHADOW_DEFAULTS = {
7
+ dx: 2,
8
+ dy: 2,
9
+ blur: 4,
10
+ color: 'rgba(0,0,0,0.2)',
11
+ };
12
+ function resolveShadow(shadow) {
13
+ return {
14
+ dx: shadow.dx ?? SHADOW_DEFAULTS.dx,
15
+ dy: shadow.dy ?? SHADOW_DEFAULTS.dy,
16
+ blur: shadow.blur ?? SHADOW_DEFAULTS.blur,
17
+ color: shadow.color ?? SHADOW_DEFAULTS.color,
18
+ };
19
+ }
20
+ function shadowFilterId(cfg) {
21
+ const colorSuffix = cfg.color.replace(/[^a-zA-Z0-9]/g, '_');
22
+ return `viz-shadow-${cfg.dx}-${cfg.dy}-${cfg.blur}-${colorSuffix}`;
23
+ }
24
+ /** Lazily ensure a shadow `<filter>` definition exists in `<defs>`. */
25
+ function ensureShadowFilter(svg, shadow) {
26
+ const cfg = resolveShadow(shadow);
27
+ const fid = shadowFilterId(cfg);
28
+ if (!svg.querySelector(`#${CSS.escape(fid)}`)) {
29
+ const defs = svg.querySelector('defs');
30
+ if (defs) {
31
+ const filter = document.createElementNS(svgNS, 'filter');
32
+ filter.setAttribute('id', fid);
33
+ filter.setAttribute('x', '-50%');
34
+ filter.setAttribute('y', '-50%');
35
+ filter.setAttribute('width', '200%');
36
+ filter.setAttribute('height', '200%');
37
+ const drop = document.createElementNS(svgNS, 'feDropShadow');
38
+ drop.setAttribute('dx', String(cfg.dx));
39
+ drop.setAttribute('dy', String(cfg.dy));
40
+ drop.setAttribute('stdDeviation', String(cfg.blur));
41
+ drop.setAttribute('flood-color', cfg.color);
42
+ drop.setAttribute('flood-opacity', '1');
43
+ filter.appendChild(drop);
44
+ defs.appendChild(filter);
45
+ }
46
+ }
47
+ return fid;
48
+ }
49
+ function sketchFilterId(seed) {
50
+ return `viz-sketch-${seed}`;
51
+ }
52
+ /** Simple seeded float in [0, 1) derived from a seed via xorshift-like mix. */
53
+ function sketchRand(seed, salt) {
54
+ let s = ((seed ^ (salt * 2654435761)) >>> 0) | 1;
55
+ s ^= s << 13;
56
+ s ^= s >>> 17;
57
+ s ^= s << 5;
58
+ return (s >>> 0) / 4294967296;
59
+ }
60
+ /** Lerp a value between min and max using a seeded random. */
61
+ function sketchLerp(seed, salt, min, max) {
62
+ return min + sketchRand(seed, salt) * (max - min);
63
+ }
64
+ /** Lazily ensure a sketch `<filter>` definition exists in `<defs>`. */
65
+ function ensureSketchFilter(svg, seed) {
66
+ const fid = sketchFilterId(seed);
67
+ if (!svg.querySelector(`#${CSS.escape(fid)}`)) {
68
+ const defs = svg.querySelector('defs');
69
+ if (defs) {
70
+ const filter = document.createElementNS(svgNS, 'filter');
71
+ filter.setAttribute('id', fid);
72
+ filter.setAttribute('filterUnits', 'userSpaceOnUse');
73
+ filter.setAttribute('x', '-10000');
74
+ filter.setAttribute('y', '-10000');
75
+ filter.setAttribute('width', '20000');
76
+ filter.setAttribute('height', '20000');
77
+ const s2 = seed + 37;
78
+ // Derive unique per-seed parameters
79
+ const freq2 = sketchLerp(seed, 1, 0.009, 0.015).toFixed(4);
80
+ const scale1 = sketchLerp(seed, 2, 2.5, 4).toFixed(1);
81
+ const scale2 = sketchLerp(seed, 3, 3, 5).toFixed(1);
82
+ const dx = sketchLerp(seed, 4, 0.3, 1.6).toFixed(2);
83
+ const dy = sketchLerp(seed, 5, 0.2, 1.3).toFixed(2);
84
+ // First noise
85
+ const turb1 = document.createElementNS(svgNS, 'feTurbulence');
86
+ turb1.setAttribute('type', 'fractalNoise');
87
+ turb1.setAttribute('baseFrequency', '0.008');
88
+ turb1.setAttribute('numOctaves', '2');
89
+ turb1.setAttribute('seed', String(seed));
90
+ turb1.setAttribute('result', 'n1');
91
+ filter.appendChild(turb1);
92
+ // Second noise (different seed + frequency)
93
+ const turb2 = document.createElementNS(svgNS, 'feTurbulence');
94
+ turb2.setAttribute('type', 'fractalNoise');
95
+ turb2.setAttribute('baseFrequency', freq2);
96
+ turb2.setAttribute('numOctaves', '2');
97
+ turb2.setAttribute('seed', String(s2));
98
+ turb2.setAttribute('result', 'n2');
99
+ filter.appendChild(turb2);
100
+ // First stroke pass
101
+ const disp1 = document.createElementNS(svgNS, 'feDisplacementMap');
102
+ disp1.setAttribute('in', 'SourceGraphic');
103
+ disp1.setAttribute('in2', 'n1');
104
+ disp1.setAttribute('scale', scale1);
105
+ disp1.setAttribute('xChannelSelector', 'R');
106
+ disp1.setAttribute('yChannelSelector', 'G');
107
+ disp1.setAttribute('result', 's1');
108
+ filter.appendChild(disp1);
109
+ // Second stroke pass (different channels)
110
+ const disp2 = document.createElementNS(svgNS, 'feDisplacementMap');
111
+ disp2.setAttribute('in', 'SourceGraphic');
112
+ disp2.setAttribute('in2', 'n2');
113
+ disp2.setAttribute('scale', scale2);
114
+ disp2.setAttribute('xChannelSelector', 'G');
115
+ disp2.setAttribute('yChannelSelector', 'R');
116
+ disp2.setAttribute('result', 's2');
117
+ filter.appendChild(disp2);
118
+ // Offset second pass — variable gap
119
+ const offset = document.createElementNS(svgNS, 'feOffset');
120
+ offset.setAttribute('in', 's2');
121
+ offset.setAttribute('dx', dx);
122
+ offset.setAttribute('dy', dy);
123
+ offset.setAttribute('result', 's2off');
124
+ filter.appendChild(offset);
125
+ // Merge both passes
126
+ const comp = document.createElementNS(svgNS, 'feComposite');
127
+ comp.setAttribute('in', 's1');
128
+ comp.setAttribute('in2', 's2off');
129
+ comp.setAttribute('operator', 'over');
130
+ filter.appendChild(comp);
131
+ defs.appendChild(filter);
132
+ }
133
+ }
134
+ return fid;
135
+ }
136
+ /** Resolve the effective sketch seed for a node, falling back to the hash of its id. */
137
+ function resolveSketchSeed(nodeStyle, id) {
138
+ if (nodeStyle?.sketchSeed !== undefined)
139
+ return nodeStyle.sketchSeed;
140
+ let h = 0;
141
+ for (let i = 0; i < id.length; i++) {
142
+ h = (Math.imul(31, h) + id.charCodeAt(i)) | 0;
143
+ }
144
+ return Math.abs(h);
145
+ }
6
146
  /** Sanitise a CSS color for use as a marker ID suffix. */
7
147
  function colorToMarkerSuffix(color) {
8
148
  return color.replace(/[^a-zA-Z0-9]/g, '_');
@@ -371,6 +511,45 @@ export function patchRuntime(scene, ctx) {
371
511
  shape.removeAttribute('opacity');
372
512
  }
373
513
  }
514
+ // Stroke-dasharray: apply resolved value to shape.
515
+ if (node.style?.strokeDasharray !== undefined) {
516
+ const resolved = resolveDasharray(node.style.strokeDasharray);
517
+ if (resolved) {
518
+ shape.setAttribute('stroke-dasharray', resolved);
519
+ }
520
+ else {
521
+ shape.removeAttribute('stroke-dasharray');
522
+ }
523
+ }
524
+ else {
525
+ shape.removeAttribute('stroke-dasharray');
526
+ }
527
+ if (node.style?.shadow) {
528
+ const fid = ensureShadowFilter(ctx.svg, node.style.shadow);
529
+ shape.setAttribute('filter', `url(#${fid})`);
530
+ }
531
+ else {
532
+ shape.removeAttribute('filter');
533
+ }
534
+ const nodeSketched = node.style?.sketch || scene.sketch?.enabled;
535
+ if (nodeSketched) {
536
+ const seed = resolveSketchSeed(node.style, node.id);
537
+ const fid = ensureSketchFilter(ctx.svg, seed);
538
+ group.setAttribute('filter', `url(#${fid})`);
539
+ if (!group.classList.contains('viz-sketch')) {
540
+ group.classList.add('viz-sketch');
541
+ }
542
+ }
543
+ else {
544
+ if (group.classList.contains('viz-sketch')) {
545
+ group.classList.remove('viz-sketch');
546
+ }
547
+ // Only remove group filter if it was a sketch filter
548
+ const cur = group.getAttribute('filter');
549
+ if (cur && cur.startsWith('url(#viz-sketch-')) {
550
+ group.removeAttribute('filter');
551
+ }
552
+ }
374
553
  // Transform conflict rule: runtime wins if it writes transform.
375
554
  const scale = node.runtime?.scale;
376
555
  const rotation = node.runtime?.rotation;
@@ -502,6 +681,28 @@ export function patchRuntime(scene, ctx) {
502
681
  line.style.removeProperty('stroke-dashoffset');
503
682
  line.removeAttribute('stroke-dashoffset');
504
683
  }
684
+ const edgeSketched = edge.style?.sketch || scene.sketch?.enabled;
685
+ if (edgeSketched) {
686
+ let h = 0;
687
+ for (let i = 0; i < edge.id.length; i++) {
688
+ h = (Math.imul(31, h) + edge.id.charCodeAt(i)) | 0;
689
+ }
690
+ const seed = Math.abs(h);
691
+ const fid = ensureSketchFilter(ctx.svg, seed);
692
+ line.setAttribute('filter', `url(#${fid})`);
693
+ if (!group.classList.contains('viz-sketch')) {
694
+ group.classList.add('viz-sketch');
695
+ }
696
+ }
697
+ else {
698
+ const cur = line.getAttribute('filter');
699
+ if (cur && cur.startsWith('url(#viz-sketch-')) {
700
+ line.removeAttribute('filter');
701
+ }
702
+ if (group.classList.contains('viz-sketch')) {
703
+ group.classList.remove('viz-sketch');
704
+ }
705
+ }
505
706
  }
506
707
  // Ensure DOM order matches zIndex order for node layer children
507
708
  // We use insertBefore to minimize DOM thrashing in the animation loop
package/dist/styles.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n text-anchor: middle;\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(0);\n}\n\n.viz-canvas svg {\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n/* Keyframes */\n@keyframes vizFlow {\n from {\n stroke-dashoffset: 20;\n }\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Animation Classes */\n\n/* Flow Animation (Dashed line moving) */\n.viz-anim-flow .viz-edge {\n stroke-dasharray: 5, 5;\n animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;\n}\n\n/* Edge base styling (path elements need explicit fill:none) */\n.viz-edge {\n fill: none;\n stroke: currentColor;\n}\n\n.viz-edge-hit {\n fill: none;\n}\n\n/* Node Transition */\n.viz-node-group {\n transition: transform 0.3s ease-out, opacity 0.3s ease-out;\n}\n\n/* Overlay Classes */\n.viz-grid-label {\n fill: #6B7280;\n font-size: 14px;\n font-weight: 600;\n opacity: 1;\n}\n\n.viz-signal {\n fill: #3B82F6;\n cursor: pointer;\n pointer-events: all; \n transition: transform 0.2s ease-out, fill 0.2s ease-out;\n}\n\n.viz-signal .viz-signal-shape {\n fill: inherit;\n}\n\n.viz-signal:hover {\n fill: #60A5FA;\n transform: scale(1.5);\n}\n\n.viz-data-point {\n fill: #F59E0B;\n transition: cx 0.3s ease-out, cy 0.3s ease-out;\n}\n\n/* Connection ports (hidden by default, shown on node hover) */\n.viz-port {\n fill: #3B82F6;\n stroke: white;\n stroke-width: 1.5;\n opacity: 0;\n pointer-events: all;\n cursor: crosshair;\n transition: opacity 0.15s ease-out;\n}\n.viz-node-group:hover .viz-port {\n opacity: 1;\n}\n";
1
+ export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n text-anchor: middle;\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(0);\n}\n\n.viz-canvas svg {\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n/* Keyframes */\n@keyframes vizFlow {\n from {\n stroke-dashoffset: 20;\n }\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Animation Classes */\n\n/* Flow Animation (Dashed line moving) */\n.viz-anim-flow .viz-edge {\n stroke-dasharray: 5, 5;\n animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;\n}\n\n/* Edge base styling (path elements need explicit fill:none) */\n.viz-edge {\n fill: none;\n stroke: currentColor;\n}\n\n.viz-edge-hit {\n fill: none;\n}\n\n/* Node Transition */\n.viz-node-group {\n transition: transform 0.3s ease-out, opacity 0.3s ease-out;\n}\n\n/* Overlay Classes */\n.viz-grid-label {\n fill: #6B7280;\n font-size: 14px;\n font-weight: 600;\n opacity: 1;\n}\n\n.viz-signal {\n fill: #3B82F6;\n cursor: pointer;\n pointer-events: all; \n transition: transform 0.2s ease-out, fill 0.2s ease-out;\n}\n\n.viz-signal .viz-signal-shape {\n fill: inherit;\n}\n\n.viz-signal:hover {\n fill: #60A5FA;\n transform: scale(1.5);\n}\n\n.viz-data-point {\n fill: #F59E0B;\n transition: cx 0.3s ease-out, cy 0.3s ease-out;\n}\n\n/* Connection ports (hidden by default, shown on node hover) */\n.viz-port {\n fill: #3B82F6;\n stroke: white;\n stroke-width: 1.5;\n opacity: 0;\n pointer-events: all;\n cursor: crosshair;\n transition: opacity 0.15s ease-out;\n}\n.viz-node-group:hover .viz-port {\n opacity: 1;\n}\n\n/* Sketch / hand-drawn rendering */\n.viz-sketch {\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-width: 2;\n}\n";
package/dist/styles.js CHANGED
@@ -96,4 +96,11 @@ export const DEFAULT_VIZ_CSS = `
96
96
  .viz-node-group:hover .viz-port {
97
97
  opacity: 1;
98
98
  }
99
+
100
+ /* Sketch / hand-drawn rendering */
101
+ .viz-sketch {
102
+ stroke-linecap: round;
103
+ stroke-linejoin: round;
104
+ stroke-width: 2;
105
+ }
99
106
  `;
@@ -19,6 +19,7 @@ export interface RenderTextOptions {
19
19
  fill?: string;
20
20
  fontSize?: number | string;
21
21
  fontWeight?: number | string;
22
+ fontFamily?: string;
22
23
  textAnchor?: 'start' | 'middle' | 'end';
23
24
  dominantBaseline?: string;
24
25
  maxWidth?: number;
package/dist/textUtils.js CHANGED
@@ -63,7 +63,7 @@ export const DEFAULT_LINE_HEIGHT = 1.2;
63
63
  * @param options layout and styling options
64
64
  */
65
65
  export function renderSvgText(x, y, text, options = {}) {
66
- const { className = '', fill, fontSize, fontWeight, textAnchor = 'middle', dominantBaseline = 'middle', maxWidth, lineHeight = DEFAULT_LINE_HEIGHT, verticalAlign = 'middle', overflow, } = options;
66
+ const { className = '', fill, fontSize, fontWeight, fontFamily, textAnchor = 'middle', dominantBaseline = 'middle', maxWidth, lineHeight = DEFAULT_LINE_HEIGHT, verticalAlign = 'middle', overflow, } = options;
67
67
  // Resolve numeric font size for wrapping
68
68
  let numericFontSize = 12; // fallback
69
69
  if (typeof fontSize === 'number') {
@@ -106,6 +106,8 @@ export function renderSvgText(x, y, text, options = {}) {
106
106
  attrs.push(`font-size="${fontSize}"`);
107
107
  if (fontWeight !== undefined)
108
108
  attrs.push(`font-weight="${fontWeight}"`);
109
+ if (fontFamily !== undefined)
110
+ attrs.push(`font-family="${escapeXmlAttr(fontFamily)}"`);
109
111
  attrs.push(`text-anchor="${textAnchor}"`);
110
112
  // Only apply dominant-baseline if it evaluates to a single line safely,
111
113
  // or apply to the group. Usually best on the <text> element.
package/dist/types.d.ts CHANGED
@@ -188,6 +188,7 @@ export type NodeLabel = {
188
188
  fill?: string;
189
189
  fontSize?: number | string;
190
190
  fontWeight?: number | string;
191
+ fontFamily?: string;
191
192
  textAnchor?: 'start' | 'middle' | 'end';
192
193
  dominantBaseline?: string;
193
194
  /** Maximum width for text wrapping (in px). If set, text wraps within this width. */
@@ -313,6 +314,31 @@ export interface VizNode {
313
314
  stroke?: string;
314
315
  strokeWidth?: number;
315
316
  opacity?: number;
317
+ /**
318
+ * SVG `stroke-dasharray` value.
319
+ * Use the presets `'dashed'` (`8,4`), `'dotted'` (`2,4`), `'dash-dot'` (`8,4,2,4`),
320
+ * or pass any valid SVG dasharray string (e.g. `'12, 3, 3, 3'`).
321
+ * `'solid'` (or omitting the property) renders a continuous stroke.
322
+ */
323
+ strokeDasharray?: 'solid' | 'dashed' | 'dotted' | 'dash-dot' | string;
324
+ /**
325
+ * Drop shadow rendered behind the node shape via an SVG `<filter>`.
326
+ *
327
+ * - `dx` — horizontal offset (default `2`)
328
+ * - `dy` — vertical offset (default `2`)
329
+ * - `blur` — Gaussian blur radius / stdDeviation (default `4`)
330
+ * - `color` — shadow color (default `'rgba(0,0,0,0.2)'`)
331
+ */
332
+ shadow?: {
333
+ dx?: number;
334
+ dy?: number;
335
+ blur?: number;
336
+ color?: string;
337
+ };
338
+ /** Render the node with a hand-drawn / sketchy appearance. */
339
+ sketch?: boolean;
340
+ /** Seed for deterministic sketch jitter (same seed → same wobble). */
341
+ sketchSeed?: number;
316
342
  };
317
343
  className?: string;
318
344
  data?: unknown;
@@ -344,6 +370,10 @@ export interface EdgeLabel {
344
370
  className?: string;
345
371
  dx?: number;
346
372
  dy?: number;
373
+ fill?: string;
374
+ fontSize?: number | string;
375
+ fontWeight?: number | string;
376
+ fontFamily?: string;
347
377
  /** Maximum width for text wrapping (in px). If set, text wraps within this width. */
348
378
  maxWidth?: number;
349
379
  /** Line height multiplier (default: 1.2) */
@@ -407,6 +437,8 @@ export interface VizEdge {
407
437
  * `'solid'` (or omitting the property) renders a continuous stroke.
408
438
  */
409
439
  strokeDasharray?: 'solid' | 'dashed' | 'dotted' | 'dash-dot' | string;
440
+ /** Render the edge with a hand-drawn / sketchy appearance. */
441
+ sketch?: boolean;
410
442
  };
411
443
  className?: string;
412
444
  hitArea?: number;
@@ -561,6 +593,25 @@ export interface NodeOptions {
561
593
  width?: number;
562
594
  };
563
595
  opacity?: number;
596
+ /** Dash pattern preset or custom SVG dasharray string. */
597
+ dash?: 'solid' | 'dashed' | 'dotted' | 'dash-dot' | string;
598
+ /**
599
+ * Drop shadow behind the node shape.
600
+ * `true` for default shadow, or a config object `{ dx, dy, blur, color }`.
601
+ */
602
+ shadow?: boolean | {
603
+ dx?: number;
604
+ dy?: number;
605
+ blur?: number;
606
+ color?: string;
607
+ };
608
+ /**
609
+ * Hand-drawn / sketchy rendering for this node.
610
+ * `true` uses a default seed; pass `{ seed }` for deterministic jitter.
611
+ */
612
+ sketch?: boolean | {
613
+ seed?: number;
614
+ };
564
615
  /** Explicit render order. Higher values render on top. Default: 0. */
565
616
  zIndex?: number;
566
617
  className?: string;
@@ -608,6 +659,8 @@ export interface EdgeOptions {
608
659
  opacity?: number;
609
660
  /** Dash pattern preset or custom SVG dasharray string. */
610
661
  dash?: 'solid' | 'dashed' | 'dotted' | 'dash-dot' | string;
662
+ /** Hand-drawn / sketchy rendering for this edge. */
663
+ sketch?: boolean;
611
664
  className?: string;
612
665
  anchor?: 'center' | 'boundary';
613
666
  fromPort?: string;
@@ -697,6 +750,11 @@ export type VizScene = {
697
750
  * Generated by the fluent AnimationBuilder API.
698
751
  */
699
752
  animationSpecs?: AnimationSpec[];
753
+ /** Global sketch / hand-drawn rendering mode. Applies to all nodes and edges. */
754
+ sketch?: {
755
+ enabled?: boolean;
756
+ seed?: number;
757
+ };
700
758
  };
701
759
  export interface PanZoomOptions {
702
760
  /** Enable pan & zoom (default: false) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vizcraft",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A fluent, type-safe SVG scene builder for composing nodes, edges, animations, and overlays with incremental DOM updates and no framework dependency.",
5
5
  "keywords": [
6
6
  "visualization",