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 +6 -0
- package/README.md +13 -0
- package/dist/builder.d.ts +25 -0
- package/dist/builder.js +271 -4
- package/dist/runtimePatcher.js +201 -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,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
|
-
|
|
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
|
-
|
|
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
|
|
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}`;
|
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,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
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.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",
|