vizcraft 1.2.0 → 1.3.1
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 +12 -0
- package/README.md +13 -0
- package/dist/builder.d.ts +25 -0
- package/dist/builder.js +278 -12
- package/dist/runtimePatcher.js +191 -0
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +7 -0
- package/dist/textUtils.d.ts +1 -0
- package/dist/textUtils.js +3 -1
- package/dist/types.d.ts +58 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# vizcraft
|
|
2
2
|
|
|
3
|
+
## 1.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`086ef9e`](https://github.com/ChipiKaf/vizcraft/commit/086ef9e7b5d505cbb03f955e8d24297fb60a6b3e) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - The fix prevents commit() from using a stale cached runtimePatchCtx (which could reference detached DOM elements) by always recreating it after \_renderSceneToDOM, and removes the redundant strokeDasharray write from patchRuntime so that base style is owned by a single write path.
|
|
8
|
+
|
|
9
|
+
## 1.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`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)
|
|
14
|
+
|
|
3
15
|
## 1.2.0
|
|
4
16
|
|
|
5
17
|
### 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;
|
|
@@ -575,14 +654,13 @@ class VizBuilderImpl {
|
|
|
575
654
|
// The reconciliation correctly re-uses existing SVG elements, inserts new ones, and deletes missing ones.
|
|
576
655
|
const scene = this.build();
|
|
577
656
|
this._renderSceneToDOM(scene, container);
|
|
578
|
-
// Apply runtime overrides (if any)
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
657
|
+
// Apply runtime overrides (if any).
|
|
658
|
+
// Always recreate the context after _renderSceneToDOM so patchRuntime
|
|
659
|
+
// never references stale / detached elements (fixes #81).
|
|
660
|
+
const ctx = createRuntimePatchCtx(svg, {
|
|
661
|
+
edgePathResolver: this._edgePathResolver,
|
|
662
|
+
});
|
|
663
|
+
runtimePatchCtxBySvg.set(svg, ctx);
|
|
586
664
|
patchRuntime(scene, ctx);
|
|
587
665
|
}
|
|
588
666
|
/**
|
|
@@ -656,6 +734,10 @@ class VizBuilderImpl {
|
|
|
656
734
|
this._gridConfig = { cols, rows, padding };
|
|
657
735
|
return this;
|
|
658
736
|
}
|
|
737
|
+
sketch(enabled = true, seed) {
|
|
738
|
+
this._sketch = enabled ? { enabled: true, seed } : null;
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
659
741
|
overlay(arg1, arg2, arg3) {
|
|
660
742
|
if (typeof arg1 === 'function') {
|
|
661
743
|
const overlay = new OverlayBuilder();
|
|
@@ -740,6 +822,9 @@ class VizBuilderImpl {
|
|
|
740
822
|
if (scene.animationSpecs) {
|
|
741
823
|
this._animationSpecs = [...scene.animationSpecs];
|
|
742
824
|
}
|
|
825
|
+
this._sketch = scene.sketch
|
|
826
|
+
? { ...scene.sketch, enabled: scene.sketch.enabled ?? true }
|
|
827
|
+
: null;
|
|
743
828
|
return this;
|
|
744
829
|
}
|
|
745
830
|
/**
|
|
@@ -764,6 +849,7 @@ class VizBuilderImpl {
|
|
|
764
849
|
edges,
|
|
765
850
|
overlays: this._overlays,
|
|
766
851
|
animationSpecs: this._animationSpecs.length > 0 ? [...this._animationSpecs] : undefined,
|
|
852
|
+
sketch: this._sketch ?? undefined,
|
|
767
853
|
};
|
|
768
854
|
this._dispatchEvent('build', { scene });
|
|
769
855
|
return scene;
|
|
@@ -1100,6 +1186,49 @@ class VizBuilderImpl {
|
|
|
1100
1186
|
}
|
|
1101
1187
|
defs.appendChild(markerEl);
|
|
1102
1188
|
});
|
|
1189
|
+
const neededShadows = new Map();
|
|
1190
|
+
nodes.forEach((n) => {
|
|
1191
|
+
if (n.style?.shadow) {
|
|
1192
|
+
const cfg = resolveShadow(n.style.shadow);
|
|
1193
|
+
const fid = shadowFilterId(cfg);
|
|
1194
|
+
if (!neededShadows.has(fid)) {
|
|
1195
|
+
neededShadows.set(fid, cfg);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
neededShadows.forEach((cfg, fid) => {
|
|
1200
|
+
const tmp = document.createElementNS(svgNS, 'svg');
|
|
1201
|
+
tmp.innerHTML = shadowFilterSvg(fid, cfg);
|
|
1202
|
+
const filterEl = tmp.querySelector('filter');
|
|
1203
|
+
if (filterEl) {
|
|
1204
|
+
defs.appendChild(filterEl);
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
const neededSketchSeeds = new Set();
|
|
1208
|
+
const globalSketch = scene.sketch?.enabled;
|
|
1209
|
+
nodes.forEach((n) => {
|
|
1210
|
+
if (n.style?.sketch || globalSketch) {
|
|
1211
|
+
neededSketchSeeds.add(resolveSketchSeed(n.style, n.id));
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
edges.forEach((e) => {
|
|
1215
|
+
if (e.style?.sketch || globalSketch) {
|
|
1216
|
+
let h = 0;
|
|
1217
|
+
for (let i = 0; i < e.id.length; i++) {
|
|
1218
|
+
h = (Math.imul(31, h) + e.id.charCodeAt(i)) | 0;
|
|
1219
|
+
}
|
|
1220
|
+
neededSketchSeeds.add(Math.abs(h));
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
neededSketchSeeds.forEach((seed) => {
|
|
1224
|
+
const fid = sketchFilterId(seed);
|
|
1225
|
+
const tmp = document.createElementNS(svgNS, 'svg');
|
|
1226
|
+
tmp.innerHTML = sketchFilterSvg(fid, seed);
|
|
1227
|
+
const filterEl = tmp.querySelector('filter');
|
|
1228
|
+
if (filterEl) {
|
|
1229
|
+
defs.appendChild(filterEl);
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1103
1232
|
svg.appendChild(defs);
|
|
1104
1233
|
// Layers
|
|
1105
1234
|
const viewport = document.createElementNS(svgNS, 'g');
|
|
@@ -1162,6 +1291,9 @@ class VizBuilderImpl {
|
|
|
1162
1291
|
}
|
|
1163
1292
|
// Compute Classes & Styles
|
|
1164
1293
|
let classes = `viz-edge-group ${edge.className || ''}`;
|
|
1294
|
+
const edgeSketched = edge.style?.sketch || scene.sketch?.enabled;
|
|
1295
|
+
if (edgeSketched)
|
|
1296
|
+
classes += ' viz-sketch';
|
|
1165
1297
|
// Reset styles
|
|
1166
1298
|
group.removeAttribute('style');
|
|
1167
1299
|
if (edge.animations) {
|
|
@@ -1281,6 +1413,17 @@ class VizBuilderImpl {
|
|
|
1281
1413
|
else {
|
|
1282
1414
|
line.style.removeProperty('stroke-dasharray');
|
|
1283
1415
|
}
|
|
1416
|
+
if (edgeSketched) {
|
|
1417
|
+
let h = 0;
|
|
1418
|
+
for (let i = 0; i < edge.id.length; i++) {
|
|
1419
|
+
h = (Math.imul(31, h) + edge.id.charCodeAt(i)) | 0;
|
|
1420
|
+
}
|
|
1421
|
+
const seed = Math.abs(h);
|
|
1422
|
+
line.setAttribute('filter', `url(#${sketchFilterId(seed)})`);
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
line.removeAttribute('filter');
|
|
1426
|
+
}
|
|
1284
1427
|
const oldHit = group.querySelector('[data-viz-role="edge-hit"]') ||
|
|
1285
1428
|
group.querySelector('.viz-edge-hit');
|
|
1286
1429
|
if (oldHit)
|
|
@@ -1311,6 +1454,10 @@ class VizBuilderImpl {
|
|
|
1311
1454
|
const labelClass = `viz-edge-label ${lbl.className || ''}`;
|
|
1312
1455
|
const edgeLabelSvg = renderSvgText(pos.x, pos.y, lbl.rich ?? lbl.text, {
|
|
1313
1456
|
className: labelClass,
|
|
1457
|
+
fill: lbl.fill,
|
|
1458
|
+
fontSize: lbl.fontSize,
|
|
1459
|
+
fontWeight: lbl.fontWeight,
|
|
1460
|
+
fontFamily: lbl.fontFamily,
|
|
1314
1461
|
textAnchor: 'middle',
|
|
1315
1462
|
dominantBaseline: 'middle',
|
|
1316
1463
|
maxWidth: lbl.maxWidth,
|
|
@@ -1370,6 +1517,9 @@ class VizBuilderImpl {
|
|
|
1370
1517
|
const isContainer = !!node.container;
|
|
1371
1518
|
// Calculate Anim Classes
|
|
1372
1519
|
let classes = `viz-node-group${isContainer ? ' viz-container' : ''} ${node.className || ''}`;
|
|
1520
|
+
const nodeSketched = node.style?.sketch || scene.sketch?.enabled;
|
|
1521
|
+
if (nodeSketched)
|
|
1522
|
+
classes += ' viz-sketch';
|
|
1373
1523
|
group.removeAttribute('style');
|
|
1374
1524
|
if (node.animations) {
|
|
1375
1525
|
node.animations.forEach((spec) => {
|
|
@@ -1426,12 +1576,25 @@ class VizBuilderImpl {
|
|
|
1426
1576
|
group.prepend(shape);
|
|
1427
1577
|
}
|
|
1428
1578
|
applyShapeGeometry(shape, node.shape, { x, y });
|
|
1579
|
+
const resolvedNodeDash = resolveDasharray(node.style?.strokeDasharray);
|
|
1580
|
+
const nodeShadowFilter = node.style?.shadow
|
|
1581
|
+
? `url(#${shadowFilterId(resolveShadow(node.style.shadow))})`
|
|
1582
|
+
: undefined;
|
|
1429
1583
|
setSvgAttributes(shape, {
|
|
1430
1584
|
fill: node.style?.fill ?? 'none',
|
|
1431
1585
|
stroke: node.style?.stroke ?? '#111',
|
|
1432
1586
|
'stroke-width': node.style?.strokeWidth ?? 2,
|
|
1433
1587
|
opacity: node.runtime?.opacity ?? node.style?.opacity,
|
|
1588
|
+
'stroke-dasharray': resolvedNodeDash || undefined,
|
|
1589
|
+
filter: nodeShadowFilter,
|
|
1434
1590
|
});
|
|
1591
|
+
if (nodeSketched) {
|
|
1592
|
+
const seed = resolveSketchSeed(node.style, node.id);
|
|
1593
|
+
group.setAttribute('filter', `url(#${sketchFilterId(seed)})`);
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
group.removeAttribute('filter');
|
|
1597
|
+
}
|
|
1435
1598
|
// Embedded media (rendered alongside the base shape)
|
|
1436
1599
|
const existingImg = group.querySelector(':scope > [data-viz-role="node-image"], :scope > .viz-node-image');
|
|
1437
1600
|
if (existingImg)
|
|
@@ -1558,6 +1721,7 @@ class VizBuilderImpl {
|
|
|
1558
1721
|
fill: node.label.fill,
|
|
1559
1722
|
fontSize: node.label.fontSize,
|
|
1560
1723
|
fontWeight: node.label.fontWeight,
|
|
1724
|
+
fontFamily: node.label.fontFamily,
|
|
1561
1725
|
textAnchor: node.label.textAnchor || 'middle',
|
|
1562
1726
|
dominantBaseline: node.label.dominantBaseline || 'middle',
|
|
1563
1727
|
maxWidth: node.label.maxWidth,
|
|
@@ -1744,6 +1908,38 @@ class VizBuilderImpl {
|
|
|
1744
1908
|
}
|
|
1745
1909
|
}
|
|
1746
1910
|
});
|
|
1911
|
+
const exportShadows = new Map();
|
|
1912
|
+
exportNodes.forEach((n) => {
|
|
1913
|
+
if (n.style?.shadow) {
|
|
1914
|
+
const cfg = resolveShadow(n.style.shadow);
|
|
1915
|
+
const fid = shadowFilterId(cfg);
|
|
1916
|
+
if (!exportShadows.has(fid)) {
|
|
1917
|
+
exportShadows.set(fid, cfg);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
exportShadows.forEach((cfg, fid) => {
|
|
1922
|
+
svgContent += shadowFilterSvg(fid, cfg);
|
|
1923
|
+
});
|
|
1924
|
+
const exportSketchSeeds = new Set();
|
|
1925
|
+
const globalSketchExport = exportScene.sketch?.enabled;
|
|
1926
|
+
exportNodes.forEach((n) => {
|
|
1927
|
+
if (n.style?.sketch || globalSketchExport) {
|
|
1928
|
+
exportSketchSeeds.add(resolveSketchSeed(n.style, n.id));
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
exportEdges.forEach((e) => {
|
|
1932
|
+
if (e.style?.sketch || globalSketchExport) {
|
|
1933
|
+
let h = 0;
|
|
1934
|
+
for (let i = 0; i < e.id.length; i++) {
|
|
1935
|
+
h = (Math.imul(31, h) + e.id.charCodeAt(i)) | 0;
|
|
1936
|
+
}
|
|
1937
|
+
exportSketchSeeds.add(Math.abs(h));
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
exportSketchSeeds.forEach((seed) => {
|
|
1941
|
+
svgContent += sketchFilterSvg(sketchFilterId(seed), seed);
|
|
1942
|
+
});
|
|
1747
1943
|
svgContent += `
|
|
1748
1944
|
</defs>`;
|
|
1749
1945
|
// Render Edges
|
|
@@ -1825,7 +2021,9 @@ class VizBuilderImpl {
|
|
|
1825
2021
|
lineRuntimeStyle += `stroke-dashoffset: ${edge.runtime.strokeDashoffset}; `;
|
|
1826
2022
|
lineRuntimeAttrs += ` stroke-dashoffset="${edge.runtime.strokeDashoffset}"`;
|
|
1827
2023
|
}
|
|
1828
|
-
|
|
2024
|
+
const edgeSketched = edge.style?.sketch || globalSketchExport;
|
|
2025
|
+
const sketchClass = edgeSketched ? ' viz-sketch' : '';
|
|
2026
|
+
svgContent += `<g data-id="${edge.id}" data-viz-role="edge-group" class="viz-edge-group${sketchClass} ${edge.className || ''} ${animClasses}" style="${animStyleStr}${runtimeStyle}">`;
|
|
1829
2027
|
let edgeInlineStyle = lineRuntimeStyle;
|
|
1830
2028
|
if (edge.style?.stroke !== undefined)
|
|
1831
2029
|
edgeInlineStyle += `stroke: ${edge.style.stroke}; `;
|
|
@@ -1841,7 +2039,15 @@ class VizBuilderImpl {
|
|
|
1841
2039
|
if (resolved)
|
|
1842
2040
|
edgeInlineStyle += `stroke-dasharray: ${resolved}; `;
|
|
1843
2041
|
}
|
|
1844
|
-
|
|
2042
|
+
let edgeSketchFilterAttr = '';
|
|
2043
|
+
if (edgeSketched) {
|
|
2044
|
+
let h = 0;
|
|
2045
|
+
for (let i = 0; i < edge.id.length; i++) {
|
|
2046
|
+
h = (Math.imul(31, h) + edge.id.charCodeAt(i)) | 0;
|
|
2047
|
+
}
|
|
2048
|
+
edgeSketchFilterAttr = ` filter="url(#${sketchFilterId(Math.abs(h))})"`;
|
|
2049
|
+
}
|
|
2050
|
+
svgContent += `<path d="${edgePath.d}" class="viz-edge" data-viz-role="edge-line" ${markerEnd} ${markerStart} style="${edgeInlineStyle}"${lineRuntimeAttrs}${edgeSketchFilterAttr} />`;
|
|
1845
2051
|
// Edge Labels (multi-position)
|
|
1846
2052
|
const allLabels = collectEdgeLabels(edge);
|
|
1847
2053
|
allLabels.forEach((lbl, idx) => {
|
|
@@ -1849,6 +2055,10 @@ class VizBuilderImpl {
|
|
|
1849
2055
|
const labelClass = `viz-edge-label ${lbl.className || ''}`;
|
|
1850
2056
|
const edgeLabelSvg = renderSvgText(pos.x, pos.y, lbl.rich ?? lbl.text, {
|
|
1851
2057
|
className: labelClass,
|
|
2058
|
+
fill: lbl.fill,
|
|
2059
|
+
fontSize: lbl.fontSize,
|
|
2060
|
+
fontWeight: lbl.fontWeight,
|
|
2061
|
+
fontFamily: lbl.fontFamily,
|
|
1852
2062
|
textAnchor: 'middle',
|
|
1853
2063
|
dominantBaseline: 'middle',
|
|
1854
2064
|
maxWidth: lbl.maxWidth,
|
|
@@ -1913,18 +2123,30 @@ class VizBuilderImpl {
|
|
|
1913
2123
|
});
|
|
1914
2124
|
}
|
|
1915
2125
|
const isContainer = !!node.container;
|
|
1916
|
-
const
|
|
2126
|
+
const nodeSketched = node.style?.sketch || globalSketchExport;
|
|
2127
|
+
const className = `viz-node-group${isContainer ? ' viz-container' : ''}${nodeSketched ? ' viz-sketch' : ''} ${node.className || ''} ${animClasses}`;
|
|
1917
2128
|
const scale = node.runtime?.scale;
|
|
1918
2129
|
const rotation = node.runtime?.rotation;
|
|
2130
|
+
let groupFilterAttr = '';
|
|
2131
|
+
if (nodeSketched) {
|
|
2132
|
+
const seed = resolveSketchSeed(node.style, node.id);
|
|
2133
|
+
groupFilterAttr = ` filter="url(#${sketchFilterId(seed)})"`;
|
|
2134
|
+
}
|
|
1919
2135
|
const transformAttr = scale !== undefined || rotation !== undefined
|
|
1920
2136
|
? ` transform="translate(${x} ${y}) rotate(${rotation ?? 0}) scale(${scale ?? 1}) translate(${-x} ${-y})"`
|
|
1921
2137
|
: '';
|
|
1922
|
-
content += `<g data-id="${node.id}" data-viz-role="node-group" class="${className}" style="${animStyleStr}"${transformAttr}>`;
|
|
2138
|
+
content += `<g data-id="${node.id}" data-viz-role="node-group" class="${className}" style="${animStyleStr}"${transformAttr}${groupFilterAttr}>`;
|
|
2139
|
+
const resolvedExportNodeDash = resolveDasharray(node.style?.strokeDasharray);
|
|
2140
|
+
const exportShadowFilter = node.style?.shadow
|
|
2141
|
+
? `url(#${shadowFilterId(resolveShadow(node.style.shadow))})`
|
|
2142
|
+
: undefined;
|
|
1923
2143
|
const shapeStyleAttrs = svgAttributeString({
|
|
1924
2144
|
fill: node.style?.fill ?? 'none',
|
|
1925
2145
|
stroke: node.style?.stroke ?? '#111',
|
|
1926
2146
|
'stroke-width': node.style?.strokeWidth ?? 2,
|
|
1927
2147
|
opacity: node.runtime?.opacity !== undefined ? undefined : node.style?.opacity,
|
|
2148
|
+
'stroke-dasharray': resolvedExportNodeDash || undefined,
|
|
2149
|
+
filter: exportShadowFilter,
|
|
1928
2150
|
});
|
|
1929
2151
|
// Shape
|
|
1930
2152
|
content += shapeSvgMarkup(shape, { x, y }, shapeStyleAttrs);
|
|
@@ -1997,6 +2219,7 @@ class VizBuilderImpl {
|
|
|
1997
2219
|
fill: node.label.fill,
|
|
1998
2220
|
fontSize: node.label.fontSize,
|
|
1999
2221
|
fontWeight: node.label.fontWeight,
|
|
2222
|
+
fontFamily: node.label.fontFamily,
|
|
2000
2223
|
textAnchor: node.label.textAnchor || 'middle',
|
|
2001
2224
|
dominantBaseline: node.label.dominantBaseline || 'middle',
|
|
2002
2225
|
maxWidth: node.label.maxWidth,
|
|
@@ -2297,6 +2520,42 @@ class NodeBuilderImpl {
|
|
|
2297
2520
|
};
|
|
2298
2521
|
return this;
|
|
2299
2522
|
}
|
|
2523
|
+
dashed() {
|
|
2524
|
+
this.nodeDef.style = {
|
|
2525
|
+
...(this.nodeDef.style || {}),
|
|
2526
|
+
strokeDasharray: 'dashed',
|
|
2527
|
+
};
|
|
2528
|
+
return this;
|
|
2529
|
+
}
|
|
2530
|
+
dotted() {
|
|
2531
|
+
this.nodeDef.style = {
|
|
2532
|
+
...(this.nodeDef.style || {}),
|
|
2533
|
+
strokeDasharray: 'dotted',
|
|
2534
|
+
};
|
|
2535
|
+
return this;
|
|
2536
|
+
}
|
|
2537
|
+
dash(pattern) {
|
|
2538
|
+
this.nodeDef.style = {
|
|
2539
|
+
...(this.nodeDef.style || {}),
|
|
2540
|
+
strokeDasharray: pattern,
|
|
2541
|
+
};
|
|
2542
|
+
return this;
|
|
2543
|
+
}
|
|
2544
|
+
shadow(config) {
|
|
2545
|
+
this.nodeDef.style = {
|
|
2546
|
+
...(this.nodeDef.style || {}),
|
|
2547
|
+
shadow: config ?? {},
|
|
2548
|
+
};
|
|
2549
|
+
return this;
|
|
2550
|
+
}
|
|
2551
|
+
sketch(config) {
|
|
2552
|
+
this.nodeDef.style = {
|
|
2553
|
+
...(this.nodeDef.style || {}),
|
|
2554
|
+
sketch: true,
|
|
2555
|
+
sketchSeed: config?.seed,
|
|
2556
|
+
};
|
|
2557
|
+
return this;
|
|
2558
|
+
}
|
|
2300
2559
|
class(name) {
|
|
2301
2560
|
if (this.nodeDef.className) {
|
|
2302
2561
|
this.nodeDef.className += ` ${name}`;
|
|
@@ -2522,6 +2781,13 @@ class EdgeBuilderImpl {
|
|
|
2522
2781
|
};
|
|
2523
2782
|
return this;
|
|
2524
2783
|
}
|
|
2784
|
+
sketch() {
|
|
2785
|
+
this.edgeDef.style = {
|
|
2786
|
+
...(this.edgeDef.style || {}),
|
|
2787
|
+
sketch: true,
|
|
2788
|
+
};
|
|
2789
|
+
return this;
|
|
2790
|
+
}
|
|
2525
2791
|
class(name) {
|
|
2526
2792
|
if (this.edgeDef.className) {
|
|
2527
2793
|
this.edgeDef.className += ` ${name}`;
|
package/dist/runtimePatcher.js
CHANGED
|
@@ -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,35 @@ export function patchRuntime(scene, ctx) {
|
|
|
371
511
|
shape.removeAttribute('opacity');
|
|
372
512
|
}
|
|
373
513
|
}
|
|
514
|
+
// NOTE: strokeDasharray is a static base style — written exclusively by
|
|
515
|
+
// _renderSceneToDOM via setSvgAttributes. patchRuntime must NOT duplicate
|
|
516
|
+
// that write to avoid the stale-context overwrite described in #81.
|
|
517
|
+
if (node.style?.shadow) {
|
|
518
|
+
const fid = ensureShadowFilter(ctx.svg, node.style.shadow);
|
|
519
|
+
shape.setAttribute('filter', `url(#${fid})`);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
shape.removeAttribute('filter');
|
|
523
|
+
}
|
|
524
|
+
const nodeSketched = node.style?.sketch || scene.sketch?.enabled;
|
|
525
|
+
if (nodeSketched) {
|
|
526
|
+
const seed = resolveSketchSeed(node.style, node.id);
|
|
527
|
+
const fid = ensureSketchFilter(ctx.svg, seed);
|
|
528
|
+
group.setAttribute('filter', `url(#${fid})`);
|
|
529
|
+
if (!group.classList.contains('viz-sketch')) {
|
|
530
|
+
group.classList.add('viz-sketch');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
if (group.classList.contains('viz-sketch')) {
|
|
535
|
+
group.classList.remove('viz-sketch');
|
|
536
|
+
}
|
|
537
|
+
// Only remove group filter if it was a sketch filter
|
|
538
|
+
const cur = group.getAttribute('filter');
|
|
539
|
+
if (cur && cur.startsWith('url(#viz-sketch-')) {
|
|
540
|
+
group.removeAttribute('filter');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
374
543
|
// Transform conflict rule: runtime wins if it writes transform.
|
|
375
544
|
const scale = node.runtime?.scale;
|
|
376
545
|
const rotation = node.runtime?.rotation;
|
|
@@ -502,6 +671,28 @@ export function patchRuntime(scene, ctx) {
|
|
|
502
671
|
line.style.removeProperty('stroke-dashoffset');
|
|
503
672
|
line.removeAttribute('stroke-dashoffset');
|
|
504
673
|
}
|
|
674
|
+
const edgeSketched = edge.style?.sketch || scene.sketch?.enabled;
|
|
675
|
+
if (edgeSketched) {
|
|
676
|
+
let h = 0;
|
|
677
|
+
for (let i = 0; i < edge.id.length; i++) {
|
|
678
|
+
h = (Math.imul(31, h) + edge.id.charCodeAt(i)) | 0;
|
|
679
|
+
}
|
|
680
|
+
const seed = Math.abs(h);
|
|
681
|
+
const fid = ensureSketchFilter(ctx.svg, seed);
|
|
682
|
+
line.setAttribute('filter', `url(#${fid})`);
|
|
683
|
+
if (!group.classList.contains('viz-sketch')) {
|
|
684
|
+
group.classList.add('viz-sketch');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const cur = line.getAttribute('filter');
|
|
689
|
+
if (cur && cur.startsWith('url(#viz-sketch-')) {
|
|
690
|
+
line.removeAttribute('filter');
|
|
691
|
+
}
|
|
692
|
+
if (group.classList.contains('viz-sketch')) {
|
|
693
|
+
group.classList.remove('viz-sketch');
|
|
694
|
+
}
|
|
695
|
+
}
|
|
505
696
|
}
|
|
506
697
|
// Ensure DOM order matches zIndex order for node layer children
|
|
507
698
|
// 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
package/dist/textUtils.d.ts
CHANGED
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.
|
|
3
|
+
"version": "1.3.1",
|
|
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",
|