@visualli/react 0.1.7 → 0.2.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 +13 -5
- package/dist/index.cjs +1900 -2033
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +354 -97
- package/dist/index.d.ts +354 -97
- package/dist/index.js +2005 -2095
- package/dist/index.js.map +1 -1
- package/fonts/Caveat-Variable.ttf +0 -0
- package/licenses/visualli-design-system/LICENSE +9 -0
- package/licenses/visualli-design-system/NOTICE.md +6 -0
- package/package.json +9 -4
- package/src/VisualliCanvas.tsx +452 -409
- package/src/VisualliRenderer.tsx +90 -112
- package/src/components/A11yLayer.tsx +73 -0
- package/src/components/KonvaContainer.tsx +28 -237
- package/src/components/KonvaContainerLabelLayer.tsx +41 -0
- package/src/components/KonvaContainerLayer.tsx +34 -33
- package/src/components/KonvaEdge.tsx +25 -240
- package/src/components/KonvaEdgeLayer.tsx +69 -49
- package/src/components/KonvaNode.tsx +21 -156
- package/src/components/KonvaNodeLayer.tsx +102 -34
- package/src/components/KonvaStage.tsx +17 -3
- package/src/components/NavigationStack.tsx +16 -159
- package/src/components/Overlays.tsx +327 -0
- package/src/components/ZoomControls.tsx +3 -107
- package/src/components/useFrames.ts +30 -0
- package/src/context/VisualliContext.tsx +3 -3
- package/src/design/choreography.ts +157 -0
- package/src/design/containers.ts +150 -0
- package/src/design/design.ts +115 -0
- package/src/design/drawing.ts +303 -0
- package/src/design/measure.ts +34 -0
- package/src/design/runtime.ts +154 -0
- package/src/design/useDesign.ts +38 -0
- package/src/generated/specCss.ts +15 -0
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useKonvaRenderer.ts +13 -18
- package/src/hooks/useLayerChoreography.ts +159 -0
- package/src/hooks/useViewportNodes.ts +0 -1
- package/src/index.ts +22 -22
- package/src/stores/stores.ts +2 -2
- package/src/stores/useViewportStore.ts +12 -2
- package/src/animations/index.ts +0 -2
- package/src/animations/konvaLayerTransition.ts +0 -223
- package/src/components/SketchyBoxKonva.tsx +0 -121
- package/src/config/blobShapes.ts +0 -204
- package/src/config/index.ts +0 -21
- package/src/config/textScaling.ts +0 -69
- package/src/hooks/useKonvaLayerTransition.ts +0 -221
|
@@ -18,11 +18,16 @@ import {
|
|
|
18
18
|
export interface IViewportStore extends ViewportState {
|
|
19
19
|
canvasWidth: number;
|
|
20
20
|
canvasHeight: number;
|
|
21
|
+
/** The layer's fit to view (scale and centre): zoom is shown and limited (VIEW) relative to it, as in the design system. */
|
|
22
|
+
fitScale: number;
|
|
23
|
+
fitCenterX: number;
|
|
24
|
+
fitCenterY: number;
|
|
21
25
|
|
|
22
26
|
pan: (deltaX: number, deltaY: number) => void;
|
|
23
27
|
zoom: (delta: number, pivotX?: number, pivotY?: number) => void;
|
|
24
28
|
setCenter: (x: number, y: number) => void;
|
|
25
29
|
setZoom: (level: number) => void;
|
|
30
|
+
setFit: (scale: number, centerX: number, centerY: number) => void;
|
|
26
31
|
updateCanvasSize: (width: number, height: number) => void;
|
|
27
32
|
|
|
28
33
|
getVisibleBounds: () => ViewportState['visibleBounds'];
|
|
@@ -52,6 +57,9 @@ export const useViewportStore = create<IViewportStore>((set, get) => {
|
|
|
52
57
|
...init,
|
|
53
58
|
canvasWidth: DEFAULT_CANVAS_WIDTH,
|
|
54
59
|
canvasHeight: DEFAULT_CANVAS_HEIGHT,
|
|
60
|
+
fitScale: 1,
|
|
61
|
+
fitCenterX: 0,
|
|
62
|
+
fitCenterY: 0,
|
|
55
63
|
|
|
56
64
|
pan: (dx, dy) => set((s) => {
|
|
57
65
|
const updated = recalculateViewportBounds(panViewport(dx, dy, getVP(s)), s.canvasWidth, s.canvasHeight);
|
|
@@ -60,7 +68,7 @@ export const useViewportStore = create<IViewportStore>((set, get) => {
|
|
|
60
68
|
|
|
61
69
|
zoom: (delta, pivotX, pivotY) => set((s) => {
|
|
62
70
|
const updated = recalculateViewportBounds(
|
|
63
|
-
zoomViewport(delta, getVP(s), pivotX, pivotY, s.canvasWidth, s.canvasHeight),
|
|
71
|
+
zoomViewport(delta, getVP(s), pivotX, pivotY, s.canvasWidth, s.canvasHeight, s.fitScale),
|
|
64
72
|
s.canvasWidth, s.canvasHeight,
|
|
65
73
|
);
|
|
66
74
|
return { centerX: updated.centerX, centerY: updated.centerY, zoomLevel: updated.zoomLevel, visibleBounds: updated.visibleBounds };
|
|
@@ -72,11 +80,13 @@ export const useViewportStore = create<IViewportStore>((set, get) => {
|
|
|
72
80
|
}),
|
|
73
81
|
|
|
74
82
|
setZoom: (level) => set((s) => {
|
|
75
|
-
const clamped = clampZoom(level);
|
|
83
|
+
const clamped = clampZoom(level, s.fitScale);
|
|
76
84
|
const updated = recalculateViewportBounds({ ...getVP(s), zoomLevel: clamped }, s.canvasWidth, s.canvasHeight);
|
|
77
85
|
return { zoomLevel: updated.zoomLevel, visibleBounds: updated.visibleBounds };
|
|
78
86
|
}),
|
|
79
87
|
|
|
88
|
+
setFit: (scale, centerX, centerY) => set({ fitScale: scale > 0 ? scale : 1, fitCenterX: centerX, fitCenterY: centerY }),
|
|
89
|
+
|
|
80
90
|
updateCanvasSize: (w, h) => set((s) => {
|
|
81
91
|
const updated = recalculateViewportBounds(getVP(s), w, h);
|
|
82
92
|
return { canvasWidth: w, canvasHeight: h, visibleBounds: updated.visibleBounds };
|
package/src/animations/index.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
// ─── Konva Layer Transition Animator ─────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// rAF-driven animation engine for layer navigation:
|
|
4
|
-
// animateZoomIntoLayer — camera-dive into child layer
|
|
5
|
-
// animateZoomOutToParent — pull back to parent layer
|
|
6
|
-
// animateCrossFade — simple cross-fade between two viewports
|
|
7
|
-
//
|
|
8
|
-
// No React dependencies — can be instantiated once and reused.
|
|
9
|
-
|
|
10
|
-
import { easeInOutSine, easeOutCubic, easeZoomTransition } from '@visualli/core';
|
|
11
|
-
import { ANIMATION_PHASES } from '@visualli/core';
|
|
12
|
-
|
|
13
|
-
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export interface AnimatorViewport {
|
|
16
|
-
centerX: number;
|
|
17
|
-
centerY: number;
|
|
18
|
-
zoomLevel: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface NodeBounds {
|
|
22
|
-
x: number;
|
|
23
|
-
y: number;
|
|
24
|
-
width: number;
|
|
25
|
-
height: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface TransitionCallbacks {
|
|
29
|
-
onUpdate: (state: { viewport: AnimatorViewport; opacity: number; backgroundColor: string }) => void;
|
|
30
|
-
onComplete: () => void;
|
|
31
|
-
onPhaseChange?: (phase: string) => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ── Color helpers ─────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
interface RGB { r: number; g: number; b: number }
|
|
37
|
-
|
|
38
|
-
function parseColor(color: string): RGB {
|
|
39
|
-
if (color.startsWith('#')) {
|
|
40
|
-
const h = color.slice(1);
|
|
41
|
-
return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) };
|
|
42
|
-
}
|
|
43
|
-
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
44
|
-
return m ? { r: +m[1], g: +m[2], b: +m[3] } : { r: 255, g: 255, b: 255 };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function interpolateRgb(a: RGB, b: RGB, t: number): string {
|
|
48
|
-
const e = easeInOutSine(t);
|
|
49
|
-
return `rgb(${(a.r + (b.r - a.r) * e + 0.5) | 0},${(a.g + (b.g - a.g) * e + 0.5) | 0},${(a.b + (b.b - a.b) * e + 0.5) | 0})`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// (easeZoomTransition is imported from @visualli/core — createCubicBezier(0.2, 0.8, 0.2, 1))
|
|
53
|
-
|
|
54
|
-
function lerp(a: number, b: number, t: number): number { return a + (b - a) * t; }
|
|
55
|
-
|
|
56
|
-
// ── Animator class ────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
export class KonvaLayerTransitionAnimator {
|
|
59
|
-
private rafId: number | null = null;
|
|
60
|
-
private timers: ReturnType<typeof setTimeout>[] = [];
|
|
61
|
-
private _active = false;
|
|
62
|
-
|
|
63
|
-
get active(): boolean { return this._active; }
|
|
64
|
-
|
|
65
|
-
cancel(): void {
|
|
66
|
-
if (this.rafId !== null) { cancelAnimationFrame(this.rafId); this.rafId = null; }
|
|
67
|
-
this.timers.forEach(clearTimeout);
|
|
68
|
-
this.timers = [];
|
|
69
|
-
this._active = false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Zoom into child layer ──────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
animateZoomIntoLayer(
|
|
75
|
-
current: AnimatorViewport,
|
|
76
|
-
targetNode: NodeBounds,
|
|
77
|
-
childCenter: { x: number; y: number },
|
|
78
|
-
fromColor: string,
|
|
79
|
-
toColor: string,
|
|
80
|
-
cb: TransitionCallbacks,
|
|
81
|
-
targetZoom = 1.0,
|
|
82
|
-
canvasWidth = 800,
|
|
83
|
-
canvasHeight = 600,
|
|
84
|
-
): void {
|
|
85
|
-
if (this._active) return;
|
|
86
|
-
this._active = true;
|
|
87
|
-
|
|
88
|
-
// Timing constants pulled from ANIMATION_PHASES (single source of truth)
|
|
89
|
-
const ZOOM_DURATION = ANIMATION_PHASES.ZOOM_IN.ZOOM_TO_TARGET; // 500 ms
|
|
90
|
-
const FADE_OUT_DELAY = ANIMATION_PHASES.ZOOM_IN.FADE_OUT_START; // 120 ms
|
|
91
|
-
const FADE_OUT_DURATION = ANIMATION_PHASES.ZOOM_IN.FADE_OUT; // 280 ms — completes at 400 ms
|
|
92
|
-
const SWAP_AT_MS = ANIMATION_PHASES.ZOOM_IN.CONTENT_SWAP; // 310 ms
|
|
93
|
-
const FADE_IN_DURATION = ANIMATION_PHASES.ZOOM_IN.FADE_IN; // 200 ms
|
|
94
|
-
const SETTLE_FACTOR = 0.94;
|
|
95
|
-
|
|
96
|
-
const z0 = current.zoomLevel;
|
|
97
|
-
const dx = targetNode.x - current.centerX;
|
|
98
|
-
const dy = targetNode.y - current.centerY;
|
|
99
|
-
const vmin = Math.min(canvasWidth, canvasHeight);
|
|
100
|
-
const fillZoom = Math.max(4.0, (vmin * 1.5) / Math.max(targetNode.width, 100));
|
|
101
|
-
|
|
102
|
-
const fromRgb = parseColor(fromColor);
|
|
103
|
-
const toRgb = parseColor(toColor);
|
|
104
|
-
const state = { viewport: { centerX: 0, centerY: 0, zoomLevel: 0 }, opacity: 1, backgroundColor: fromColor };
|
|
105
|
-
|
|
106
|
-
const start = performance.now();
|
|
107
|
-
let swapped = false;
|
|
108
|
-
let fadeInStart = 0;
|
|
109
|
-
|
|
110
|
-
cb.onPhaseChange?.('zoom');
|
|
111
|
-
|
|
112
|
-
const tick = (now: number): void => {
|
|
113
|
-
if (!this._active) return;
|
|
114
|
-
const elapsed = now - start;
|
|
115
|
-
|
|
116
|
-
if (!swapped) {
|
|
117
|
-
if (elapsed >= SWAP_AT_MS) {
|
|
118
|
-
swapped = true;
|
|
119
|
-
fadeInStart = now;
|
|
120
|
-
state.viewport = { centerX: childCenter.x, centerY: childCenter.y, zoomLevel: targetZoom * SETTLE_FACTOR };
|
|
121
|
-
state.opacity = 0;
|
|
122
|
-
state.backgroundColor = toColor;
|
|
123
|
-
cb.onUpdate(state);
|
|
124
|
-
cb.onPhaseChange?.('swap');
|
|
125
|
-
this.rafId = requestAnimationFrame(tick);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const rawT = Math.min(elapsed / ZOOM_DURATION, 1);
|
|
129
|
-
const zoomT = easeZoomTransition(rawT);
|
|
130
|
-
const zl = lerp(z0, fillZoom, zoomT);
|
|
131
|
-
state.viewport = {
|
|
132
|
-
centerX: targetNode.x - dx * z0 * (1 - zoomT) / zl,
|
|
133
|
-
centerY: targetNode.y - dy * z0 * (1 - zoomT) / zl,
|
|
134
|
-
zoomLevel: zl,
|
|
135
|
-
};
|
|
136
|
-
state.opacity = elapsed > FADE_OUT_DELAY
|
|
137
|
-
? Math.max(0, 1 - easeOutCubic(Math.min((elapsed - FADE_OUT_DELAY) / FADE_OUT_DURATION, 1)))
|
|
138
|
-
: 1;
|
|
139
|
-
state.backgroundColor = interpolateRgb(fromRgb, toRgb, rawT);
|
|
140
|
-
cb.onUpdate(state);
|
|
141
|
-
} else {
|
|
142
|
-
const fi = now - fadeInStart;
|
|
143
|
-
const ft = Math.min(fi / FADE_IN_DURATION, 1);
|
|
144
|
-
const eased = easeOutCubic(ft);
|
|
145
|
-
state.viewport = { centerX: childCenter.x, centerY: childCenter.y, zoomLevel: lerp(targetZoom * SETTLE_FACTOR, targetZoom, eased) };
|
|
146
|
-
state.opacity = eased;
|
|
147
|
-
state.backgroundColor = toColor;
|
|
148
|
-
cb.onUpdate(state);
|
|
149
|
-
if (ft >= 1) { cb.onPhaseChange?.('fade-in'); this.rafId = null; this._complete(cb.onComplete); return; }
|
|
150
|
-
}
|
|
151
|
-
this.rafId = requestAnimationFrame(tick);
|
|
152
|
-
};
|
|
153
|
-
this.rafId = requestAnimationFrame(tick);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── Zoom out to parent layer ───────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
animateZoomOutToParent(
|
|
159
|
-
current: AnimatorViewport,
|
|
160
|
-
parent: AnimatorViewport,
|
|
161
|
-
fromColor: string,
|
|
162
|
-
toColor: string,
|
|
163
|
-
cb: TransitionCallbacks,
|
|
164
|
-
): void {
|
|
165
|
-
if (this._active) return;
|
|
166
|
-
this._active = true;
|
|
167
|
-
|
|
168
|
-
const FADE_OUT = ANIMATION_PHASES.ZOOM_OUT.FADE_OUT;
|
|
169
|
-
const FADE_IN = ANIMATION_PHASES.ZOOM_OUT.FADE_IN;
|
|
170
|
-
const OPACITY_SETTLE = 66;
|
|
171
|
-
|
|
172
|
-
const fromRgb = parseColor(fromColor);
|
|
173
|
-
const toRgb = parseColor(toColor);
|
|
174
|
-
const state = { viewport: { centerX: 0, centerY: 0, zoomLevel: 0 }, opacity: 1, backgroundColor: fromColor };
|
|
175
|
-
|
|
176
|
-
const start = performance.now();
|
|
177
|
-
let swapped = false;
|
|
178
|
-
let fadeInStart = 0;
|
|
179
|
-
|
|
180
|
-
cb.onPhaseChange?.('fade-out');
|
|
181
|
-
|
|
182
|
-
const tick = (now: number): void => {
|
|
183
|
-
if (!this._active) return;
|
|
184
|
-
const elapsed = now - start;
|
|
185
|
-
|
|
186
|
-
if (!swapped) {
|
|
187
|
-
if (elapsed >= FADE_OUT) {
|
|
188
|
-
swapped = true; fadeInStart = now;
|
|
189
|
-
state.viewport = { centerX: parent.centerX, centerY: parent.centerY, zoomLevel: parent.zoomLevel * 5 };
|
|
190
|
-
state.opacity = 0;
|
|
191
|
-
state.backgroundColor = toColor;
|
|
192
|
-
cb.onUpdate(state); cb.onPhaseChange?.('zoom-back');
|
|
193
|
-
this.rafId = requestAnimationFrame(tick); return;
|
|
194
|
-
}
|
|
195
|
-
const rawT = Math.min(elapsed / FADE_OUT, 1);
|
|
196
|
-
const t = easeOutCubic(rawT);
|
|
197
|
-
state.viewport = { centerX: current.centerX, centerY: current.centerY, zoomLevel: lerp(current.zoomLevel, current.zoomLevel * 0.6, t) };
|
|
198
|
-
state.opacity = 1 - t;
|
|
199
|
-
state.backgroundColor = interpolateRgb(fromRgb, toRgb, rawT);
|
|
200
|
-
cb.onUpdate(state);
|
|
201
|
-
} else {
|
|
202
|
-
const fi = now - fadeInStart;
|
|
203
|
-
const rawT = Math.min(fi / FADE_IN, 1);
|
|
204
|
-
const t = easeZoomTransition(rawT);
|
|
205
|
-
state.viewport = { centerX: parent.centerX, centerY: parent.centerY, zoomLevel: lerp(parent.zoomLevel * 5, parent.zoomLevel, t) };
|
|
206
|
-
state.opacity = fi < OPACITY_SETTLE ? 0 : easeZoomTransition(Math.min((fi - OPACITY_SETTLE) / (FADE_IN - OPACITY_SETTLE), 1));
|
|
207
|
-
state.backgroundColor = toColor;
|
|
208
|
-
cb.onUpdate(state);
|
|
209
|
-
if (rawT >= 1) { cb.onPhaseChange?.('fade-in'); this.rafId = null; this._complete(cb.onComplete); return; }
|
|
210
|
-
}
|
|
211
|
-
this.rafId = requestAnimationFrame(tick);
|
|
212
|
-
};
|
|
213
|
-
this.rafId = requestAnimationFrame(tick);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private _complete(onComplete: () => void): void {
|
|
217
|
-
this.timers.forEach(clearTimeout); this.timers = [];
|
|
218
|
-
this._active = false;
|
|
219
|
-
onComplete();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export const konvaLayerTransitionAnimator = new KonvaLayerTransitionAnimator();
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// ─── SketchyBoxKonva ─────────────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// Renders the hand-drawn tooltip box using an imperative Konva canvas.
|
|
4
|
-
// Two paths are scaled to fit the container:
|
|
5
|
-
// - BACK_PATH : shadow stroke, offset 8px down
|
|
6
|
-
// - FRONT_PATH: filled main box, offset 5.65px right
|
|
7
|
-
//
|
|
8
|
-
// Matches the visualli.ai SketchyBoxKonva component exactly.
|
|
9
|
-
|
|
10
|
-
import React, { useEffect, useRef } from 'react';
|
|
11
|
-
import Konva from 'konva';
|
|
12
|
-
|
|
13
|
-
const BACK_PATH =
|
|
14
|
-
'M0.653,0 L485.653,1 C485.653,1,484.782,152.773,' +
|
|
15
|
-
'486.653,165 C413.653,171,14.653,178,5.653,168' +
|
|
16
|
-
'C2.653,158,0.653,0,0.653,0';
|
|
17
|
-
|
|
18
|
-
const FRONT_PATH =
|
|
19
|
-
'M485,167.524 L0,166.524' +
|
|
20
|
-
'C0,166.524,1.870,8.751,0,0' +
|
|
21
|
-
'C73,0,479,0,487,8' +
|
|
22
|
-
'C490,18,485,167.524,485,167.524';
|
|
23
|
-
|
|
24
|
-
const BACK_W = 486;
|
|
25
|
-
const BACK_H = 178;
|
|
26
|
-
const FRONT_W = 490;
|
|
27
|
-
const FRONT_H = 168;
|
|
28
|
-
|
|
29
|
-
const BACK_OFFSET_Y = 8;
|
|
30
|
-
const FRONT_OFFSET_X = 5.65;
|
|
31
|
-
const SHADOW_PAD = 12;
|
|
32
|
-
|
|
33
|
-
export interface SketchyBoxKonvaProps {
|
|
34
|
-
children: React.ReactNode;
|
|
35
|
-
fill: string;
|
|
36
|
-
stroke: string;
|
|
37
|
-
backStroke: string;
|
|
38
|
-
padding?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const SketchyBoxKonva: React.FC<SketchyBoxKonvaProps> = ({
|
|
42
|
-
children,
|
|
43
|
-
fill,
|
|
44
|
-
stroke,
|
|
45
|
-
backStroke,
|
|
46
|
-
padding = '1rem 1.25rem',
|
|
47
|
-
}) => {
|
|
48
|
-
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
49
|
-
const konvaContainerRef = useRef<HTMLDivElement>(null);
|
|
50
|
-
const stageRef = useRef<Konva.Stage | null>(null);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
const wrapper = wrapperRef.current;
|
|
54
|
-
const konvaEl = konvaContainerRef.current;
|
|
55
|
-
if (!wrapper || !konvaEl) return;
|
|
56
|
-
|
|
57
|
-
function draw(W: number, H: number) {
|
|
58
|
-
if (!konvaEl || W <= 0 || H <= 0) return;
|
|
59
|
-
if (stageRef.current) { stageRef.current.destroy(); stageRef.current = null; }
|
|
60
|
-
|
|
61
|
-
const stage = new Konva.Stage({ container: konvaEl, width: W, height: H });
|
|
62
|
-
const layer = new Konva.Layer({ listening: false });
|
|
63
|
-
const contentH = H - SHADOW_PAD;
|
|
64
|
-
|
|
65
|
-
layer.add(new Konva.Path({
|
|
66
|
-
x: 0, y: BACK_OFFSET_Y,
|
|
67
|
-
data: BACK_PATH,
|
|
68
|
-
scaleX: W / BACK_W,
|
|
69
|
-
scaleY: contentH / BACK_H,
|
|
70
|
-
fill: '#ffffff',
|
|
71
|
-
stroke: backStroke,
|
|
72
|
-
strokeWidth: 2,
|
|
73
|
-
strokeScaleEnabled: false,
|
|
74
|
-
listening: false,
|
|
75
|
-
}));
|
|
76
|
-
|
|
77
|
-
layer.add(new Konva.Path({
|
|
78
|
-
x: FRONT_OFFSET_X, y: 0,
|
|
79
|
-
data: FRONT_PATH,
|
|
80
|
-
scaleX: (W - FRONT_OFFSET_X) / FRONT_W,
|
|
81
|
-
scaleY: contentH / FRONT_H,
|
|
82
|
-
fill,
|
|
83
|
-
stroke,
|
|
84
|
-
strokeWidth: 3,
|
|
85
|
-
strokeScaleEnabled: false,
|
|
86
|
-
listening: false,
|
|
87
|
-
}));
|
|
88
|
-
|
|
89
|
-
stage.add(layer);
|
|
90
|
-
stageRef.current = stage;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
draw(wrapper.offsetWidth, wrapper.offsetHeight);
|
|
94
|
-
|
|
95
|
-
const ro = new ResizeObserver(entries => {
|
|
96
|
-
const el = entries[0].target as HTMLElement;
|
|
97
|
-
draw(el.offsetWidth, el.offsetHeight);
|
|
98
|
-
});
|
|
99
|
-
ro.observe(wrapper);
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
ro.disconnect();
|
|
103
|
-
stageRef.current?.destroy();
|
|
104
|
-
stageRef.current = null;
|
|
105
|
-
};
|
|
106
|
-
}, [fill, stroke, backStroke]);
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<div ref={wrapperRef} style={{ position: 'relative', paddingBottom: `${SHADOW_PAD}px` }}>
|
|
110
|
-
<div
|
|
111
|
-
ref={konvaContainerRef}
|
|
112
|
-
style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 0, overflow: 'hidden' }}
|
|
113
|
-
/>
|
|
114
|
-
<div style={{ position: 'relative', padding, zIndex: 1 }}>
|
|
115
|
-
{children}
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
export default SketchyBoxKonva;
|
package/src/config/blobShapes.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
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
|
-
}
|
package/src/config/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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';
|