@visualli/react 0.1.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/README.md +335 -0
- package/dist/index.cjs +3262 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +375 -0
- package/dist/index.d.ts +375 -0
- package/dist/index.js +3289 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/VisualliCanvas.tsx +1087 -0
- package/src/VisualliRenderer.tsx +427 -0
- package/src/animations/index.ts +2 -0
- package/src/animations/konvaLayerTransition.ts +223 -0
- package/src/components/KonvaContainer.tsx +255 -0
- package/src/components/KonvaContainerLayer.tsx +63 -0
- package/src/components/KonvaEdge.tsx +254 -0
- package/src/components/KonvaEdgeLayer.tsx +72 -0
- package/src/components/KonvaNode.tsx +177 -0
- package/src/components/KonvaNodeLayer.tsx +61 -0
- package/src/components/KonvaStage.tsx +80 -0
- package/src/components/NavigationStack.tsx +184 -0
- package/src/components/SketchyBoxKonva.tsx +121 -0
- package/src/components/ZoomControls.tsx +110 -0
- package/src/config/blobShapes.ts +204 -0
- package/src/config/index.ts +21 -0
- package/src/config/textScaling.ts +69 -0
- package/src/context/VisualliContext.tsx +50 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useKonvaLayerTransition.ts +221 -0
- package/src/hooks/useKonvaRenderer.ts +185 -0
- package/src/hooks/useViewportNodes.ts +59 -0
- package/src/index.ts +81 -0
- package/src/konvaCompat.ts +29 -0
- package/src/stores/index.ts +3 -0
- package/src/stores/stores.ts +17 -0
- package/src/stores/useNodeStore.ts +101 -0
- package/src/stores/useViewportStore.ts +95 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/layerNavigation.ts +15 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ─── ZoomControls ─────────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Floating zoom control matching the visualli.ai design:
|
|
4
|
+
// • Side-by-side ZoomIn / ZoomOut buttons (65×32px)
|
|
5
|
+
// • SVG icons (no external dependency)
|
|
6
|
+
// • Inline styles (no Tailwind required in SDK)
|
|
7
|
+
|
|
8
|
+
import React, { useCallback, memo } from 'react';
|
|
9
|
+
import { ZOOM_MIN, ZOOM_MAX } from '@visualli/core';
|
|
10
|
+
import { useViewportStore } from '../stores/useViewportStore';
|
|
11
|
+
|
|
12
|
+
export interface ZoomControlsProps {
|
|
13
|
+
isDark?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── ZoomIn SVG (Lucide-compatible path) ───────────────────────────────────────
|
|
17
|
+
const ZoomInIcon = () => (
|
|
18
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
19
|
+
<circle cx="11" cy="11" r="8" />
|
|
20
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
21
|
+
<line x1="11" y1="8" x2="11" y2="14" />
|
|
22
|
+
<line x1="8" y1="11" x2="14" y2="11" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// ── ZoomOut SVG ───────────────────────────────────────────────────────────────
|
|
27
|
+
const ZoomOutIcon = () => (
|
|
28
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
29
|
+
<circle cx="11" cy="11" r="8" />
|
|
30
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
31
|
+
<line x1="8" y1="11" x2="14" y2="11" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const ZOOM_STEP = 0.2;
|
|
36
|
+
|
|
37
|
+
const ZoomControls = memo(function ZoomControls({ isDark = false }: ZoomControlsProps) {
|
|
38
|
+
const zoomLevel = useViewportStore(s => s.zoomLevel);
|
|
39
|
+
const setZoom = useViewportStore(s => s.setZoom);
|
|
40
|
+
|
|
41
|
+
const handleZoomIn = useCallback(() => setZoom(Math.min(ZOOM_MAX, zoomLevel * 1.2)), [zoomLevel, setZoom]);
|
|
42
|
+
const handleZoomOut = useCallback(() => setZoom(Math.max(ZOOM_MIN, zoomLevel / 1.2)), [zoomLevel, setZoom]);
|
|
43
|
+
|
|
44
|
+
const surfaceBg = isDark ? 'var(--bg-surface, #1C1C1A)' : 'var(--bg-surface, #FAF8F4)';
|
|
45
|
+
const border = isDark ? 'var(--border-color, #333330)' : 'var(--border-color, #DDD9D0)';
|
|
46
|
+
const primary = isDark ? 'var(--text-primary, #F0EDE6)' : 'var(--text-primary, #1A1A18)';
|
|
47
|
+
const muted = isDark ? 'var(--text-muted, #8E8C88)' : 'var(--text-muted, #747270)';
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
position: 'relative',
|
|
53
|
+
background: surfaceBg,
|
|
54
|
+
border: `0.5px solid ${border}`,
|
|
55
|
+
borderRadius: '8px',
|
|
56
|
+
overflow: 'hidden',
|
|
57
|
+
width: '65px',
|
|
58
|
+
height: '32px',
|
|
59
|
+
flexShrink: 0,
|
|
60
|
+
}}
|
|
61
|
+
role="group"
|
|
62
|
+
aria-label="Zoom controls"
|
|
63
|
+
>
|
|
64
|
+
{/* Zoom In — left half */}
|
|
65
|
+
<button
|
|
66
|
+
onClick={handleZoomIn}
|
|
67
|
+
disabled={zoomLevel >= ZOOM_MAX}
|
|
68
|
+
style={{
|
|
69
|
+
position: 'absolute',
|
|
70
|
+
top: 0, left: 0,
|
|
71
|
+
width: '32px', height: '100%',
|
|
72
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
73
|
+
border: 'none', background: 'transparent', cursor: zoomLevel >= ZOOM_MAX ? 'not-allowed' : 'pointer',
|
|
74
|
+
color: zoomLevel >= ZOOM_MAX ? muted : primary,
|
|
75
|
+
transition: 'background 0.15s',
|
|
76
|
+
}}
|
|
77
|
+
onMouseEnter={e => { if (zoomLevel < ZOOM_MAX) (e.currentTarget as HTMLButtonElement).style.background = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'; }}
|
|
78
|
+
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent'; }}
|
|
79
|
+
aria-label="Zoom in"
|
|
80
|
+
>
|
|
81
|
+
<ZoomInIcon />
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
{/* Divider */}
|
|
85
|
+
<div style={{ position: 'absolute', top: 0, bottom: 0, left: '32px', width: '0.5px', background: border }} />
|
|
86
|
+
|
|
87
|
+
{/* Zoom Out — right half */}
|
|
88
|
+
<button
|
|
89
|
+
onClick={handleZoomOut}
|
|
90
|
+
disabled={zoomLevel <= ZOOM_MIN}
|
|
91
|
+
style={{
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
top: 0, right: 0,
|
|
94
|
+
width: '33px', height: '100%',
|
|
95
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
96
|
+
border: 'none', background: 'transparent', cursor: zoomLevel <= ZOOM_MIN ? 'not-allowed' : 'pointer',
|
|
97
|
+
color: zoomLevel <= ZOOM_MIN ? muted : primary,
|
|
98
|
+
transition: 'background 0.15s',
|
|
99
|
+
}}
|
|
100
|
+
onMouseEnter={e => { if (zoomLevel > ZOOM_MIN) (e.currentTarget as HTMLButtonElement).style.background = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)'; }}
|
|
101
|
+
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent'; }}
|
|
102
|
+
aria-label="Zoom out"
|
|
103
|
+
>
|
|
104
|
+
<ZoomOutIcon />
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export default ZoomControls;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// ─── Blob Shapes Configuration ───────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Blob shapes are centered at (0, 0). Coordinates are relative — multiply by
|
|
4
|
+
// (radiusX, radiusY) to get world coords. Drawing uses closed quadratic bezier
|
|
5
|
+
// curves through midpoints of consecutive control points (same as visualli.ai).
|
|
6
|
+
//
|
|
7
|
+
// Shapes match the visualli.ai reference designs exactly.
|
|
8
|
+
|
|
9
|
+
export interface BlobPoint { x: number; y: number }
|
|
10
|
+
export type BlobShape = ReadonlyArray<BlobPoint>;
|
|
11
|
+
|
|
12
|
+
// ── Blob background layer depth config ────────────────────────────────────────
|
|
13
|
+
// Index 0 = outermost layer (rendered first, behind the top blob).
|
|
14
|
+
|
|
15
|
+
export const BLOB_LAYER_CONFIG = [
|
|
16
|
+
{ opacity: 0.3, rotation: -15, scaleUp: 1.30, strokeWidth: 3, dash: [6, 4] as [number, number] },
|
|
17
|
+
{ opacity: 0.4, rotation: -10, scaleUp: 1.20, strokeWidth: 1, dash: undefined },
|
|
18
|
+
{ opacity: 0.5, rotation: -5, scaleUp: 1.10, strokeWidth: 2, dash: undefined },
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export const NODE_LAYER_CONFIG = { opacity: 1, rotation: 0, strokeWidth: 3 } as const;
|
|
22
|
+
|
|
23
|
+
// ── Six organic blob variants ─────────────────────────────────────────────────
|
|
24
|
+
export const ALL_BLOB_SHAPES: ReadonlyArray<BlobShape> = [
|
|
25
|
+
// Shape 0 · Blob 1 (root / L0)
|
|
26
|
+
[
|
|
27
|
+
{ x: 1.12, y: 0.00 }, { x: 0.86, y: 0.56 }, { x: 0.46, y: 0.94 },
|
|
28
|
+
{ x: -0.08, y: 1.06 }, { x: -0.68, y: 0.82 }, { x: -0.96, y: 0.08 },
|
|
29
|
+
{ x: -0.55, y: -0.82 }, { x: 0.04, y: -1.02 }, { x: 0.72, y: -0.80 },
|
|
30
|
+
{ x: 1.08, y: -0.38 },
|
|
31
|
+
],
|
|
32
|
+
// Shape 1 · Blob 2
|
|
33
|
+
[
|
|
34
|
+
{ x: -0.489, y: -0.625 }, { x: -0.384, y: -0.716 }, { x: -0.285, y: -0.792 },
|
|
35
|
+
{ x: -0.189, y: -0.855 }, { x: -0.090, y: -0.903 }, { x: 0.014, y: -0.932 },
|
|
36
|
+
{ x: 0.129, y: -0.940 }, { x: 0.255, y: -0.924 }, { x: 0.412, y: -0.876 },
|
|
37
|
+
{ x: 0.516, y: -0.832 }, { x: 0.612, y: -0.780 }, { x: 0.695, y: -0.725 },
|
|
38
|
+
{ x: 0.767, y: -0.666 }, { x: 0.830, y: -0.606 }, { x: 0.885, y: -0.546 },
|
|
39
|
+
{ x: 0.933, y: -0.486 }, { x: 0.979, y: -0.425 }, { x: 1.006, y: -0.385 },
|
|
40
|
+
{ x: 1.034, y: -0.334 }, { x: 1.058, y: -0.276 }, { x: 1.078, y: -0.209 },
|
|
41
|
+
{ x: 1.092, y: -0.133 }, { x: 1.101, y: -0.046 }, { x: 1.102, y: 0.052 },
|
|
42
|
+
{ x: 1.094, y: 0.173 }, { x: 1.083, y: 0.244 }, { x: 1.063, y: 0.324 },
|
|
43
|
+
{ x: 1.034, y: 0.401 }, { x: 0.996, y: 0.477 }, { x: 0.948, y: 0.549 },
|
|
44
|
+
{ x: 0.889, y: 0.617 }, { x: 0.820, y: 0.681 }, { x: 0.735, y: 0.741 },
|
|
45
|
+
{ x: 0.658, y: 0.783 }, { x: 0.569, y: 0.822 }, { x: 0.474, y: 0.855 },
|
|
46
|
+
{ x: 0.372, y: 0.882 }, { x: 0.265, y: 0.904 }, { x: 0.156, y: 0.920 },
|
|
47
|
+
{ x: 0.046, y: 0.931 }, { x: -0.069, y: 0.938 }, { x: -0.155, y: 0.935 },
|
|
48
|
+
{ x: -0.250, y: 0.919 }, { x: -0.346, y: 0.890 }, { x: -0.443, y: 0.851 },
|
|
49
|
+
{ x: -0.539, y: 0.800 }, { x: -0.633, y: 0.738 }, { x: -0.723, y: 0.667 },
|
|
50
|
+
{ x: -0.808, y: 0.585 }, { x: -0.882, y: 0.501 }, { x: -0.944, y: 0.421 },
|
|
51
|
+
{ x: -0.994, y: 0.343 }, { x: -1.034, y: 0.267 }, { x: -1.064, y: 0.192 },
|
|
52
|
+
{ x: -1.085, y: 0.120 }, { x: -1.097, y: 0.042 }, { x: -1.102, y: 0.001 },
|
|
53
|
+
{ x: -1.090, y: -0.243 }, { x: -1.050, y: -0.365 }, { x: -0.990, y: -0.441 },
|
|
54
|
+
{ x: -0.911, y: -0.479 }, { x: -0.819, y: -0.499 }, { x: -0.715, y: -0.517 },
|
|
55
|
+
{ x: -0.605, y: -0.551 },
|
|
56
|
+
],
|
|
57
|
+
// Shape 2 · Blob 3
|
|
58
|
+
[
|
|
59
|
+
{ x: 0.003, y: -0.970 }, { x: 0.163, y: -0.984 }, { x: 0.286, y: -1.028 },
|
|
60
|
+
{ x: 0.389, y: -1.096 }, { x: 0.488, y: -1.174 }, { x: 0.597, y: -1.210 },
|
|
61
|
+
{ x: 0.728, y: -1.165 }, { x: 0.867, y: -1.036 }, { x: 0.996, y: -0.839 },
|
|
62
|
+
{ x: 1.098, y: -0.590 }, { x: 1.158, y: -0.304 }, { x: 1.153, y: 0.012 },
|
|
63
|
+
{ x: 1.080, y: 0.336 }, { x: 0.944, y: 0.639 }, { x: 0.756, y: 0.889 },
|
|
64
|
+
{ x: 0.484, y: 1.072 }, { x: 0.339, y: 1.079 }, { x: 0.219, y: 1.051 },
|
|
65
|
+
{ x: 0.131, y: 0.994 }, { x: 0.059, y: 0.939 }, { x: 0.008, y: 0.915 },
|
|
66
|
+
{ x: -0.105, y: 0.931 }, { x: -0.176, y: 0.965 }, { x: -0.249, y: 1.005 },
|
|
67
|
+
{ x: -0.345, y: 1.039 }, { x: -0.497, y: 1.055 }, { x: -0.672, y: 1.032 },
|
|
68
|
+
{ x: -0.817, y: 0.952 }, { x: -0.945, y: 0.810 }, { x: -1.054, y: 0.600 },
|
|
69
|
+
{ x: -1.160, y: 0.302 }, { x: -1.187, y: -0.011 }, { x: -1.165, y: -0.326 },
|
|
70
|
+
{ x: -1.097, y: -0.599 }, { x: -0.991, y: -0.838 }, { x: -0.852, y: -0.912 },
|
|
71
|
+
{ x: -0.695, y: -0.958 }, { x: -0.530, y: -0.988 }, { x: -0.357, y: -0.992 },
|
|
72
|
+
{ x: -0.180, y: -0.983 },
|
|
73
|
+
],
|
|
74
|
+
// Shape 3 · Blob 4
|
|
75
|
+
[
|
|
76
|
+
{ x: -0.734, y: -1.127 }, { x: -0.656, y: -1.146 }, { x: -0.567, y: -1.142 },
|
|
77
|
+
{ x: -0.468, y: -1.115 }, { x: -0.358, y: -1.079 }, { x: -0.238, y: -1.034 },
|
|
78
|
+
{ x: -0.107, y: -0.989 }, { x: 0.035, y: -0.953 }, { x: 0.188, y: -0.929 },
|
|
79
|
+
{ x: 0.325, y: -0.916 }, { x: 0.484, y: -0.896 }, { x: 0.650, y: -0.854 },
|
|
80
|
+
{ x: 0.811, y: -0.770 }, { x: 0.955, y: -0.631 }, { x: 1.068, y: -0.422 },
|
|
81
|
+
{ x: 1.138, y: -0.126 }, { x: 1.152, y: 0.272 }, { x: 1.121, y: 0.451 },
|
|
82
|
+
{ x: 1.046, y: 0.602 }, { x: 0.928, y: 0.727 }, { x: 0.767, y: 0.833 },
|
|
83
|
+
{ x: 0.565, y: 0.925 }, { x: 0.323, y: 1.001 }, { x: 0.042, y: 1.075 },
|
|
84
|
+
{ x: -0.277, y: 1.142 }, { x: -0.368, y: 1.147 }, { x: -0.475, y: 1.136 },
|
|
85
|
+
{ x: -0.589, y: 1.104 }, { x: -0.704, y: 1.050 }, { x: -0.815, y: 0.973 },
|
|
86
|
+
{ x: -0.913, y: 0.871 }, { x: -0.993, y: 0.746 }, { x: -1.048, y: 0.593 },
|
|
87
|
+
{ x: -1.109, y: 0.298 }, { x: -1.145, y: 0.018 }, { x: -1.154, y: -0.243 },
|
|
88
|
+
{ x: -1.135, y: -0.482 }, { x: -1.085, y: -0.695 }, { x: -1.003, y: -0.874 },
|
|
89
|
+
{ x: -0.886, y: -1.020 },
|
|
90
|
+
],
|
|
91
|
+
// Shape 4 · Blob 5 (inverted Blob 4)
|
|
92
|
+
[
|
|
93
|
+
{ x: -0.734, y: 1.127 }, { x: -0.656, y: 1.146 }, { x: -0.567, y: 1.142 },
|
|
94
|
+
{ x: -0.468, y: 1.115 }, { x: -0.358, y: 1.079 }, { x: -0.238, y: 1.034 },
|
|
95
|
+
{ x: -0.107, y: 0.989 }, { x: 0.035, y: 0.953 }, { x: 0.188, y: 0.929 },
|
|
96
|
+
{ x: 0.325, y: 0.916 }, { x: 0.484, y: 0.896 }, { x: 0.650, y: 0.854 },
|
|
97
|
+
{ x: 0.811, y: 0.770 }, { x: 0.955, y: 0.631 }, { x: 1.068, y: 0.422 },
|
|
98
|
+
{ x: 1.138, y: 0.126 }, { x: 1.152, y: -0.272 }, { x: 1.121, y: -0.451 },
|
|
99
|
+
{ x: 1.046, y: -0.602 }, { x: 0.928, y: -0.727 }, { x: 0.767, y: -0.833 },
|
|
100
|
+
{ x: 0.565, y: -0.925 }, { x: 0.323, y: -1.001 }, { x: 0.042, y: -1.075 },
|
|
101
|
+
{ x: -0.277, y: -1.142 }, { x: -0.368, y: -1.147 }, { x: -0.475, y: -1.136 },
|
|
102
|
+
{ x: -0.589, y: -1.104 }, { x: -0.704, y: -1.050 }, { x: -0.815, y: -0.973 },
|
|
103
|
+
{ x: -0.913, y: -0.871 }, { x: -0.993, y: -0.746 }, { x: -1.048, y: -0.593 },
|
|
104
|
+
{ x: -1.109, y: -0.298 }, { x: -1.145, y: -0.018 }, { x: -1.154, y: 0.243 },
|
|
105
|
+
{ x: -1.135, y: 0.482 }, { x: -1.085, y: 0.695 }, { x: -1.003, y: 0.874 },
|
|
106
|
+
{ x: -0.886, y: 1.020 },
|
|
107
|
+
],
|
|
108
|
+
// Shape 5 · Blob 6
|
|
109
|
+
[
|
|
110
|
+
{ x: 0.001, y: -1.006 }, { x: 0.134, y: -1.001 }, { x: 0.262, y: -1.024 },
|
|
111
|
+
{ x: 0.382, y: -1.073 }, { x: 0.498, y: -1.107 }, { x: 0.614, y: -1.105 },
|
|
112
|
+
{ x: 0.733, y: -1.049 }, { x: 0.850, y: -0.932 }, { x: 0.953, y: -0.755 },
|
|
113
|
+
{ x: 1.030, y: -0.529 }, { x: 1.067, y: -0.265 }, { x: 1.054, y: 0.018 },
|
|
114
|
+
{ x: 0.986, y: 0.300 }, { x: 0.868, y: 0.557 }, { x: 0.715, y: 0.766 },
|
|
115
|
+
{ x: 0.550, y: 0.909 }, { x: 0.396, y: 0.984 }, { x: 0.262, y: 1.005 },
|
|
116
|
+
{ x: 0.148, y: 1.003 }, { x: 0.037, y: 1.008 }, { x: -0.083, y: 1.040 },
|
|
117
|
+
{ x: -0.220, y: 1.118 }, { x: -0.360, y: 1.192 }, { x: -0.500, y: 1.234 },
|
|
118
|
+
{ x: -0.650, y: 1.175 }, { x: -0.810, y: 1.045 }, { x: -0.990, y: 0.855 },
|
|
119
|
+
{ x: -1.140, y: 0.610 }, { x: -1.240, y: 0.330 }, { x: -1.270, y: 0.040 },
|
|
120
|
+
{ x: -1.275, y: -0.250 }, { x: -1.225, y: -0.500 }, { x: -1.080, y: -0.720 },
|
|
121
|
+
{ x: -0.890, y: -0.920 }, { x: -0.734, y: -1.053 }, { x: -0.614, y: -1.112 },
|
|
122
|
+
{ x: -0.498, y: -1.127 }, { x: -0.381, y: -1.110 }, { x: -0.261, y: -1.095 },
|
|
123
|
+
{ x: -0.133, y: -1.058 }, { x: 0.001, y: -1.010 }, { x: 0.134, y: -1.001 },
|
|
124
|
+
],
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
export const ACTIVE_BLOB_TYPES: ReadonlyArray<number> = [0, 1, 2, 3, 4, 5];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Per-shape text x-offset as a fraction of baseRadiusX.
|
|
131
|
+
* Compensates for visually asymmetric blobs. Positive = right, negative = left.
|
|
132
|
+
*/
|
|
133
|
+
export const BLOB_TEXT_OFFSETS: ReadonlyArray<number> = [0, 0, 0, 0, 0, -0.10];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Deterministic pseudo-random blob-type selector per tree level.
|
|
137
|
+
* Root level (0) always uses shape 0; consecutive levels use different shapes.
|
|
138
|
+
*/
|
|
139
|
+
export function getBlobTypeForLayer(level: number): number {
|
|
140
|
+
const numTypes = ACTIVE_BLOB_TYPES.length;
|
|
141
|
+
if (numTypes <= 1) return ACTIVE_BLOB_TYPES[0] ?? 0;
|
|
142
|
+
let currentType = 0;
|
|
143
|
+
for (let i = 1; i <= level; i++) {
|
|
144
|
+
const pseudoRandom = Math.floor(Math.abs(Math.sin((i + 1) * 12.9898) * 43758.5453));
|
|
145
|
+
const stepOffset = (pseudoRandom % (numTypes - 1)) + 1;
|
|
146
|
+
currentType = (currentType + stepOffset) % numTypes;
|
|
147
|
+
}
|
|
148
|
+
return ACTIVE_BLOB_TYPES[currentType];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Draw the blob path onto a Konva canvas context (centered at 0,0).
|
|
153
|
+
* Uses closed quadratic bezier curves through midpoints of control points.
|
|
154
|
+
* Call this inside a Konva Shape's sceneFunc / hitFunc.
|
|
155
|
+
*/
|
|
156
|
+
export function drawBlobPath(
|
|
157
|
+
ctx: { beginPath(): void; moveTo(x: number, y: number): void; quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; closePath(): void },
|
|
158
|
+
radiusX: number,
|
|
159
|
+
radiusY: number,
|
|
160
|
+
blobType: number,
|
|
161
|
+
): void {
|
|
162
|
+
const srcPts = ALL_BLOB_SHAPES[blobType] ?? ALL_BLOB_SHAPES[0];
|
|
163
|
+
const pts = srcPts.map(p => ({ x: p.x * radiusX, y: p.y * radiusY }));
|
|
164
|
+
ctx.beginPath();
|
|
165
|
+
const last = pts[pts.length - 1];
|
|
166
|
+
const first = pts[0];
|
|
167
|
+
ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
|
|
168
|
+
for (let i = 0; i < pts.length; i++) {
|
|
169
|
+
const curr = pts[i];
|
|
170
|
+
const next = pts[(i + 1) % pts.length];
|
|
171
|
+
ctx.quadraticCurveTo(curr.x, curr.y, (curr.x + next.x) / 2, (curr.y + next.y) / 2);
|
|
172
|
+
}
|
|
173
|
+
ctx.closePath();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Legacy export kept for consumers that may import buildBlobPathData.
|
|
177
|
+
// Generates an SVG path string (less accurate than the sceneFunc approach).
|
|
178
|
+
export function buildBlobPathData(
|
|
179
|
+
_shape: BlobShape,
|
|
180
|
+
cx: number,
|
|
181
|
+
cy: number,
|
|
182
|
+
width: number,
|
|
183
|
+
height: number,
|
|
184
|
+
): string {
|
|
185
|
+
const rx = width / 2;
|
|
186
|
+
const ry = height / 2;
|
|
187
|
+
const blobType = 0;
|
|
188
|
+
const srcPts = ALL_BLOB_SHAPES[blobType];
|
|
189
|
+
const pts = srcPts.map(p => ({ x: cx + p.x * rx, y: cy + p.y * ry }));
|
|
190
|
+
const mid = (a: { x: number; y: number }, b: { x: number; y: number }) =>
|
|
191
|
+
({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
|
|
192
|
+
const last = pts[pts.length - 1];
|
|
193
|
+
const first = pts[0];
|
|
194
|
+
const s = mid(last, first);
|
|
195
|
+
let d = `M ${s.x} ${s.y}`;
|
|
196
|
+
for (let i = 0; i < pts.length; i++) {
|
|
197
|
+
const curr = pts[i];
|
|
198
|
+
const next = pts[(i + 1) % pts.length];
|
|
199
|
+
const m = mid(curr, next);
|
|
200
|
+
d += ` Q ${curr.x} ${curr.y} ${m.x} ${m.y}`;
|
|
201
|
+
}
|
|
202
|
+
d += ' Z';
|
|
203
|
+
return d;
|
|
204
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ALL_BLOB_SHAPES,
|
|
3
|
+
ACTIVE_BLOB_TYPES,
|
|
4
|
+
BLOB_LAYER_CONFIG,
|
|
5
|
+
NODE_LAYER_CONFIG,
|
|
6
|
+
getBlobTypeForLayer,
|
|
7
|
+
BLOB_TEXT_OFFSETS,
|
|
8
|
+
drawBlobPath,
|
|
9
|
+
NODE_TEXT_BASE_FONT_PX,
|
|
10
|
+
DESCRIPTION_TEXT_BASE_FONT_PX,
|
|
11
|
+
CONTAINER_LABEL_BASE_FONT_PX,
|
|
12
|
+
EDGE_LABEL_BASE_FONT_PX,
|
|
13
|
+
OVERLAY_FONT_UPPER_BOOST_PX,
|
|
14
|
+
computeNodeTextWorldScale,
|
|
15
|
+
computeNodeTextScreenScale,
|
|
16
|
+
computeOverlayScale,
|
|
17
|
+
computeEdgeLabelScale,
|
|
18
|
+
} from '@visualli/core';
|
|
19
|
+
|
|
20
|
+
export { buildBlobPathData } from './blobShapes';
|
|
21
|
+
export type { BlobPoint, BlobShape } from '@visualli/core';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ─── Text Scaling Helpers ─────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Determines the scale applied to node title text, keeping text legible and
|
|
4
|
+
// fitted inside the blob shape across the full zoom range.
|
|
5
|
+
//
|
|
6
|
+
// Matches the visualli.ai textScaling logic exactly.
|
|
7
|
+
|
|
8
|
+
import { NODE_HEIGHT } from '@visualli/core';
|
|
9
|
+
|
|
10
|
+
export const NODE_TEXT_BASE_FONT_PX = 30;
|
|
11
|
+
export const DESCRIPTION_TEXT_BASE_FONT_PX = 12;
|
|
12
|
+
export const CONTAINER_LABEL_BASE_FONT_PX = 24;
|
|
13
|
+
export const EDGE_LABEL_BASE_FONT_PX = 18;
|
|
14
|
+
export const OVERLAY_FONT_UPPER_BOOST_PX = 8;
|
|
15
|
+
|
|
16
|
+
// Keep text away from blob edges
|
|
17
|
+
const NODE_TEXT_SAFE_BLOB_FILL_RATIO = 0.88;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Computes world-space text scale for node title:
|
|
21
|
+
* - inverse zoom for screen-space consistency
|
|
22
|
+
* - clamped so text stays inside the node blob with safe padding
|
|
23
|
+
* - uses base width of 300px for consistent text sizing
|
|
24
|
+
* - allows taller text box for multi-line wrapping
|
|
25
|
+
*
|
|
26
|
+
* Improved from visualli.ai to handle long labels better with multi-line support.
|
|
27
|
+
*/
|
|
28
|
+
export function computeNodeTextWorldScale(nodeWidth: number, zoomLevel: number): number {
|
|
29
|
+
const safeZoom = Math.max(zoomLevel, 0.0001);
|
|
30
|
+
|
|
31
|
+
// Use a consistent base width (300px) for text scaling
|
|
32
|
+
// This keeps font size more consistent across different node widths
|
|
33
|
+
const baseWidth = 300;
|
|
34
|
+
|
|
35
|
+
const baseRadiusX = baseWidth / 2;
|
|
36
|
+
const baseRadiusY = Math.max((NODE_HEIGHT * 1.6) / 2, baseRadiusX * 0.74);
|
|
37
|
+
const textNaturalWidth = baseWidth - 40;
|
|
38
|
+
// Increase text height to allow 2-3 lines of wrapped text
|
|
39
|
+
const textNaturalHeight = NODE_HEIGHT * 1.2;
|
|
40
|
+
|
|
41
|
+
const maxScaleX = ((baseRadiusX * 2) * NODE_TEXT_SAFE_BLOB_FILL_RATIO) / Math.max(textNaturalWidth, 1);
|
|
42
|
+
const maxScaleY = ((baseRadiusY * 2) * NODE_TEXT_SAFE_BLOB_FILL_RATIO) / Math.max(textNaturalHeight, 1);
|
|
43
|
+
const fitScale = Math.max(0.01, Math.min(maxScaleX, maxScaleY));
|
|
44
|
+
|
|
45
|
+
return Math.min(1 / safeZoom, fitScale);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Screen-space scale (zoom-adjusted) for text overlays. */
|
|
49
|
+
export function computeNodeTextScreenScale(nodeWidth: number, zoomLevel: number): number {
|
|
50
|
+
return computeNodeTextWorldScale(nodeWidth, zoomLevel) * zoomLevel;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Screen-space overlay scale for description/semantic tooltips.
|
|
55
|
+
* - lower bound: 1x
|
|
56
|
+
* - upper bound: node-title-root + OVERLAY_FONT_UPPER_BOOST_PX
|
|
57
|
+
*/
|
|
58
|
+
export function computeOverlayScale(zoomLevel: number): number {
|
|
59
|
+
const safeZoom = Math.max(zoomLevel, 0.0001);
|
|
60
|
+
const inverseScale = 1 / safeZoom;
|
|
61
|
+
const maxScale = (NODE_TEXT_BASE_FONT_PX + OVERLAY_FONT_UPPER_BOOST_PX) / NODE_TEXT_BASE_FONT_PX;
|
|
62
|
+
return Math.min(Math.max(inverseScale, 1), maxScale);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Scale for edge path labels in screen pixels. */
|
|
66
|
+
export function computeEdgeLabelScale(zoomLevel: number): number {
|
|
67
|
+
const safeZoom = Math.max(zoomLevel, 0.0001);
|
|
68
|
+
return Math.min(Math.max(EDGE_LABEL_BASE_FONT_PX / safeZoom, 10), 32);
|
|
69
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// ─── Visualli Context ─────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// React Context for Visualli components (minimal implementation)
|
|
4
|
+
|
|
5
|
+
import React, { createContext, useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
export interface VisualliContextValue {
|
|
8
|
+
// Reserved for future use
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const VisualliContext = createContext<VisualliContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface VisualliProviderProps {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Provider for Visualli context
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <VisualliProvider>
|
|
23
|
+
* <VisualliCanvas document={doc} />
|
|
24
|
+
* </VisualliProvider>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function VisualliProvider({
|
|
28
|
+
children,
|
|
29
|
+
}: VisualliProviderProps) {
|
|
30
|
+
const value: VisualliContextValue = {};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<VisualliContext.Provider value={value}>
|
|
34
|
+
{children}
|
|
35
|
+
</VisualliContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook to access Visualli context
|
|
41
|
+
*
|
|
42
|
+
* Returns default empty values if used outside of VisualliProvider
|
|
43
|
+
*/
|
|
44
|
+
export function useVisualli(): VisualliContextValue {
|
|
45
|
+
const ctx = useContext(VisualliContext);
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
return ctx;
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { useKonvaRenderer } from './useKonvaRenderer';
|
|
2
|
+
export type { UseKonvaRendererOptions } from './useKonvaRenderer';
|
|
3
|
+
|
|
4
|
+
export { useKonvaLayerTransition } from './useKonvaLayerTransition';
|
|
5
|
+
export type { LayerTransitionHandlers } from './useKonvaLayerTransition';
|
|
6
|
+
|
|
7
|
+
export { useViewportNodes } from './useViewportNodes';
|