@visualli/core 0.1.6 → 0.2.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/README.md +37 -105
- package/dist/chunk-25AJTPMS.js +68 -0
- package/dist/chunk-25AJTPMS.js.map +1 -0
- package/dist/{chunk-6NXADJK2.js → chunk-7NJWM7ZE.js} +84 -117
- package/dist/chunk-7NJWM7ZE.js.map +1 -0
- package/dist/chunk-KVSM3APM.js +96 -0
- package/dist/chunk-KVSM3APM.js.map +1 -0
- package/dist/{chunk-PDP5CVUL.js → chunk-LOQLNYB3.js} +2 -2
- package/dist/chunk-LV3EXUQY.js +753 -0
- package/dist/chunk-LV3EXUQY.js.map +1 -0
- package/dist/chunk-N7HCYULH.js +12 -0
- package/dist/chunk-N7HCYULH.js.map +1 -0
- package/dist/{chunk-I75TMOE4.js → chunk-P6PKWVYF.js} +2 -2
- package/dist/chunk-TPLJJKAU.js +151 -0
- package/dist/chunk-TPLJJKAU.js.map +1 -0
- package/dist/chunk-UAMEJG2C.js +267 -0
- package/dist/chunk-UAMEJG2C.js.map +1 -0
- package/dist/{chunk-LJYSKSCE.js → chunk-W2CAOOJJ.js} +6 -6
- package/dist/chunk-W2CAOOJJ.js.map +1 -0
- package/dist/config/index.cjs +250 -349
- package/dist/config/index.cjs.map +1 -1
- package/dist/config/index.d.cts +107 -66
- package/dist/config/index.d.ts +107 -66
- package/dist/config/index.js +52 -34
- package/dist/constants/index.cjs +14 -333
- package/dist/constants/index.cjs.map +1 -1
- package/dist/constants/index.d.cts +5 -243
- package/dist/constants/index.d.ts +5 -243
- package/dist/constants/index.js +5 -75
- package/dist/designSystem-BOrZHfEq.d.cts +150 -0
- package/dist/designSystem-BOrZHfEq.d.ts +150 -0
- package/dist/document-BFBwn93X.d.ts +26 -0
- package/dist/document-MEJEfxl3.d.cts +26 -0
- package/dist/ideaSize-CLDKeyqD.d.cts +11 -0
- package/dist/ideaSize-DxxGy2E8.d.ts +11 -0
- package/dist/index-BmILeKj6.d.cts +99 -0
- package/dist/index-CcPRwVUL.d.ts +99 -0
- package/dist/index.cjs +1636 -961
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +350 -27
- package/dist/index.d.ts +350 -27
- package/dist/index.js +445 -190
- package/dist/index.js.map +1 -1
- package/dist/{document-B3hnNr-F.d.cts → layer-B2A3Ki48.d.cts} +2 -16
- package/dist/{document-B3hnNr-F.d.ts → layer-B2A3Ki48.d.ts} +2 -16
- package/dist/layout/index.d.cts +2 -1
- package/dist/layout/index.d.ts +2 -1
- package/dist/mindmap-C3Qr0qKW.d.cts +182 -0
- package/dist/mindmap-DHeVa86G.d.ts +182 -0
- package/dist/parser/index.cjs +745 -179
- package/dist/parser/index.cjs.map +1 -1
- package/dist/parser/index.d.cts +23 -8
- package/dist/parser/index.d.ts +23 -8
- package/dist/parser/index.js +7 -2
- package/dist/performance/index.cjs +13 -0
- package/dist/performance/index.cjs.map +1 -1
- package/dist/performance/index.js +2 -2
- package/dist/theme/index.cjs +884 -0
- package/dist/theme/index.cjs.map +1 -0
- package/dist/theme/index.d.cts +2 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +53 -0
- package/dist/types/index.d.cts +4 -2
- package/dist/types/index.d.ts +4 -2
- package/dist/viewport/index.cjs +17 -6
- package/dist/viewport/index.cjs.map +1 -1
- package/dist/viewport/index.d.cts +6 -4
- package/dist/viewport/index.d.ts +6 -4
- package/dist/viewport/index.js +2 -2
- package/licenses/visualli-design-system/LICENSE +9 -0
- package/licenses/visualli-design-system/NOTICE.md +6 -0
- package/package.json +13 -5
- package/src/config/README.md +2 -4
- package/src/config/USAGE_EXAMPLES.md +0 -4
- package/src/config/index.ts +3 -4
- package/src/constants/index.ts +1 -1
- package/src/constants/layout.ts +11 -0
- package/src/constants/performanceConstants.ts +8 -27
- package/src/generated/README.md +12 -0
- package/src/generated/designSystem.ts +758 -0
- package/src/generated/geometry/blob.ts +99 -0
- package/src/generated/geometry/blobShapes.ts +4 -0
- package/src/generated/geometry/color.ts +37 -0
- package/src/generated/geometry/container.ts +84 -0
- package/src/generated/geometry/detail.ts +22 -0
- package/src/generated/geometry/idea.ts +56 -0
- package/src/generated/geometry/interaction.ts +122 -0
- package/src/generated/geometry/motion.ts +140 -0
- package/src/index.ts +18 -1
- package/src/parser/configUtils.ts +12 -19
- package/src/parser/visualliConverter.ts +71 -112
- package/src/parser/visualliParser.ts +16 -1
- package/src/rendering/culling.ts +16 -12
- package/src/rendering/ideaSize.ts +21 -0
- package/src/rendering/nodeGeometry.ts +97 -0
- package/src/theme/index.ts +136 -0
- package/src/types/document.ts +9 -0
- package/src/types/layer.ts +1 -1
- package/src/types/mindmap.ts +13 -0
- package/src/utils/navigation.ts +33 -63
- package/src/viewport/viewportUtils.ts +6 -4
- package/dist/animations/index.cjs +0 -146
- package/dist/animations/index.cjs.map +0 -1
- package/dist/animations/index.d.cts +0 -81
- package/dist/animations/index.d.ts +0 -81
- package/dist/animations/index.js +0 -35
- package/dist/chunk-6NXADJK2.js.map +0 -1
- package/dist/chunk-F36SMC3K.js +0 -106
- package/dist/chunk-F36SMC3K.js.map +0 -1
- package/dist/chunk-G5H3P4YZ.js +0 -273
- package/dist/chunk-G5H3P4YZ.js.map +0 -1
- package/dist/chunk-LDJRGT2Q.js +0 -163
- package/dist/chunk-LDJRGT2Q.js.map +0 -1
- package/dist/chunk-LJYSKSCE.js.map +0 -1
- package/dist/chunk-LNTJTDB2.js +0 -349
- package/dist/chunk-LNTJTDB2.js.map +0 -1
- package/dist/mindmap-DWS0WZT_.d.cts +0 -118
- package/dist/mindmap-DWS0WZT_.d.ts +0 -118
- package/src/animations/constants.ts +0 -72
- package/src/animations/easing.ts +0 -70
- package/src/animations/index.ts +0 -2
- package/src/config/blobShapes.ts +0 -146
- package/src/config/textScaling.ts +0 -54
- package/src/constants/design.ts +0 -281
- /package/dist/{chunk-PDP5CVUL.js.map → chunk-LOQLNYB3.js.map} +0 -0
- /package/dist/{chunk-I75TMOE4.js.map → chunk-P6PKWVYF.js.map} +0 -0
- /package/dist/{animations → theme}/index.js.map +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/blob.ts — do not edit here.
|
|
2
|
+
import { BLOB_SHAPES } from './blobShapes';
|
|
3
|
+
export { BLOB_SHAPES };
|
|
4
|
+
|
|
5
|
+
/** Which outline ideas take. Every idea, at every depth, has the same shape (`idea`, the shape of the map's centre);
|
|
6
|
+
* depth shows through the depth trail, rings, the layer tint and the step-inside motion. The other authored shapes
|
|
7
|
+
* stay in BLOB_SHAPES for explicit use (`Node shape`); `varyByLevel: true` brings back one shape per level. */
|
|
8
|
+
export const SHAPE: { idea: number; varyByLevel: boolean } = { idea: 0, varyByLevel: false };
|
|
9
|
+
|
|
10
|
+
/** Shape for a layer level: SHAPE.idea for every level. With SHAPE.varyByLevel, level 0 is SHAPE.idea and consecutive
|
|
11
|
+
* levels never repeat. */
|
|
12
|
+
export function shapeForLevel(level: number): number {
|
|
13
|
+
if (!SHAPE.varyByLevel) return SHAPE.idea;
|
|
14
|
+
const n = BLOB_SHAPES.length;
|
|
15
|
+
let cur = SHAPE.idea;
|
|
16
|
+
for (let i = 1; i <= level; i++) {
|
|
17
|
+
const r = Math.floor(Math.abs(Math.sin((i + 1) * 12.9898) * 43758.5453));
|
|
18
|
+
cur = (cur + (r % (n - 1)) + 1) % n;
|
|
19
|
+
}
|
|
20
|
+
return cur;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** SVG path of a node outline centered on (cx, cy): midpoint-quadratic smoothing through the authored points. */
|
|
24
|
+
export function blobPath(shape: number, rx: number, ry: number, cx = 0, cy = 0): string {
|
|
25
|
+
const pts = BLOB_SHAPES[((shape % BLOB_SHAPES.length) + BLOB_SHAPES.length) % BLOB_SHAPES.length].map(([x, y]) => [cx + x * rx, cy + y * ry]);
|
|
26
|
+
const mid = (a: number[], b: number[]) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
27
|
+
const f = (n: number) => n.toFixed(2);
|
|
28
|
+
const start = mid(pts[pts.length - 1], pts[0]);
|
|
29
|
+
let d = `M${f(start[0])} ${f(start[1])}`;
|
|
30
|
+
for (let i = 0; i < pts.length; i++) {
|
|
31
|
+
const p = pts[i], m = mid(p, pts[(i + 1) % pts.length]);
|
|
32
|
+
d += `Q${f(p[0])} ${f(p[1])} ${f(m[0])} ${f(m[1])}`;
|
|
33
|
+
}
|
|
34
|
+
return d + 'Z';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The path calls drawBlob makes: satisfied by a CanvasRenderingContext2D and by Konva's Context (sceneFunc / hitFunc). */
|
|
38
|
+
export type PathContext = { beginPath(): void; moveTo(x: number, y: number): void; quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; closePath(): void };
|
|
39
|
+
|
|
40
|
+
/** Canvas twin of blobPath — call fill()/stroke() after (in Konva: ctx.fillStrokeShape(shape)). */
|
|
41
|
+
export function drawBlob(ctx: PathContext, shape: number, rx: number, ry: number, cx = 0, cy = 0) {
|
|
42
|
+
const pts = BLOB_SHAPES[((shape % BLOB_SHAPES.length) + BLOB_SHAPES.length) % BLOB_SHAPES.length].map(([x, y]) => [cx + x * rx, cy + y * ry]);
|
|
43
|
+
const mid = (a: number[], b: number[]) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
44
|
+
const s = mid(pts[pts.length - 1], pts[0]);
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.moveTo(s[0], s[1]);
|
|
47
|
+
pts.forEach((p, i) => { const m = mid(p, pts[(i + 1) % pts.length]); ctx.quadraticCurveTo(p[0], p[1], m[0], m[1]); });
|
|
48
|
+
ctx.closePath();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Distance from the center to the smoothed outline (blobPath) in direction `angle` (radians, SVG y-down).
|
|
52
|
+
* Connectors use it to start and end just outside the outline an idea actually has, not a stand-in ellipse. */
|
|
53
|
+
export function blobRadius(shape: number, rx: number, ry: number, angle: number): number {
|
|
54
|
+
const pts = BLOB_SHAPES[((shape % BLOB_SHAPES.length) + BLOB_SHAPES.length) % BLOB_SHAPES.length].map(([x, y]) => [x * rx, y * ry]);
|
|
55
|
+
const mid = (a: number[], b: number[]) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
56
|
+
// Sample the midpoint-quadratic curve into a polygon, then cast a ray from the center.
|
|
57
|
+
const poly: number[][] = [];
|
|
58
|
+
for (let i = 0; i < pts.length; i++) {
|
|
59
|
+
const p0 = mid(pts[(i - 1 + pts.length) % pts.length], pts[i]), c = pts[i], p1 = mid(pts[i], pts[(i + 1) % pts.length]);
|
|
60
|
+
for (let k = 0; k < 8; k++) { const t = k / 8, u = 1 - t; poly.push([u * u * p0[0] + 2 * u * t * c[0] + t * t * p1[0], u * u * p0[1] + 2 * u * t * c[1] + t * t * p1[1]]); }
|
|
61
|
+
}
|
|
62
|
+
const dx = Math.cos(angle), dy = Math.sin(angle);
|
|
63
|
+
let best = 0;
|
|
64
|
+
for (let i = 0; i < poly.length; i++) {
|
|
65
|
+
const [ax, ay] = poly[i], [bx, by] = poly[(i + 1) % poly.length], ex = bx - ax, ey = by - ay;
|
|
66
|
+
const den = dx * ey - dy * ex;
|
|
67
|
+
if (Math.abs(den) < 1e-9) continue;
|
|
68
|
+
const r = (ax * ey - ay * ex) / den, q = (ax * dy - ay * dx) / den;
|
|
69
|
+
if (r > 0 && q >= 0 && q <= 1) best = Math.max(best, r);
|
|
70
|
+
}
|
|
71
|
+
return best || Math.max(rx, ry);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Ring recipe: index 0 is the innermost ring. A node shows min(childLayers, 3) rings. */
|
|
75
|
+
export const RINGS = [
|
|
76
|
+
{ scale: 1.1, opacity: 0.5, rotate: -5, width: 2, dash: '' },
|
|
77
|
+
{ scale: 1.2, opacity: 0.4, rotate: -10, width: 1, dash: '' },
|
|
78
|
+
{ scale: 1.3, opacity: 0.3, rotate: -15, width: 3, dash: '6 4' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** Connector geometry: a cubic that bows perpendicular to the chord, flatter as distance grows. */
|
|
82
|
+
export function edgePath(a: { x: number; y: number }, b: { x: number; y: number }, bow = 1) {
|
|
83
|
+
const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1;
|
|
84
|
+
const arc = Math.min(45, 45 / (1 + Math.max(0, (d - 200) / 400))) * bow;
|
|
85
|
+
const nx = -dy / d, ny = dx / d;
|
|
86
|
+
const mx = (a.x + b.x) / 2 + nx * arc, my = (a.y + b.y) / 2 + ny * arc;
|
|
87
|
+
const c1 = { x: a.x + (mx - a.x) * 0.66 + 0, y: a.y + (my - a.y) * 0.66 };
|
|
88
|
+
const c2 = { x: b.x + (mx - b.x) * 0.66, y: b.y + (my - b.y) * 0.66 };
|
|
89
|
+
const ang = Math.atan2(b.y - c2.y, b.x - c2.x);
|
|
90
|
+
return { d: `M${a.x.toFixed(1)} ${a.y.toFixed(1)}C${c1.x.toFixed(1)} ${c1.y.toFixed(1)} ${c2.x.toFixed(1)} ${c2.y.toFixed(1)} ${b.x.toFixed(1)} ${b.y.toFixed(1)}`, angle: ang, mid: { x: mx, y: my } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Open V arrowhead at point p, pointing along angle. */
|
|
94
|
+
export function arrowPath(p: { x: number; y: number }, angle: number, len = 11) {
|
|
95
|
+
const s = Math.PI / 5.5;
|
|
96
|
+
const a1 = { x: p.x - len * Math.cos(angle - s), y: p.y - len * Math.sin(angle - s) };
|
|
97
|
+
const a2 = { x: p.x - len * Math.cos(angle + s), y: p.y - len * Math.sin(angle + s) };
|
|
98
|
+
return `M${a1.x.toFixed(1)} ${a1.y.toFixed(1)}L${p.x.toFixed(1)} ${p.y.toFixed(1)}L${a2.x.toFixed(1)} ${a2.y.toFixed(1)}`;
|
|
99
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/blobShapes.ts — do not edit here.
|
|
2
|
+
// The six hand-authored Visualli node outlines, in normalized units (rx = ry = 1).
|
|
3
|
+
// Copied verbatim from the web app (features/canvas/config/blobShapes.ts). Shape 0 is the root shape.
|
|
4
|
+
export const BLOB_SHAPES: number[][][] = [[[1.12,0.0],[0.86,0.56],[0.46,0.94],[-0.08,1.06],[-0.68,0.82],[-0.96,0.08],[-0.55,-0.82],[0.04,-1.02],[0.72,-0.8],[1.08,-0.38]],[[-0.489,-0.625],[-0.384,-0.716],[-0.285,-0.792],[-0.189,-0.855],[-0.09,-0.903],[0.014,-0.932],[0.129,-0.94],[0.255,-0.924],[0.412,-0.876],[0.516,-0.832],[0.612,-0.78],[0.695,-0.725],[0.767,-0.666],[0.83,-0.606],[0.885,-0.546],[0.933,-0.486],[0.979,-0.425],[1.006,-0.385],[1.034,-0.334],[1.058,-0.276],[1.078,-0.209],[1.092,-0.133],[1.101,-0.046],[1.102,0.052],[1.094,0.173],[1.083,0.244],[1.063,0.324],[1.034,0.401],[0.996,0.477],[0.948,0.549],[0.889,0.617],[0.82,0.681],[0.735,0.741],[0.658,0.783],[0.569,0.822],[0.474,0.855],[0.372,0.882],[0.265,0.904],[0.156,0.92],[0.046,0.931],[-0.069,0.938],[-0.155,0.935],[-0.25,0.919],[-0.346,0.89],[-0.443,0.851],[-0.539,0.8],[-0.633,0.738],[-0.723,0.667],[-0.808,0.585],[-0.882,0.501],[-0.944,0.421],[-0.994,0.343],[-1.034,0.267],[-1.064,0.192],[-1.085,0.12],[-1.097,0.042],[-1.102,0.001],[-1.09,-0.243],[-1.05,-0.365],[-0.99,-0.441],[-0.911,-0.479],[-0.819,-0.499],[-0.715,-0.517],[-0.605,-0.551]],[[0.003,-0.97],[0.163,-0.984],[0.286,-1.028],[0.389,-1.096],[0.488,-1.174],[0.597,-1.21],[0.728,-1.165],[0.867,-1.036],[0.996,-0.839],[1.098,-0.59],[1.158,-0.304],[1.153,0.012],[1.08,0.336],[0.944,0.639],[0.756,0.889],[0.484,1.072],[0.339,1.079],[0.219,1.051],[0.131,0.994],[0.059,0.939],[0.008,0.915],[-0.105,0.931],[-0.176,0.965],[-0.249,1.005],[-0.345,1.039],[-0.497,1.055],[-0.672,1.032],[-0.817,0.952],[-0.945,0.81],[-1.054,0.6],[-1.16,0.302],[-1.187,-0.011],[-1.165,-0.326],[-1.097,-0.599],[-0.991,-0.838],[-0.852,-0.912],[-0.695,-0.958],[-0.53,-0.988],[-0.357,-0.992],[-0.18,-0.983]],[[-0.734,-1.127],[-0.656,-1.146],[-0.567,-1.142],[-0.468,-1.115],[-0.358,-1.079],[-0.238,-1.034],[-0.107,-0.989],[0.035,-0.953],[0.188,-0.929],[0.325,-0.916],[0.484,-0.896],[0.65,-0.854],[0.811,-0.77],[0.955,-0.631],[1.068,-0.422],[1.138,-0.126],[1.152,0.272],[1.121,0.451],[1.046,0.602],[0.928,0.727],[0.767,0.833],[0.565,0.925],[0.323,1.001],[0.042,1.075],[-0.277,1.142],[-0.368,1.147],[-0.475,1.136],[-0.589,1.104],[-0.704,1.05],[-0.815,0.973],[-0.913,0.871],[-0.993,0.746],[-1.048,0.593],[-1.109,0.298],[-1.145,0.018],[-1.154,-0.243],[-1.135,-0.482],[-1.085,-0.695],[-1.003,-0.874],[-0.886,-1.02]],[[-0.734,1.127],[-0.656,1.146],[-0.567,1.142],[-0.468,1.115],[-0.358,1.079],[-0.238,1.034],[-0.107,0.989],[0.035,0.953],[0.188,0.929],[0.325,0.916],[0.484,0.896],[0.65,0.854],[0.811,0.77],[0.955,0.631],[1.068,0.422],[1.138,0.126],[1.152,-0.272],[1.121,-0.451],[1.046,-0.602],[0.928,-0.727],[0.767,-0.833],[0.565,-0.925],[0.323,-1.001],[0.042,-1.075],[-0.277,-1.142],[-0.368,-1.147],[-0.475,-1.136],[-0.589,-1.104],[-0.704,-1.05],[-0.815,-0.973],[-0.913,-0.871],[-0.993,-0.746],[-1.048,-0.593],[-1.109,-0.298],[-1.145,-0.018],[-1.154,0.243],[-1.135,0.482],[-1.085,0.695],[-1.003,0.874],[-0.886,1.02]],[[0.001,-1.006],[0.134,-1.001],[0.262,-1.024],[0.382,-1.073],[0.498,-1.107],[0.614,-1.105],[0.733,-1.049],[0.85,-0.932],[0.953,-0.755],[1.03,-0.529],[1.067,-0.265],[1.054,0.018],[0.986,0.3],[0.868,0.557],[0.715,0.766],[0.55,0.909],[0.396,0.984],[0.262,1.005],[0.148,1.003],[0.037,1.008],[-0.083,1.04],[-0.22,1.118],[-0.36,1.192],[-0.5,1.234],[-0.65,1.175],[-0.81,1.045],[-0.99,0.855],[-1.14,0.61],[-1.24,0.33],[-1.27,0.04],[-1.275,-0.25],[-1.225,-0.5],[-1.08,-0.72],[-0.89,-0.92],[-0.734,-1.053],[-0.614,-1.112],[-0.498,-1.127],[-0.381,-1.11],[-0.261,-1.095],[-0.133,-1.058],[0.001,-1.01],[0.134,-1.001]]];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/color.ts — do not edit here.
|
|
2
|
+
/* Which topic an idea is drawn in. A `.visualli` file's `data.color` may name one of the eight topics or be any
|
|
3
|
+
other color, or be missing. Every renderer resolves it with this rule, so a file looks the same everywhere:
|
|
4
|
+
a topic name ('teal', 'Harbor ') → that topic (case and surrounding spaces don't matter);
|
|
5
|
+
no color → the topics in order, by the idea's position among its siblings (so siblings differ);
|
|
6
|
+
anything else (e.g. '#b7e7f3') → null: not a topic — drawn as given (customColor): the color as the fill, and
|
|
7
|
+
the fill darkened (CUSTOM.ringShade) as the ring; a custom color doesn't follow themes.
|
|
8
|
+
No dependencies, so it can travel alone. */
|
|
9
|
+
|
|
10
|
+
/** The eight topics, in the order missing colors cycle through. */
|
|
11
|
+
export const TOPIC_ORDER = ['teal', 'harbor', 'iris', 'berry', 'coral', 'amber', 'sun', 'stone'] as const;
|
|
12
|
+
export type TopicName = (typeof TOPIC_ORDER)[number];
|
|
13
|
+
|
|
14
|
+
/** The topic a color names, or null. */
|
|
15
|
+
export function topicFromName(color: string | null | undefined): TopicName | null {
|
|
16
|
+
if (!color) return null;
|
|
17
|
+
const c = color.trim().toLowerCase();
|
|
18
|
+
return (TOPIC_ORDER as readonly string[]).includes(c) ? (c as TopicName) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The topic for an idea: its named topic; with no color, the next topic in sibling order; null for any other color. */
|
|
22
|
+
export function topicFor(color: string | null | undefined, siblingIndex: number): TopicName | null {
|
|
23
|
+
if (!color || !color.trim()) return TOPIC_ORDER[((siblingIndex % TOPIC_ORDER.length) + TOPIC_ORDER.length) % TOPIC_ORDER.length];
|
|
24
|
+
return topicFromName(color);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A custom (non-topic) color: drawn as given, with a darker ring. */
|
|
28
|
+
export const CUSTOM = { /** The ring is the fill with each RGB channel multiplied by this. */ ringShade: 0.78 } as const;
|
|
29
|
+
|
|
30
|
+
/** Fill and ring for a custom color. `ring` is null when the color isn't a 6-digit hex (use the `edge` token then). */
|
|
31
|
+
export function customColor(color: string): { fill: string; ring: string | null } {
|
|
32
|
+
const m = /^#?([0-9a-f]{6})$/i.exec((color || '').trim());
|
|
33
|
+
if (!m) return { fill: color, ring: null };
|
|
34
|
+
const n = parseInt(m[1], 16), f = CUSTOM.ringShade;
|
|
35
|
+
const r = Math.round(((n >> 16) & 255) * f), g = Math.round(((n >> 8) & 255) * f), b = Math.round((n & 255) * f);
|
|
36
|
+
return { fill: color.trim(), ring: '#' + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1) };
|
|
37
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/container.ts — do not edit here.
|
|
2
|
+
/* Containers: the dashed hull around a group of ideas, and where its name goes.
|
|
3
|
+
The name is a pill straddling the hull's boundary. Which point of the boundary depends on what's inside and around
|
|
4
|
+
the container: the spot is chosen by scoring candidates along all four edges against the ideas, the connectors and
|
|
5
|
+
the names already placed — deterministic, so every renderer (VisualMap, the spec SDK on a canvas, PDF export) puts
|
|
6
|
+
it in the same place for the same file. Pure functions, no dependencies. World units (the map's coordinates). */
|
|
7
|
+
|
|
8
|
+
export type Pt = { x: number; y: number };
|
|
9
|
+
export type Box = { x0: number; y0: number; x1: number; y1: number };
|
|
10
|
+
export type Hull = Box & { radius: number };
|
|
11
|
+
export type LabelSide = 'bottom' | 'top' | 'left' | 'right';
|
|
12
|
+
export type LabelPlacement = { x: number; y: number; side: LabelSide; score: number };
|
|
13
|
+
|
|
14
|
+
/** The hull reaches this far beyond its ideas' centers, and rounds its corners this much. */
|
|
15
|
+
export const HULL = { padX: 150, padY: 125, radius: 48 };
|
|
16
|
+
/** Penalties: an idea or another name under the pill is never fine; a connector through it is bad; order breaks ties. */
|
|
17
|
+
const WEIGHT = { idea: 1000, label: 1000, connector: 60, preference: 1 };
|
|
18
|
+
|
|
19
|
+
/** Room kept between a member's outline and the hull, for ideas larger than the base size. */
|
|
20
|
+
export const HULL_CLEAR = { x: 50, y: 51 } as const;
|
|
21
|
+
|
|
22
|
+
/** The hull around a group's ideas: `padX` / `padY` beyond their centres, or more for an idea that has grown to show
|
|
23
|
+
* its label (`rx` / `ry`, its half size: then its outline plus HULL_CLEAR). */
|
|
24
|
+
export function containerHull(members: ReadonlyArray<Pt & { rx?: number; ry?: number }>): Hull | null {
|
|
25
|
+
if (!members.length) return null;
|
|
26
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
27
|
+
for (const m of members) {
|
|
28
|
+
const px = Math.max(HULL.padX, (m.rx ?? 0) + HULL_CLEAR.x), py = Math.max(HULL.padY, (m.ry ?? 0) + HULL_CLEAR.y);
|
|
29
|
+
x0 = Math.min(x0, m.x - px); x1 = Math.max(x1, m.x + px); y0 = Math.min(y0, m.y - py); y1 = Math.max(y1, m.y + py);
|
|
30
|
+
}
|
|
31
|
+
return { x0, x1, y0, y1, radius: HULL.radius };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Candidate spots for a w × h pill, in preference order: bottom center (the default look), top center, the thirds of
|
|
35
|
+
* bottom and top, then the sides. Spots keep the whole pill on the straight part of an edge, clear of the corners. */
|
|
36
|
+
export function labelCandidates(hull: Hull, w: number, h: number): { x: number; y: number; side: LabelSide }[] {
|
|
37
|
+
const { x0, x1, y0, y1, radius } = hull;
|
|
38
|
+
const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
|
|
39
|
+
const minX = x0 + radius + w / 2, maxX = x1 - radius - w / 2; // horizontal room on top / bottom
|
|
40
|
+
const minY = y0 + radius + h / 2, maxY = y1 - radius - h / 2; // vertical room on the sides
|
|
41
|
+
const along = (lo: number, hi: number, f: number) => (lo > hi ? (lo + hi) / 2 : lo + (hi - lo) * f);
|
|
42
|
+
const out: { x: number; y: number; side: LabelSide }[] = [
|
|
43
|
+
{ x: cx, y: y1, side: 'bottom' }, { x: cx, y: y0, side: 'top' },
|
|
44
|
+
{ x: along(minX, maxX, 0.2), y: y1, side: 'bottom' }, { x: along(minX, maxX, 0.8), y: y1, side: 'bottom' },
|
|
45
|
+
{ x: along(minX, maxX, 0.2), y: y0, side: 'top' }, { x: along(minX, maxX, 0.8), y: y0, side: 'top' },
|
|
46
|
+
{ x: x1, y: cy, side: 'right' }, { x: x0, y: cy, side: 'left' },
|
|
47
|
+
{ x: x1, y: along(minY, maxY, 0.25), side: 'right' }, { x: x1, y: along(minY, maxY, 0.75), side: 'right' },
|
|
48
|
+
{ x: x0, y: along(minY, maxY, 0.25), side: 'left' }, { x: x0, y: along(minY, maxY, 0.75), side: 'left' },
|
|
49
|
+
];
|
|
50
|
+
// The extremes of the top / bottom edges, for wide labels on narrow hulls.
|
|
51
|
+
out.push({ x: along(minX, maxX, 0), y: y1, side: 'bottom' }, { x: along(minX, maxX, 1), y: y1, side: 'bottom' },
|
|
52
|
+
{ x: along(minX, maxX, 0), y: y0, side: 'top' }, { x: along(minX, maxX, 1), y: y0, side: 'top' });
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const overlap = (a: Box, b: Box) => Math.max(0, Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0)) * Math.max(0, Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0));
|
|
57
|
+
|
|
58
|
+
/** Where a container's name goes. `ideas`: the boxes of every idea on the layer (outermost ring included);
|
|
59
|
+
* `connectors`: points sampled along every connector; `labels`: the pills already placed on this layer.
|
|
60
|
+
* `gap` keeps a little air between the pill and what it avoids. */
|
|
61
|
+
export function placeContainerLabel(hull: Hull, size: { w: number; h: number }, obstacles: { ideas: Box[]; connectors?: Pt[][]; labels?: Box[] }, gap = 10): LabelPlacement {
|
|
62
|
+
const { w, h } = size;
|
|
63
|
+
let best: LabelPlacement | null = null;
|
|
64
|
+
labelCandidates(hull, w, h).forEach((c, i) => {
|
|
65
|
+
const box = { x0: c.x - w / 2 - gap, x1: c.x + w / 2 + gap, y0: c.y - h / 2 - gap, y1: c.y + h / 2 + gap };
|
|
66
|
+
const area = w * h;
|
|
67
|
+
let score = i * WEIGHT.preference;
|
|
68
|
+
for (const b of obstacles.ideas) score += (overlap(box, b) / area) * WEIGHT.idea;
|
|
69
|
+
for (const b of obstacles.labels || []) score += (overlap(box, b) / area) * WEIGHT.label;
|
|
70
|
+
for (const line of obstacles.connectors || []) if (line.some((p) => p.x > box.x0 && p.x < box.x1 && p.y > box.y0 && p.y < box.y1)) score += WEIGHT.connector;
|
|
71
|
+
if (!best || score < best.score) best = { x: c.x, y: c.y, side: c.side, score };
|
|
72
|
+
});
|
|
73
|
+
return best!;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Points along a connector, for `connectors`: the same cubic edgePath draws (control points two-thirds of the way
|
|
77
|
+
* from each end to the bowed midpoint `mid`, which edgePath returns). */
|
|
78
|
+
export function connectorSamples(a: Pt, mid: Pt, b: Pt, n = 16): Pt[] {
|
|
79
|
+
const c1 = { x: a.x + (mid.x - a.x) * 0.66, y: a.y + (mid.y - a.y) * 0.66 }, c2 = { x: b.x + (mid.x - b.x) * 0.66, y: b.y + (mid.y - b.y) * 0.66 };
|
|
80
|
+
return Array.from({ length: n + 1 }, (_, k) => {
|
|
81
|
+
const t = k / n, u = 1 - t;
|
|
82
|
+
return { x: u * u * u * a.x + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * b.x, y: u * u * u * a.y + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * b.y };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/detail.ts — do not edit here.
|
|
2
|
+
/* Level of detail: what an idea draws at the size it's seen. Far-out views of large maps show ideas as specks; the
|
|
3
|
+
rings, label and shadow of an idea a few pixels wide can't be seen, and drawing them makes large maps slow. Every
|
|
4
|
+
renderer follows the same thresholds, so a map simplifies alike everywhere. Sizes are on-screen CSS pixels of the
|
|
5
|
+
idea's width. No dependencies, so it can travel alone. */
|
|
6
|
+
|
|
7
|
+
export const DETAIL = {
|
|
8
|
+
/** Below this on-screen width an idea draws only its body: no rings, label or shadow. */ bodyOnlyBelow: 24,
|
|
9
|
+
/** Below this it may be drawn as a plain filled speck in its topic fill (no outline) — indistinguishable at this size. */ speckBelow: 12,
|
|
10
|
+
/** Above this many ideas in view, idea shadows are skipped (the costliest effect to draw). */ shadowsMaxVisible: 150,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type IdeaDetail = 'full' | 'body' | 'speck';
|
|
14
|
+
|
|
15
|
+
/** What an idea of this on-screen width draws. A selected, focused or hovered idea always draws in full. */
|
|
16
|
+
export function ideaDetail(screenWidth: number, emphasised = false): IdeaDetail {
|
|
17
|
+
if (emphasised || screenWidth >= DETAIL.bodyOnlyBelow) return 'full';
|
|
18
|
+
return screenWidth < DETAIL.speckBelow ? 'speck' : 'body';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether idea shadows are drawn with this many ideas in view. */
|
|
22
|
+
export function shadowsShown(visibleIdeas: number): boolean { return visibleIdeas <= DETAIL.shadowsMaxVisible; }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/idea.ts — do not edit here.
|
|
2
|
+
/* An idea's size and its label. An idea grows to show its whole label: it starts at its base width and widens in
|
|
3
|
+
steps until the label fits in `labelLines` lines; at its widest it grows taller instead, so a label is never cut.
|
|
4
|
+
Labels (and connector labels) grow a little when the map is zoomed out, so they stay readable. Every renderer sizes
|
|
5
|
+
ideas with these rules, measuring text with its own font engine (`measure`). No dependencies, so it can travel alone. */
|
|
6
|
+
|
|
7
|
+
export type IdeaKind = 'root' | 'node' | 'mini';
|
|
8
|
+
|
|
9
|
+
/** How much labels grow when zoomed out (`scale` = the map's zoom): never smaller than their size, up to these caps.
|
|
10
|
+
* Idea labels and container names use `idea`; connector labels `connector`. Below 100% they grow; above, they don't. */
|
|
11
|
+
export const LABEL_GROWTH = { idea: 1.3, connector: 1.6 } as const;
|
|
12
|
+
|
|
13
|
+
export const IDEA = {
|
|
14
|
+
/** Width an idea starts at… */ baseWidth: { root: 240, node: 200, mini: 120 },
|
|
15
|
+
/** …and the widest it gets before it grows taller instead. */ maxWidth: { root: 400, node: 360, mini: 200 },
|
|
16
|
+
/** Width grows in steps of this. */ step: 20,
|
|
17
|
+
/** Height = width × aspect (at least `minRy` × 2), more if the label needs it. */ aspect: 0.74, minRy: 64,
|
|
18
|
+
/** The label's box is the idea's width less this (22 each side). */ labelInset: 44,
|
|
19
|
+
/** The idea widens until its label fits in this many lines. */ labelLines: 3,
|
|
20
|
+
/** Label size (before the label scale and zoom growth), weight from the canvas type styles. */ labelSize: { root: 30, node: 22, mini: 15 },
|
|
21
|
+
labelLineHeight: 1.15,
|
|
22
|
+
/** The label may fill at most this share of the idea's height. */ labelFillHeight: 0.7,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
/** Greedy word wrap of `text` into lines no wider than `maxWidth` (a single word longer than that gets its own line). */
|
|
26
|
+
export function wrapLines(text: string, maxWidth: number, measure: (s: string) => number): string[] {
|
|
27
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
let cur = '';
|
|
30
|
+
for (const w of words) {
|
|
31
|
+
const next = cur ? `${cur} ${w}` : w;
|
|
32
|
+
if (cur && measure(next) > maxWidth) { lines.push(cur); cur = w; } else cur = next;
|
|
33
|
+
}
|
|
34
|
+
if (cur) lines.push(cur);
|
|
35
|
+
return lines.length ? lines : [''];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** An idea's size for its label: width, half-width / half-height for the blob, and the label's lines. The label is
|
|
39
|
+
* measured at its largest drawn size — its type size × `labelScale` (Comfort: larger text) × LABEL_GROWTH.idea (zoomed
|
|
40
|
+
* out) — so it fits at every zoom and the idea never changes size while zooming. `measure(text, px)` returns the
|
|
41
|
+
* text's width in px in the label's face at `px`. */
|
|
42
|
+
export function ideaSize(kind: IdeaKind, label: string, measure: (text: string, px: number) => number, labelScale = 1) {
|
|
43
|
+
const px = IDEA.labelSize[kind] * labelScale * LABEL_GROWTH.idea;
|
|
44
|
+
const fit = (w: number) => wrapLines(label, w - IDEA.labelInset, (t) => measure(t, px));
|
|
45
|
+
const base: number = IDEA.baseWidth[kind], max: number = IDEA.maxWidth[kind];
|
|
46
|
+
let width = base, lines = fit(width);
|
|
47
|
+
while (lines.length > IDEA.labelLines && width < max) { width = Math.min(max, width + IDEA.step); lines = fit(width); }
|
|
48
|
+
const rx = width / 2;
|
|
49
|
+
const ry = Math.max(IDEA.minRy, rx * IDEA.aspect, (lines.length * px * IDEA.labelLineHeight) / 2 / IDEA.labelFillHeight);
|
|
50
|
+
return { width, height: ry * 2, rx, ry, lines };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function labelGrowth(scale: number): { idea: number; connector: number } {
|
|
54
|
+
const inv = 1 / Math.max(scale, 0.0001);
|
|
55
|
+
return { idea: Math.max(1, Math.min(inv, LABEL_GROWTH.idea)), connector: Math.max(1, Math.min(inv, LABEL_GROWTH.connector)) };
|
|
56
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/interaction.ts — do not edit here.
|
|
2
|
+
/* How the canvas answers the pointer and the keyboard — the timing and rules behind the peek (an idea's summary)
|
|
3
|
+
and terms (semantic anchors: an underlined word and its definition card). The look and the pop-in animation live
|
|
4
|
+
in the CSS (.vi-fact, .vi-anchor, .vi-anchor-card); these are the behaviours, as data, so every renderer (VisualMap,
|
|
5
|
+
the spec SDK on a canvas) opens and closes them alike. Times in ms. No dependencies, so it can travel alone. */
|
|
6
|
+
|
|
7
|
+
/** Terms (semantic anchors) in a peek's text. */
|
|
8
|
+
export const TERM = {
|
|
9
|
+
/** Hovering the term this long opens its definition card… */ hoverOpen: 250,
|
|
10
|
+
/** …which closes this long after the pointer leaves both the term and the card (moving between them keeps it open). */ hoverClose: 300,
|
|
11
|
+
/** Only a mouse opens on hover; touch and pen open on tap. */ hoverPointers: ['mouse'] as readonly string[],
|
|
12
|
+
/** A click, tap, Enter or Space pins the card open (hovering away doesn't close it); pressing again unpins and closes. */ clickPins: true,
|
|
13
|
+
/** Escape closes the card and returns focus to the term; a press anywhere outside the term and its card closes it. */ escapeCloses: true, pressOutsideCloses: true,
|
|
14
|
+
/** Opening by click or keyboard moves focus to the card's first link or button. */ focusCardOnOpen: true,
|
|
15
|
+
/** The card is kept this far inside the viewport's left and right edges. */ viewportMargin: 8,
|
|
16
|
+
/** The card's link to a source about the term. */ learnMoreLabel: 'Learn more',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/** The peek: an idea's summary card on hover (pointer) or tap (touch). */
|
|
20
|
+
export const PEEK = {
|
|
21
|
+
/** Hovering an idea opens its peek at once; it stays this long after the pointer leaves the idea (and while the
|
|
22
|
+
* pointer is over the peek itself), so the pointer can travel from the idea onto the card. */ hoverClose: 180,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
/** The touch peek: a bottom sheet with a grip. */
|
|
26
|
+
export const SHEET = {
|
|
27
|
+
/** Dragging the grip down this far closes the sheet (or collapses it when expanded); up this far expands it. */ dismissDrag: 80, expandDrag: 40,
|
|
28
|
+
/** Its height: this share of the map, at most `maxHeight` px; the map pans the chosen idea into the space above it. */ heightRatio: 0.42, maxHeight: 300,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/** Zoom, pan and fit. */
|
|
32
|
+
export const VIEW = {
|
|
33
|
+
/** Each zoom step multiplies or divides the zoom by this. */ zoomStep: 1.2,
|
|
34
|
+
/** Zoom limits (the zoom-min / zoom-max tokens; the build checks they agree). */ zoomMin: 0.3, zoomMax: 5,
|
|
35
|
+
/** Fit to view never enlarges a layer beyond this. */ fitMax: 1.1,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/** Touch gestures on the map (any touch input — phones, tablets and touchscreen laptops alike; it follows the pointer
|
|
39
|
+
* in use, not the device, so a mouse or trackpad on the same laptop isn't affected). */
|
|
40
|
+
export const GESTURE = {
|
|
41
|
+
/** Two fingers pinch to zoom the map's contents — ideas, connectors, containers and their labels — around the
|
|
42
|
+
* point between the fingers, within VIEW's limits. Controls, the depth trail, the peek, chat and the page never scale. */ pinchZoom: true,
|
|
43
|
+
/** Moving both fingers pans the contents. */ twoFingerPan: true,
|
|
44
|
+
/** The pointers that pinch (PointerEvent.pointerType). */ pointers: ['touch'] as readonly string[],
|
|
45
|
+
/** Where the map is the page (an app: AppShell, the mobile app), it takes every touch gesture (touch-action: none),
|
|
46
|
+
* so the browser never zooms the page. Embedded on a scrolling page (website, docs), one finger still scrolls the
|
|
47
|
+
* page (touch-action: pan-x pan-y); two fingers that land together pinch the map. */ appTouchAction: 'none', embeddedTouchAction: 'pan-x pan-y',
|
|
48
|
+
/** Zoom never navigates: stepping inside and backing out stay explicit (tap an idea / Step inside, the depth trail,
|
|
49
|
+
* Escape or Backspace). */ zoomNavigates: false,
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
/** The zoom after a pinch: the zoom at the start, scaled by how far the fingers spread, within VIEW's limits. */
|
|
53
|
+
export function pinchZoom(startZoom: number, startDistance: number, distance: number): number {
|
|
54
|
+
const z = startZoom * (distance / Math.max(startDistance, 1));
|
|
55
|
+
return Math.min(VIEW.zoomMax, Math.max(VIEW.zoomMin, z));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Keeps the map point that was under `from` under `to` when the scale changes from `s0` to `s1`: the new
|
|
59
|
+
* translation, given the old one (screen = world × s + t). Used to zoom around the fingers. */
|
|
60
|
+
export function zoomAround(t0: { x: number; y: number }, s0: number, s1: number, from: { x: number; y: number }, to: { x: number; y: number }): { x: number; y: number } {
|
|
61
|
+
const wx = (from.x - t0.x) / s0, wy = (from.y - t0.y) / s0;
|
|
62
|
+
return { x: to.x - wx * s1, y: to.y - wy * s1 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Framing a layer (fit to view): its ideas with room around them, and its container hulls with room for the name pill. */
|
|
66
|
+
export const FRAME = {
|
|
67
|
+
/** Room beyond each idea's outline (at least `minPadX` / `minPadY` from its centre). */ clearX: 50, clearY: 46, minPadX: 150, minPadY: 120,
|
|
68
|
+
/** Room beyond each container hull for its dashed line and the name pill straddling it. */ hullX: 24, hullY: 40,
|
|
69
|
+
} as const;
|
|
70
|
+
export type FrameBox = { x0: number; y0: number; x1: number; y1: number };
|
|
71
|
+
|
|
72
|
+
/** The box a layer is framed by: every idea (`rx` / `ry` its half size) and every container hull. */
|
|
73
|
+
export function layerBounds(ideas: ReadonlyArray<{ x: number; y: number; rx: number; ry: number }>, hulls: ReadonlyArray<FrameBox> = []): FrameBox {
|
|
74
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
75
|
+
for (const n of ideas) {
|
|
76
|
+
const px = Math.max(FRAME.minPadX, n.rx + FRAME.clearX), py = Math.max(FRAME.minPadY, n.ry + FRAME.clearY);
|
|
77
|
+
x0 = Math.min(x0, n.x - px); x1 = Math.max(x1, n.x + px); y0 = Math.min(y0, n.y - py); y1 = Math.max(y1, n.y + py);
|
|
78
|
+
}
|
|
79
|
+
for (const h of hulls) { x0 = Math.min(x0, h.x0 - FRAME.hullX); x1 = Math.max(x1, h.x1 + FRAME.hullX); y0 = Math.min(y0, h.y0 - FRAME.hullY); y1 = Math.max(y1, h.y1 + FRAME.hullY); }
|
|
80
|
+
if (!Number.isFinite(x0)) return { x0: -FRAME.minPadX, y0: -FRAME.minPadY, x1: FRAME.minPadX, y1: FRAME.minPadY };
|
|
81
|
+
return { x0, y0, x1, y1 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Fit to view: the scale that shows `bounds` in an area of `width` × `height` (never beyond VIEW.fitMax), and the
|
|
85
|
+
* world point to centre. */
|
|
86
|
+
export function fitView(bounds: FrameBox, width: number, height: number): { scale: number; center: { x: number; y: number } } {
|
|
87
|
+
const scale = Math.min(width / Math.max(1, bounds.x1 - bounds.x0), height / Math.max(1, bounds.y1 - bounds.y0), VIEW.fitMax);
|
|
88
|
+
return { scale, center: { x: (bounds.x0 + bounds.x1) / 2, y: (bounds.y0 + bounds.y1) / 2 } };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Where the peek card sits (pointer): beside the idea — right of it if it fits, else left — and level with it. */
|
|
92
|
+
export const PEEK_PLACEMENT = {
|
|
93
|
+
/** Gap between the idea's outermost ring and the card. */ gap: 16,
|
|
94
|
+
/** The card's width and the height kept clear below it. */ cardWidth: 300, cardHeight: 220,
|
|
95
|
+
/** The card is raised this much above the idea's centre. */ raise: 70,
|
|
96
|
+
/** Kept this far inside the map's edges. */ margin: 12,
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
/** The peek card's top-left, in map (screen) pixels. `idea`: the idea's centre on screen and its on-screen half
|
|
100
|
+
* width with its outermost ring (`half`); `box`: the map's size. */
|
|
101
|
+
export function peekPosition(idea: { x: number; y: number; half: number }, box: { w: number; h: number }, card: { w: number; h: number } = { w: PEEK_PLACEMENT.cardWidth, h: PEEK_PLACEMENT.cardHeight }) {
|
|
102
|
+
const P = PEEK_PLACEMENT, half = idea.half + P.gap;
|
|
103
|
+
const right = idea.x + half + card.w < box.w - P.margin;
|
|
104
|
+
return { left: right ? idea.x + half : Math.max(P.margin, idea.x - half - card.w), top: Math.min(Math.max(P.margin, idea.y - P.raise), box.h - card.h), side: right ? 'right' as const : 'left' as const };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The depth trail: each entry shows the idea that was stepped into — its title and a dot in its color (the same
|
|
108
|
+
* topic or custom color the idea is drawn in, including the sibling-order topic of an uncolored idea). The first
|
|
109
|
+
* entry is the map itself: a `stone` dot and the home glyph only (`rootLabel: 'icon'`) — no text, since a map's title
|
|
110
|
+
* usually repeats its centre idea's, which the next entry shows; its accessible name and tooltip are the map's title.
|
|
111
|
+
* The dot is the idea shape (shapeForLevel). */
|
|
112
|
+
export const TRAIL = { rootTopic: 'stone', rootLabel: 'icon', homeIconSize: 12, dotBox: 16, dotRadius: 6.4, stroke: 1.25, currentStroke: 1.75 } as const;
|
|
113
|
+
|
|
114
|
+
/** How an input answers: a mouse hovers (peeks and terms open on hover); touch and pen tap. Null for unknown pointers. */
|
|
115
|
+
export function pointerModeFor(pointerType: string): 'hover' | 'touch' | null {
|
|
116
|
+
return pointerType === 'mouse' ? 'hover' : pointerType === 'touch' || pointerType === 'pen' ? 'touch' : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Whether a pointer of this type opens a term on hover (PointerEvent.pointerType). */
|
|
120
|
+
export function hoverOpensTerm(pointerType: string): boolean {
|
|
121
|
+
return TERM.hoverPointers.includes(pointerType);
|
|
122
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Generated from visualli.ai design-system/components/src/motion.ts — do not edit here.
|
|
2
|
+
/* The canvas choreography — how a layer arrives, how you step inside and back out, how an idea answers hover.
|
|
3
|
+
One source for every renderer: VisualMap drives its CSS animations from these values (as CSS variables and
|
|
4
|
+
animation delays), and a canvas renderer (e.g. the spec SDK on Konva) can run the same timeline from its own loop.
|
|
5
|
+
Times in ms, scales as factors, easings as cubic-bezier control points. No dependencies, so it can travel alone.
|
|
6
|
+
The durations and easings equal the motion tokens (duration-*, ease-*, stagger-reveal); the build checks that. */
|
|
7
|
+
|
|
8
|
+
export type Bezier = readonly [number, number, number, number];
|
|
9
|
+
|
|
10
|
+
export const EASE = {
|
|
11
|
+
/** ease-standard — UI and connector drawing. */ standard: [0.4, 0, 0.2, 1] as Bezier,
|
|
12
|
+
/** ease-zoom — diving into and out of layers. */ zoom: [0.2, 0.8, 0.2, 1] as Bezier,
|
|
13
|
+
/** ease-bloom — ideas arriving, with a little overshoot. */ bloom: [0.34, 1.4, 0.64, 1] as Bezier,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const MOTION = {
|
|
17
|
+
/** A layer arrives: ideas bloom one by one in sibling order, center first, then connectors draw. */
|
|
18
|
+
reveal: {
|
|
19
|
+
/** duration-reveal: each idea's bloom. */ duration: 420,
|
|
20
|
+
/** stagger-reveal: delay between consecutive ideas. */ stagger: 70,
|
|
21
|
+
/** An idea starts at this scale… */ fromScale: 0.55,
|
|
22
|
+
/** …and this fraction of the way from its place toward the layer's center. */ fromCenter: 0.5,
|
|
23
|
+
ease: EASE.bloom,
|
|
24
|
+
/** Settle time after the last bloom before the map is idle (interactive again). */ tail: 200,
|
|
25
|
+
},
|
|
26
|
+
/** Connectors of an arriving layer: they start once the ideas are mostly in. */
|
|
27
|
+
connector: {
|
|
28
|
+
/** Lead time before the first connector, on top of the ideas' stagger. */ lead: 240,
|
|
29
|
+
/** Delay between consecutive connectors. */ stagger: 60,
|
|
30
|
+
/** A solid connector draws from source to target; a dashed one fades in instead. */ draw: 500,
|
|
31
|
+
/** The connector group's fade (duration-base). */ fade: 240,
|
|
32
|
+
ease: EASE.standard,
|
|
33
|
+
},
|
|
34
|
+
/** Step inside: the current layer zooms toward the idea and fades; the inner layer then arrives (reveal). */
|
|
35
|
+
dive: {
|
|
36
|
+
/** The old layer's zoom, toward the idea's position. */ scale: 3.2,
|
|
37
|
+
/** duration-zoom. */ duration: 500,
|
|
38
|
+
/** It fades out after this delay, over `fade`. */ fadeDelay: 150, fade: 160,
|
|
39
|
+
/** The layers swap (and the reveal starts) this long after the dive starts. */ swapAfter: 320,
|
|
40
|
+
ease: EASE.zoom,
|
|
41
|
+
},
|
|
42
|
+
/** Back out (depth trail): the current layer shrinks toward the center and fades; the outer layer arrives. */
|
|
43
|
+
surface: {
|
|
44
|
+
scale: 0.6, duration: 220, fade: 200,
|
|
45
|
+
swapAfter: 220,
|
|
46
|
+
ease: EASE.zoom,
|
|
47
|
+
},
|
|
48
|
+
/** The arriving layer as a whole settles from slightly small. */
|
|
49
|
+
settle: { fromScale: 0.94, duration: 260, ease: EASE.standard },
|
|
50
|
+
/** Hover (pointer) or select: the idea lifts; its rings turn and grow a touch — a nudge, not a wobble. */
|
|
51
|
+
hover: {
|
|
52
|
+
lift: 2, ringRotate: -2, ringScale: 1.015,
|
|
53
|
+
/** duration-base, for both the lift (ease-bloom) and the rings (ease-standard). */ duration: 240,
|
|
54
|
+
liftEase: EASE.bloom, ringEase: EASE.standard,
|
|
55
|
+
},
|
|
56
|
+
/** Reduced motion, or Gradual reveal off: everything appears together with a short fade; dives become cross-fades. */
|
|
57
|
+
reduced: { fade: 120 },
|
|
58
|
+
/** Large layers. The stagger is written for a dozen ideas; a layer of hundreds would take minutes to arrive and
|
|
59
|
+
* every frame of a bloom repaints the whole layer. Above `staggerSlots` ideas (and connectors), they share that many
|
|
60
|
+
* slots, so a layer never takes longer to arrive than `staggerSlots` ideas would. Above `bloomMax` ideas the layer
|
|
61
|
+
* fades in together (the reduced fade) and hover lifts snap instead of easing. */
|
|
62
|
+
largeLayer: { staggerSlots: 30, bloomMax: 150 },
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
export type Point = { x: number; y: number };
|
|
66
|
+
|
|
67
|
+
/** Delay before idea `i` (sibling order) starts to bloom. */
|
|
68
|
+
export function revealDelay(i: number): number { return i * MOTION.reveal.stagger; }
|
|
69
|
+
|
|
70
|
+
/** Delay before connector `i` starts, in a layer of `nodeCount` ideas. */
|
|
71
|
+
export function connectorDelay(i: number, nodeCount: number): number {
|
|
72
|
+
return MOTION.connector.lead + nodeCount * MOTION.reveal.stagger + i * MOTION.connector.stagger;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** How long a layer's arrival takes, until the map is idle again. 0 with reduced motion. */
|
|
76
|
+
export function revealTotal(nodeCount: number, reduced = false): number {
|
|
77
|
+
return reduced ? 0 : MOTION.reveal.duration + nodeCount * MOTION.reveal.stagger + MOTION.reveal.tail;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Where an idea starts its bloom, as an offset from its final position: part of the way toward the layer's center. */
|
|
81
|
+
export function bloomOffset(node: Point, center: Point): Point {
|
|
82
|
+
const k = MOTION.reveal.fromCenter;
|
|
83
|
+
return { x: (center.x - node.x) * k, y: (center.y - node.y) * k };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** When the layers swap after Step inside ('in') or Back out ('out'). 0 with reduced motion (a cross-fade). */
|
|
87
|
+
export function swapDelay(dir: 'in' | 'out', reduced = false): number {
|
|
88
|
+
return reduced ? 0 : dir === 'in' ? MOTION.dive.swapAfter : MOTION.surface.swapAfter;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The stagger slot of item `i` of `count` (ideas or connectors): its own index up to `staggerSlots`, then shared. */
|
|
92
|
+
export function revealSlot(i: number, count: number): number {
|
|
93
|
+
const slots = Math.min(count, MOTION.largeLayer.staggerSlots);
|
|
94
|
+
return count > slots ? Math.floor((i * slots) / count) : i;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether a layer of `nodeCount` ideas blooms in one by one (else it fades in together and hover lifts snap). */
|
|
98
|
+
export function layerBlooms(nodeCount: number): boolean { return nodeCount <= MOTION.largeLayer.bloomMax; }
|
|
99
|
+
|
|
100
|
+
/** A layer's whole arrival, large layers included: when idea `i` and connector `i` start, and when the layer is idle. */
|
|
101
|
+
export function layerReveal(nodeCount: number, connectorCount: number, reduced = false) {
|
|
102
|
+
const instant = reduced || !layerBlooms(nodeCount);
|
|
103
|
+
const slots = Math.min(nodeCount, MOTION.largeLayer.staggerSlots);
|
|
104
|
+
const edgeSlots = Math.min(connectorCount, MOTION.largeLayer.staggerSlots);
|
|
105
|
+
return {
|
|
106
|
+
/** Fades in together (reduced motion, Gradual reveal off, or a layer past `bloomMax`). */ instant,
|
|
107
|
+
ideaDelay: (i: number) => (instant ? 0 : revealDelay(revealSlot(i, nodeCount))),
|
|
108
|
+
connectorDelay: (i: number) => (instant ? 0 : connectorDelay(revealSlot(i, connectorCount), slots)),
|
|
109
|
+
/** Until the map is idle again. */
|
|
110
|
+
total: instant ? (reduced ? 0 : MOTION.reduced.fade) : Math.max(revealTotal(slots), edgeSlots ? connectorDelay(edgeSlots - 1, slots) + MOTION.connector.draw : 0),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** CSS form of an easing. */
|
|
115
|
+
export function cssEase(e: Bezier): string { return `cubic-bezier(${e.join(', ')})`; }
|
|
116
|
+
|
|
117
|
+
/** Evaluate an easing at progress t (0–1), for renderers that animate in their own loop. */
|
|
118
|
+
export function ease(e: Bezier, t: number): number {
|
|
119
|
+
const [x1, y1, x2, y2] = e;
|
|
120
|
+
if (t <= 0) return 0; if (t >= 1) return 1;
|
|
121
|
+
const bx = (u: number) => 3 * x1 * u * (1 - u) ** 2 + 3 * x2 * u * u * (1 - u) + u ** 3;
|
|
122
|
+
const by = (u: number) => 3 * y1 * u * (1 - u) ** 2 + 3 * y2 * u * u * (1 - u) + u ** 3;
|
|
123
|
+
let lo = 0, hi = 1, u = t;
|
|
124
|
+
for (let k = 0; k < 24; k++) { u = (lo + hi) / 2; if (bx(u) < t) lo = u; else hi = u; }
|
|
125
|
+
return by(u);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The CSS variables VisualMap sets on its root, so its stylesheet animates with exactly these values. */
|
|
129
|
+
export function motionVars(): Record<string, string> {
|
|
130
|
+
const m = MOTION;
|
|
131
|
+
return {
|
|
132
|
+
'--vi-bloom-from': String(m.reveal.fromScale),
|
|
133
|
+
'--vi-line-draw': `${m.connector.draw}ms`,
|
|
134
|
+
'--vi-dive-scale': String(m.dive.scale), '--vi-dive-fade': `${m.dive.fade}ms`, '--vi-dive-fade-delay': `${m.dive.fadeDelay}ms`,
|
|
135
|
+
'--vi-surface-scale': String(m.surface.scale), '--vi-surface-dur': `${m.surface.duration}ms`, '--vi-surface-fade': `${m.surface.fade}ms`,
|
|
136
|
+
'--vi-settle-from': String(m.settle.fromScale), '--vi-settle-dur': `${m.settle.duration}ms`,
|
|
137
|
+
'--vi-reduced-fade': `${m.reduced.fade}ms`,
|
|
138
|
+
'--vi-lift': `${m.hover.lift}px`, '--vi-ring-turn': `${m.hover.ringRotate}deg`, '--vi-ring-grow': String(m.hover.ringScale),
|
|
139
|
+
};
|
|
140
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,9 +9,26 @@ export * from './parser/index.js';
|
|
|
9
9
|
export * from './constants/index.js';
|
|
10
10
|
export * from './viewport/index.js';
|
|
11
11
|
export * from './performance/index.js';
|
|
12
|
-
export * from './animations/index.js';
|
|
13
12
|
export * from './spatial/index.js';
|
|
14
13
|
export * from './stores/index.js';
|
|
15
14
|
export * from './utils/navigation.js';
|
|
16
15
|
export * from './rendering/culling.js';
|
|
17
16
|
export * from './config/index.js';
|
|
17
|
+
export * from './theme/index.js';
|
|
18
|
+
|
|
19
|
+
// The design system's choreography (layer reveal, step inside, back out, hover, reduced motion), verbatim.
|
|
20
|
+
export { MOTION, EASE, revealDelay, connectorDelay, revealTotal, bloomOffset, swapDelay, cssEase, ease as easeBezier, motionVars, revealSlot, layerBlooms, layerReveal } from './generated/geometry/motion.js';
|
|
21
|
+
export type { Bezier } from './generated/geometry/motion.js';
|
|
22
|
+
// Containers: the dashed hull around a group's ideas and where its name goes (a pill on the hull, placed clear of
|
|
23
|
+
// ideas, connectors and other names), verbatim from the design system's geometry/container.ts.
|
|
24
|
+
export { HULL, HULL_CLEAR, containerHull, labelCandidates, placeContainerLabel, connectorSamples } from './generated/geometry/container.js';
|
|
25
|
+
export type { Hull, Box as LabelBox, LabelPlacement, LabelSide, Pt } from './generated/geometry/container.js';
|
|
26
|
+
// How terms, the peek, the touch sheet, zoom, fit and the depth trail behave, and which colour an idea gets: the design
|
|
27
|
+
// system's geometry/interaction.ts and geometry/color.ts, verbatim. (Idea size and label growth, geometry/idea.ts, come
|
|
28
|
+
// through rendering/ideaSize.)
|
|
29
|
+
export { TERM, PEEK, SHEET, VIEW, GESTURE, FRAME, PEEK_PLACEMENT, TRAIL, pointerModeFor, hoverOpensTerm, pinchZoom, zoomAround, layerBounds, fitView, peekPosition } from './generated/geometry/interaction.js';
|
|
30
|
+
export type { FrameBox } from './generated/geometry/interaction.js';
|
|
31
|
+
// Level of detail: what an idea draws at the size it's seen, and when shadows are skipped (geometry/detail.ts, verbatim).
|
|
32
|
+
export { DETAIL, ideaDetail, shadowsShown } from './generated/geometry/detail.js';
|
|
33
|
+
export type { IdeaDetail } from './generated/geometry/detail.js';
|
|
34
|
+
export { TOPIC_ORDER, CUSTOM, topicFor } from './generated/geometry/color.js';
|