symbiote-ui 0.3.0-alpha.52 → 0.3.0-alpha.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/canvas/CanvasGraph/CanvasGraph.js +39 -40
- package/canvas/graph-explorer.js +63 -0
- package/canvas/index.js +4 -0
- package/chat/presenter-cursor.js +96 -1
- package/custom-elements.json +36 -4
- package/docs/runtime-ui-construction.md +5 -0
- package/effects/CellBg/CellBg.js +202 -21
- package/effects/CellBg/cell-bg-render-state.js +155 -0
- package/index.js +4 -0
- package/manifest/component-registry.js +10 -2
- package/node/GraphNode/GraphNode.css.js +8 -0
- package/node/GraphNode/GraphNode.js +9 -0
- package/package.json +1 -1
- package/ui/index.js +4 -0
- package/webmcp.js +37 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `symbiote-ui` will be documented in this file.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Unreleased
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
+
- Added public `CanvasGraph.getViewport()` and `setViewport()` camera contracts
|
|
10
|
+
for clamped absolute live and deterministic viewport projection.
|
|
11
|
+
- Added ordered v3 interaction-cue planning through
|
|
12
|
+
`createWebMcpTourTurnActionPlans`; presentation tools now consume cue bindings
|
|
13
|
+
directly and preserve strict tool input schemas.
|
|
9
14
|
- Added bounded, cancellation-aware Media Studio frame-sequence playback,
|
|
10
15
|
caption overlays, and external-clock timeline controls for synchronized
|
|
11
16
|
audio, seek, pause, and resume workflows.
|
|
@@ -35,6 +40,9 @@ All notable changes to `symbiote-ui` will be documented in this file.
|
|
|
35
40
|
`{ steps: [{ target, holdMs?, gesture?, label? }] }` scenarios with a host
|
|
36
41
|
`resolveTarget`, per-step `onStep`, configurable `defaultHoldMs`, and
|
|
37
42
|
`AbortSignal` support.
|
|
43
|
+
- Added synchronous, seed-driven `presentAnnotationFrame` projection to the
|
|
44
|
+
presenter cursor so offline render workers can reproduce all marker and symbol
|
|
45
|
+
gestures without wall-clock or prior-frame state.
|
|
38
46
|
- Added `actions` and `embed` custom-content message parts to `ChatMessageItem`
|
|
39
47
|
and the message model: an `actions` part
|
|
40
48
|
(`{ type: 'actions', actions: [{ id, label, icon, variant }] }`) renders inline
|
|
@@ -46,6 +54,8 @@ All notable changes to `symbiote-ui` will be documented in this file.
|
|
|
46
54
|
|
|
47
55
|
### Fixed
|
|
48
56
|
|
|
57
|
+
- `registerWebMcpTool()` now forwards native registration options, including
|
|
58
|
+
`AbortSignal` and origin exposure, to `document.modelContext.registerTool()`.
|
|
49
59
|
- Fixed cascade theme widget/editor slider rows staying stale or failing to
|
|
50
60
|
apply range input after switching theme variants, and tuned the `classic`
|
|
51
61
|
preset to sharp, no-outline chrome while keeping its lightness and chroma
|
|
@@ -69,9 +69,6 @@ const DEFAULT_MENU_ITEMS = Object.freeze([
|
|
|
69
69
|
|
|
70
70
|
const INCREMENTAL_LAYOUT_INITIAL_ALPHA = 0.045;
|
|
71
71
|
const SEEDED_LAYOUT_INITIAL_ALPHA = 0.22;
|
|
72
|
-
const VISUAL_FOCUS_LAYOUT_INITIAL_ALPHA = 0.18;
|
|
73
|
-
const CRYSTAL_FOCUS_LAYOUT_INITIAL_ALPHA = 0.07;
|
|
74
|
-
const VISUAL_FOCUS_LAYOUT_PADDING = 8;
|
|
75
72
|
const NODE_APPEARANCE_START_SCALE = 0.2;
|
|
76
73
|
const ENTERING_LAYOUT_SIZE_SCALE = 0.18;
|
|
77
74
|
const ENTERING_LAYOUT_SIZE_WARMUP_TICKS = 72;
|
|
@@ -674,7 +671,6 @@ export class CanvasGraph extends Symbiote {
|
|
|
674
671
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
675
672
|
if (oldValue === newValue) return;
|
|
676
673
|
if (name === 'active-node-scale' || name === 'info-panel-scale') {
|
|
677
|
-
this._restartWorkerForVisualFocus();
|
|
678
674
|
this.needsDraw = true;
|
|
679
675
|
this._wakeLoop?.();
|
|
680
676
|
return;
|
|
@@ -1208,6 +1204,37 @@ export class CanvasGraph extends Symbiote {
|
|
|
1208
1204
|
return true;
|
|
1209
1205
|
}
|
|
1210
1206
|
|
|
1207
|
+
getViewport() {
|
|
1208
|
+
return {
|
|
1209
|
+
zoom: this.zoom,
|
|
1210
|
+
panX: this.panX,
|
|
1211
|
+
panY: this.panY,
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
setViewport(viewport = {}) {
|
|
1216
|
+
if (!Number.isFinite(viewport.zoom)
|
|
1217
|
+
|| !Number.isFinite(viewport.panX)
|
|
1218
|
+
|| !Number.isFinite(viewport.panY)) {
|
|
1219
|
+
throw new TypeError('canvas-graph viewport requires finite zoom, panX, and panY');
|
|
1220
|
+
}
|
|
1221
|
+
let rect = this.canvas?.getBoundingClientRect?.();
|
|
1222
|
+
let target = {
|
|
1223
|
+
zoom: this._clampZoom(viewport.zoom, rect),
|
|
1224
|
+
panX: viewport.panX,
|
|
1225
|
+
panY: viewport.panY,
|
|
1226
|
+
animate: viewport.animate === true,
|
|
1227
|
+
viewportEase: viewport.viewportEase,
|
|
1228
|
+
};
|
|
1229
|
+
let applied = target.animate
|
|
1230
|
+
? this._applyViewportTarget(target)
|
|
1231
|
+
: this._setViewportImmediate(target);
|
|
1232
|
+
if (!applied) return false;
|
|
1233
|
+
this.needsDraw = true;
|
|
1234
|
+
this._wakeLoop();
|
|
1235
|
+
return this.getViewport();
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1211
1238
|
_captureViewportState() {
|
|
1212
1239
|
return {
|
|
1213
1240
|
zoom: this.zoom,
|
|
@@ -1366,7 +1393,10 @@ export class CanvasGraph extends Symbiote {
|
|
|
1366
1393
|
this._queueTransitionMarker(previousNode.id, node.id, options);
|
|
1367
1394
|
}
|
|
1368
1395
|
this.updateInteractionDepths();
|
|
1369
|
-
if (isNewActivation)
|
|
1396
|
+
if (isNewActivation) {
|
|
1397
|
+
this.needsDraw = true;
|
|
1398
|
+
this._wakeLoop();
|
|
1399
|
+
}
|
|
1370
1400
|
return true;
|
|
1371
1401
|
}
|
|
1372
1402
|
|
|
@@ -2075,7 +2105,6 @@ export class CanvasGraph extends Symbiote {
|
|
|
2075
2105
|
max: 4,
|
|
2076
2106
|
});
|
|
2077
2107
|
}
|
|
2078
|
-
this._restartWorkerForVisualFocus();
|
|
2079
2108
|
this.needsDraw = true;
|
|
2080
2109
|
this._wakeLoop();
|
|
2081
2110
|
}
|
|
@@ -2085,43 +2114,13 @@ export class CanvasGraph extends Symbiote {
|
|
|
2085
2114
|
let height = node?.h;
|
|
2086
2115
|
if (this.renderMode === 'dots') {
|
|
2087
2116
|
const conns = this.adjMap.get(node.id)?.size || 0;
|
|
2088
|
-
const
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const layoutRadius = radius + (isActiveLayoutNode ? VISUAL_FOCUS_LAYOUT_PADDING : 0);
|
|
2092
|
-
width = layoutRadius * 2;
|
|
2093
|
-
height = layoutRadius * 2;
|
|
2117
|
+
const radius = getNodeRadius(node, conns, { scale: 1 });
|
|
2118
|
+
width = radius * 2;
|
|
2119
|
+
height = radius * 2;
|
|
2094
2120
|
}
|
|
2095
2121
|
return { width, height };
|
|
2096
2122
|
}
|
|
2097
2123
|
|
|
2098
|
-
_getWorkerRestartOptions(options = {}) {
|
|
2099
|
-
const baseOptions = this._lastWorkerOptions && typeof this._lastWorkerOptions === 'object'
|
|
2100
|
-
? { ...this._lastWorkerOptions }
|
|
2101
|
-
: {};
|
|
2102
|
-
return {
|
|
2103
|
-
...baseOptions,
|
|
2104
|
-
activeGroupId: this.currentGroupId,
|
|
2105
|
-
boundaryRadius: this.currentGroupId ? this.graphDB.nodes.get(this.currentGroupId)?.w / 2 : null,
|
|
2106
|
-
attractors: baseOptions.attractors ?? null,
|
|
2107
|
-
...options,
|
|
2108
|
-
};
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
_restartWorkerForVisualFocus() {
|
|
2112
|
-
if (this._layoutSuspended || !this.worker || !this.nodes?.length || this.renderMode !== 'dots') return;
|
|
2113
|
-
const baseAlpha = Number.isFinite(this._lastWorkerOptions?.initialAlpha)
|
|
2114
|
-
? this._lastWorkerOptions.initialAlpha
|
|
2115
|
-
: 0;
|
|
2116
|
-
const focusAlpha = this._usesCrystalForceLayout()
|
|
2117
|
-
? CRYSTAL_FOCUS_LAYOUT_INITIAL_ALPHA
|
|
2118
|
-
: VISUAL_FOCUS_LAYOUT_INITIAL_ALPHA;
|
|
2119
|
-
this.startWorker(this._getWorkerRestartOptions({
|
|
2120
|
-
initialAlpha: Math.max(baseAlpha, focusAlpha),
|
|
2121
|
-
crystalReseed: this._usesCrystalForceLayout(),
|
|
2122
|
-
}));
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
2124
|
_queueNodeAppearances(nodeIds, options = {}) {
|
|
2126
2125
|
if (!Array.isArray(nodeIds) || nodeIds.length === 0) return;
|
|
2127
2126
|
let now = renderNow();
|
|
@@ -2451,7 +2450,7 @@ export class CanvasGraph extends Symbiote {
|
|
|
2451
2450
|
nodeHeight: this.renderMode === 'dots' ? DOT_RADIUS * 2 : 40,
|
|
2452
2451
|
mode: 'continuous',
|
|
2453
2452
|
positionOrigin: this.renderMode === 'dots' ? 'center' : 'top-left',
|
|
2454
|
-
activeVisualNodeId:
|
|
2453
|
+
activeVisualNodeId: null,
|
|
2455
2454
|
...autoOptions,
|
|
2456
2455
|
...this._forceLayoutOverrides,
|
|
2457
2456
|
...(customOptions || {}),
|
package/canvas/graph-explorer.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
export const GRAPH_PATH_STYLES = ['pcb', 'bezier', 'orthogonal', 'straight'];
|
|
2
2
|
export const GRAPH_VIEW_MODES = ['structured', 'flat'];
|
|
3
|
+
export const GRAPH_PATH_STYLE_MENU_GROUP = Object.freeze({
|
|
4
|
+
group: 'path',
|
|
5
|
+
groupLabel: 'Connections',
|
|
6
|
+
groupOrder: 20,
|
|
7
|
+
});
|
|
8
|
+
export const GRAPH_PATH_STYLE_MENU_ITEMS = Object.freeze({
|
|
9
|
+
pcb: Object.freeze({ label: 'Routed', icon: 'route' }),
|
|
10
|
+
orthogonal: Object.freeze({ label: 'Right angles', icon: 'account_tree' }),
|
|
11
|
+
bezier: Object.freeze({ label: 'Curved', icon: 'gesture' }),
|
|
12
|
+
straight: Object.freeze({ label: 'Straight', icon: 'trending_flat' }),
|
|
13
|
+
});
|
|
3
14
|
|
|
4
15
|
export const GRAPH_DIRECTORY_FRAME_COLORS = [
|
|
5
16
|
'var(--sn-graph-cluster-0)',
|
|
@@ -22,6 +33,43 @@ export function normalizeGraphExplorerViewMode(mode) {
|
|
|
22
33
|
|
|
23
34
|
export const normalizeGraphViewMode = normalizeGraphExplorerViewMode;
|
|
24
35
|
|
|
36
|
+
function supportsGraphPathStyleMenuActions(mode) {
|
|
37
|
+
return mode === 'structured';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveGraphPathStyleAction(actionId) {
|
|
41
|
+
const id = String(actionId || '');
|
|
42
|
+
if (!id.startsWith('path:')) return '';
|
|
43
|
+
const style = id.slice('path:'.length);
|
|
44
|
+
return GRAPH_PATH_STYLES.includes(style) ? style : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createGraphPathStyleMenuActions({
|
|
48
|
+
mode = 'structured',
|
|
49
|
+
pathStyle = 'pcb',
|
|
50
|
+
labels = {},
|
|
51
|
+
titles = {},
|
|
52
|
+
group = GRAPH_PATH_STYLE_MENU_GROUP.group,
|
|
53
|
+
groupLabel = GRAPH_PATH_STYLE_MENU_GROUP.groupLabel,
|
|
54
|
+
groupOrder = GRAPH_PATH_STYLE_MENU_GROUP.groupOrder,
|
|
55
|
+
} = {}) {
|
|
56
|
+
if (!supportsGraphPathStyleMenuActions(mode)) return [];
|
|
57
|
+
return GRAPH_PATH_STYLES.map((style) => {
|
|
58
|
+
const item = GRAPH_PATH_STYLE_MENU_ITEMS[style] || {};
|
|
59
|
+
const label = labels[style] || item.label || style;
|
|
60
|
+
return {
|
|
61
|
+
id: `path:${style}`,
|
|
62
|
+
label,
|
|
63
|
+
icon: item.icon || 'route',
|
|
64
|
+
title: titles[style] || `Use ${label} connection paths`,
|
|
65
|
+
group,
|
|
66
|
+
groupLabel,
|
|
67
|
+
groupOrder,
|
|
68
|
+
active: style === pathStyle,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
25
73
|
function scheduleGraphExplorerFrame(callback) {
|
|
26
74
|
if (typeof requestAnimationFrame === 'function') {
|
|
27
75
|
requestAnimationFrame(callback);
|
|
@@ -346,6 +394,21 @@ export function createGraphExplorerViewController({
|
|
|
346
394
|
return api;
|
|
347
395
|
},
|
|
348
396
|
|
|
397
|
+
getPathStyleMenuActions(options = {}) {
|
|
398
|
+
return createGraphPathStyleMenuActions({
|
|
399
|
+
...options,
|
|
400
|
+
mode: state.mode,
|
|
401
|
+
pathStyle: state.pathStyle,
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
runPathStyleMenuAction(actionId) {
|
|
406
|
+
const style = resolveGraphPathStyleAction(actionId);
|
|
407
|
+
if (!style || state.mode !== 'structured') return false;
|
|
408
|
+
api.setPathStyle(style);
|
|
409
|
+
return true;
|
|
410
|
+
},
|
|
411
|
+
|
|
349
412
|
fitView({ structuredArgs = [], flatArgs = [] } = {}) {
|
|
350
413
|
if (state.mode === 'flat') {
|
|
351
414
|
state.flatGraph?.fitView?.(...flatArgs);
|
package/canvas/index.js
CHANGED
|
@@ -36,11 +36,14 @@ export {
|
|
|
36
36
|
} from './CanvasGraph/CanvasGraphViewport.js';
|
|
37
37
|
export {
|
|
38
38
|
GRAPH_DIRECTORY_FRAME_COLORS,
|
|
39
|
+
GRAPH_PATH_STYLE_MENU_GROUP,
|
|
40
|
+
GRAPH_PATH_STYLE_MENU_ITEMS,
|
|
39
41
|
GRAPH_PATH_STYLES,
|
|
40
42
|
GRAPH_VIEW_MODES,
|
|
41
43
|
addGraphDirectoryFrames,
|
|
42
44
|
applyGraphExplorerViewMode,
|
|
43
45
|
buildFlatPathHash,
|
|
46
|
+
createGraphPathStyleMenuActions,
|
|
44
47
|
createGraphExplorerViewController,
|
|
45
48
|
createGraphViewModeController,
|
|
46
49
|
getFileSelectionNodeId,
|
|
@@ -52,6 +55,7 @@ export {
|
|
|
52
55
|
normalizeGraphViewMode,
|
|
53
56
|
renderGraphPathStyleButton,
|
|
54
57
|
renderGraphViewModeButton,
|
|
58
|
+
resolveGraphPathStyleAction,
|
|
55
59
|
resolveFlatHashChange,
|
|
56
60
|
resolveInitialGraphViewMode,
|
|
57
61
|
selectGraphLabelMode,
|
package/chat/presenter-cursor.js
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* same deterministic move counter as the travel arc, so runs differ yet stay
|
|
18
18
|
* reproducible without `Math.random`. A faint, themed ink trail traces under the
|
|
19
19
|
* cursor so the flourish reads, then fades.
|
|
20
|
+
* `presentAnnotationFrame` projects those same paths synchronously from explicit
|
|
21
|
+
* progress and seed values. It uses no animation frame, timer, or prior cursor
|
|
22
|
+
* position, so offline render workers can reproduce any frame independently.
|
|
20
23
|
*
|
|
21
24
|
* `clickElement(el)` is the native-control gesture: the cursor travels to a real
|
|
22
25
|
* button/tab/control, pulses a click target ring, and then calls the element's
|
|
@@ -148,6 +151,7 @@ const TRAVEL_PX_PER_MS = 0.75; // longer hops take a little more time
|
|
|
148
151
|
const GESTURE_MS = 720; // base duration of a single gesture pass
|
|
149
152
|
const GESTURE_JITTER_PX = 2.4; // peak per-frame hand-tremor amplitude
|
|
150
153
|
const INK_FADE_MS = 520; // how long the ink trail lingers before it clears
|
|
154
|
+
const DETERMINISTIC_GESTURE_STEPS = 96;
|
|
151
155
|
|
|
152
156
|
const DEFAULT_HOLD_MS = 1200;
|
|
153
157
|
|
|
@@ -887,6 +891,9 @@ function inertCursor() {
|
|
|
887
891
|
} catch (_) {}
|
|
888
892
|
}
|
|
889
893
|
},
|
|
894
|
+
presentAnnotationFrame() {
|
|
895
|
+
return { presented: false, reason: 'unsupported' };
|
|
896
|
+
},
|
|
890
897
|
clear() {},
|
|
891
898
|
dispose() {},
|
|
892
899
|
isSupported() {
|
|
@@ -903,7 +910,7 @@ function inertCursor() {
|
|
|
903
910
|
* env this returns inert no-ops and `isSupported()` is false.
|
|
904
911
|
*
|
|
905
912
|
* @param {Document} [doc] - document to render into (defaults to the global one).
|
|
906
|
-
* @returns {{ moveTo: (el: Element, opts?: object) => void, markElement: (el: Element, opts?: object) => void, annotateElement: (el: Element, opts?: object) => void, clickElement: (el: Element, opts?: object) => void, clear: () => void, dispose: () => void, isSupported: () => boolean }}
|
|
913
|
+
* @returns {{ moveTo: (el: Element, opts?: object) => void, markElement: (el: Element, opts?: object) => void, annotateElement: (el: Element, opts?: object) => void, presentAnnotationFrame: (el: Element, annotation: object, frame: object) => object, clickElement: (el: Element, opts?: object) => void, clear: () => void, dispose: () => void, isSupported: () => boolean }}
|
|
907
914
|
*/
|
|
908
915
|
export function createPresenterCursor(doc = typeof document !== 'undefined' ? document : null) {
|
|
909
916
|
if (!hasDocument(doc)) return inertCursor();
|
|
@@ -1456,6 +1463,93 @@ export function createPresenterCursor(doc = typeof document !== 'undefined' ? do
|
|
|
1456
1463
|
}
|
|
1457
1464
|
}
|
|
1458
1465
|
|
|
1466
|
+
function presentAnnotationFrame(el, value, frame = {}) {
|
|
1467
|
+
if (disposed) return { presented: false, reason: 'disposed' };
|
|
1468
|
+
let annotation = normalizePresenterAnnotation(value);
|
|
1469
|
+
if (!annotation) {
|
|
1470
|
+
clear();
|
|
1471
|
+
return { presented: false, reason: 'invalid-annotation' };
|
|
1472
|
+
}
|
|
1473
|
+
if (!el || typeof el.getBoundingClientRect !== 'function') {
|
|
1474
|
+
clear();
|
|
1475
|
+
return { presented: false, reason: 'invalid-target' };
|
|
1476
|
+
}
|
|
1477
|
+
let viewport = {
|
|
1478
|
+
width: win?.innerWidth || doc.documentElement?.clientWidth || 0,
|
|
1479
|
+
height: win?.innerHeight || doc.documentElement?.clientHeight || 0,
|
|
1480
|
+
};
|
|
1481
|
+
let rect = resolvePresenterVisibleRect(el, viewport);
|
|
1482
|
+
if (!rect) {
|
|
1483
|
+
clear();
|
|
1484
|
+
return { presented: false, reason: 'hidden-target' };
|
|
1485
|
+
}
|
|
1486
|
+
let progress = Math.max(0, Math.min(1, Number(frame.progress) || 0));
|
|
1487
|
+
let seed = Number(frame.seed);
|
|
1488
|
+
if (!Number.isFinite(seed)) seed = 0;
|
|
1489
|
+
let drawRect = annotationRectFor(rect, viewport, annotation);
|
|
1490
|
+
let factory = annotation.kind === 'symbol'
|
|
1491
|
+
? SYMBOLS[annotation.symbol]
|
|
1492
|
+
: GESTURES[annotation.marker];
|
|
1493
|
+
let options = annotation.kind === 'symbol' ? { placement: annotation.placement } : {};
|
|
1494
|
+
let plan = factory?.(drawRect, seed, options);
|
|
1495
|
+
if (!plan || typeof plan.point !== 'function') {
|
|
1496
|
+
clear();
|
|
1497
|
+
return { presented: false, reason: 'unsupported-annotation' };
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
cancelTravel();
|
|
1501
|
+
cancelDrag();
|
|
1502
|
+
cancelGesture();
|
|
1503
|
+
cancelClick();
|
|
1504
|
+
overlay.classList.remove('is-paused');
|
|
1505
|
+
overlay.classList.add('is-visible');
|
|
1506
|
+
hideMarqueeFrame();
|
|
1507
|
+
|
|
1508
|
+
let pointCount = progress > 0
|
|
1509
|
+
? Math.max(2, Math.floor(progress * DETERMINISTIC_GESTURE_STEPS) + 1)
|
|
1510
|
+
: 0;
|
|
1511
|
+
let points = [];
|
|
1512
|
+
let amp = GESTURE_JITTER_PX * (0.85 + (variation(seed, 31) * 0.5 + 0.5) * 0.4);
|
|
1513
|
+
for (let index = 0; index < pointCount; index += 1) {
|
|
1514
|
+
let t = Math.min(1, index / DETERMINISTIC_GESTURE_STEPS);
|
|
1515
|
+
let eased = easeInOutCubic(t);
|
|
1516
|
+
let ideal = plan.point(eased);
|
|
1517
|
+
let fade = 1 - eased * eased;
|
|
1518
|
+
points.push({
|
|
1519
|
+
x: ideal.x + jitter(seed, eased, amp, 0) * fade,
|
|
1520
|
+
y: ideal.y + jitter(seed, eased, amp, 1) * fade,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
let path = points.length >= 2
|
|
1524
|
+
? points.reduce((d, point, index) => `${d}${index ? 'L' : 'M'}${point.x.toFixed(1)} ${point.y.toFixed(1)}`, '')
|
|
1525
|
+
: '';
|
|
1526
|
+
inkPath.setAttribute('d', path);
|
|
1527
|
+
ink.classList.toggle('is-inking', Boolean(path));
|
|
1528
|
+
let startIdeal = plan.point(0);
|
|
1529
|
+
let startPoint = {
|
|
1530
|
+
x: startIdeal.x + jitter(seed, 0, amp, 0),
|
|
1531
|
+
y: startIdeal.y + jitter(seed, 0, amp, 1),
|
|
1532
|
+
};
|
|
1533
|
+
let cursorPoint = progress >= 1 ? (plan.rest || plan.point(1)) : (points.at(-1) || startPoint);
|
|
1534
|
+
setCursor(cursorPoint.x, cursorPoint.y);
|
|
1535
|
+
let pathDigest = 2166136261;
|
|
1536
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
1537
|
+
pathDigest ^= path.charCodeAt(index);
|
|
1538
|
+
pathDigest = Math.imul(pathDigest, 16777619);
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
presented: true,
|
|
1542
|
+
kind: annotation.kind,
|
|
1543
|
+
name: annotation.marker || annotation.symbol,
|
|
1544
|
+
progress,
|
|
1545
|
+
seed,
|
|
1546
|
+
rect: drawRect,
|
|
1547
|
+
cursor: cursorPoint,
|
|
1548
|
+
pathPoints: points.length,
|
|
1549
|
+
pathDigest: (pathDigest >>> 0).toString(16).padStart(8, '0'),
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1459
1553
|
function clear() {
|
|
1460
1554
|
if (disposed) return;
|
|
1461
1555
|
cancelTravel();
|
|
@@ -1483,6 +1577,7 @@ export function createPresenterCursor(doc = typeof document !== 'undefined' ? do
|
|
|
1483
1577
|
moveTo,
|
|
1484
1578
|
markElement,
|
|
1485
1579
|
annotateElement,
|
|
1580
|
+
presentAnnotationFrame,
|
|
1486
1581
|
clickElement,
|
|
1487
1582
|
clear,
|
|
1488
1583
|
dispose,
|
package/custom-elements.json
CHANGED
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
"name": "CanvasGraph",
|
|
235
235
|
"tagName": "canvas-graph",
|
|
236
236
|
"description": "Generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax.",
|
|
237
|
-
"componentDescription": "Generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax. Use this interactive visual graph surface when composing canvas UI. Capabilities: hierarchical-graph, overview-read-renderer, force-layout, semantic-clusters, focus-selection, layout-snapshot, device-orientation-parallax. Data ownership: host-owned model; component renders and emits intent events. WebMCP tools: canvas_graph_set_model, canvas_graph_focus_node, canvas_graph_focus_nodes, canvas_graph_set_path.",
|
|
237
|
+
"componentDescription": "Generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax. Use this interactive visual graph surface when composing canvas UI. Capabilities: hierarchical-graph, overview-read-renderer, force-layout, semantic-clusters, focus-selection, layout-snapshot, viewport-control, device-orientation-parallax. Data ownership: host-owned model; component renders and emits intent events. WebMCP tools: canvas_graph_set_model, canvas_graph_focus_node, canvas_graph_focus_nodes, canvas_graph_set_path.",
|
|
238
238
|
"attributes": [
|
|
239
239
|
{
|
|
240
240
|
"name": "active-node-scale",
|
|
@@ -376,6 +376,16 @@
|
|
|
376
376
|
"name": "fitView",
|
|
377
377
|
"description": "Fits the visible graph into the canvas viewport; accepts optional viewportEase for live replay tracking."
|
|
378
378
|
},
|
|
379
|
+
{
|
|
380
|
+
"kind": "method",
|
|
381
|
+
"name": "getViewport",
|
|
382
|
+
"description": "Returns the current zoom and pan viewport as a portable snapshot."
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"kind": "method",
|
|
386
|
+
"name": "setViewport",
|
|
387
|
+
"description": "Applies an absolute, clamped zoom and pan viewport immediately or through the graph camera easing loop."
|
|
388
|
+
},
|
|
379
389
|
{
|
|
380
390
|
"kind": "method",
|
|
381
391
|
"name": "setVisualOptions",
|
|
@@ -1049,7 +1059,7 @@
|
|
|
1049
1059
|
}
|
|
1050
1060
|
},
|
|
1051
1061
|
"agent": {
|
|
1052
|
-
"componentDescription": "Generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax. Use this interactive visual graph surface when composing canvas UI. Capabilities: hierarchical-graph, overview-read-renderer, force-layout, semantic-clusters, focus-selection, layout-snapshot, device-orientation-parallax. Data ownership: host-owned model; component renders and emits intent events. WebMCP tools: canvas_graph_set_model, canvas_graph_focus_node, canvas_graph_focus_nodes, canvas_graph_set_path.",
|
|
1062
|
+
"componentDescription": "Generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax. Use this interactive visual graph surface when composing canvas UI. Capabilities: hierarchical-graph, overview-read-renderer, force-layout, semantic-clusters, focus-selection, layout-snapshot, viewport-control, device-orientation-parallax. Data ownership: host-owned model; component renders and emits intent events. WebMCP tools: canvas_graph_set_model, canvas_graph_focus_node, canvas_graph_focus_nodes, canvas_graph_set_path.",
|
|
1053
1063
|
"semanticRole": "interactive visual graph surface",
|
|
1054
1064
|
"usage": "Render or compose <canvas-graph> when the host needs generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax.",
|
|
1055
1065
|
"dataOwnership": "host-owned model; component renders and emits intent events",
|
|
@@ -29623,7 +29633,7 @@
|
|
|
29623
29633
|
"name": "CellBg",
|
|
29624
29634
|
"tagName": "cell-bg",
|
|
29625
29635
|
"description": "Animated cellular automaton background effect.",
|
|
29626
|
-
"componentDescription": "Animated cellular automaton background effect. Use this effects UI component when composing effects UI. Capabilities: animated-background, canvas-effect, activity-trigger, smooth-start, smooth-stop, pulse, resize-aware, reduced-motion-friendly, layout-lifecycle. Data ownership: host-owned props and attributes; component owns presentation state. WebMCP tools: cell_bg_trigger, cell_bg_start, cell_bg_stop.",
|
|
29636
|
+
"componentDescription": "Animated cellular automaton background effect. Use this effects UI component when composing effects UI. Capabilities: animated-background, canvas-effect, activity-trigger, smooth-start, smooth-stop, pulse, resize-aware, reduced-motion-friendly, layout-lifecycle, deterministic-render. Data ownership: host-owned props and attributes; component owns presentation state. WebMCP tools: cell_bg_trigger, cell_bg_start, cell_bg_stop.",
|
|
29627
29637
|
"attributes": [
|
|
29628
29638
|
{
|
|
29629
29639
|
"name": "active",
|
|
@@ -29645,6 +29655,13 @@
|
|
|
29645
29655
|
"text": "number"
|
|
29646
29656
|
},
|
|
29647
29657
|
"description": "Default timed pulse duration in milliseconds."
|
|
29658
|
+
},
|
|
29659
|
+
{
|
|
29660
|
+
"name": "seed",
|
|
29661
|
+
"type": {
|
|
29662
|
+
"text": "string"
|
|
29663
|
+
},
|
|
29664
|
+
"description": "Stable seed used by deterministic external-frame rendering."
|
|
29648
29665
|
}
|
|
29649
29666
|
],
|
|
29650
29667
|
"members": [
|
|
@@ -29716,6 +29733,21 @@
|
|
|
29716
29733
|
"kind": "method",
|
|
29717
29734
|
"name": "resumeLayout",
|
|
29718
29735
|
"description": "Restores persistent animation only when it was active before layout suspension."
|
|
29736
|
+
},
|
|
29737
|
+
{
|
|
29738
|
+
"kind": "method",
|
|
29739
|
+
"name": "setFrameDriver",
|
|
29740
|
+
"description": "Selects self-driven live animation or externally driven deterministic rendering."
|
|
29741
|
+
},
|
|
29742
|
+
{
|
|
29743
|
+
"kind": "method",
|
|
29744
|
+
"name": "presentFrame",
|
|
29745
|
+
"description": "Synchronously paints the current shared render-clock time."
|
|
29746
|
+
},
|
|
29747
|
+
{
|
|
29748
|
+
"kind": "method",
|
|
29749
|
+
"name": "getFramePresentation",
|
|
29750
|
+
"description": "Returns deterministic frame metrics and the current grid hash."
|
|
29719
29751
|
}
|
|
29720
29752
|
],
|
|
29721
29753
|
"events": [
|
|
@@ -30084,7 +30116,7 @@
|
|
|
30084
30116
|
}
|
|
30085
30117
|
},
|
|
30086
30118
|
"agent": {
|
|
30087
|
-
"componentDescription": "Animated cellular automaton background effect. Use this effects UI component when composing effects UI. Capabilities: animated-background, canvas-effect, activity-trigger, smooth-start, smooth-stop, pulse, resize-aware, reduced-motion-friendly, layout-lifecycle. Data ownership: host-owned props and attributes; component owns presentation state. WebMCP tools: cell_bg_trigger, cell_bg_start, cell_bg_stop.",
|
|
30119
|
+
"componentDescription": "Animated cellular automaton background effect. Use this effects UI component when composing effects UI. Capabilities: animated-background, canvas-effect, activity-trigger, smooth-start, smooth-stop, pulse, resize-aware, reduced-motion-friendly, layout-lifecycle, deterministic-render. Data ownership: host-owned props and attributes; component owns presentation state. WebMCP tools: cell_bg_trigger, cell_bg_start, cell_bg_stop.",
|
|
30088
30120
|
"semanticRole": "effects UI component",
|
|
30089
30121
|
"usage": "Render or compose <cell-bg> when the host needs animated cellular automaton background effect.",
|
|
30090
30122
|
"dataOwnership": "host-owned props and attributes; component owns presentation state",
|
|
@@ -136,6 +136,11 @@ changing the host-owned graph model.
|
|
|
136
136
|
Use `fitNodes(ids, { viewportEase })` or `fitView({ viewportEase })` when a
|
|
137
137
|
live replay should track a changing area of interest with a slower camera path
|
|
138
138
|
than ordinary user-initiated focus.
|
|
139
|
+
Use `getViewport()` to capture the current `{ zoom, panX, panY }` camera and
|
|
140
|
+
`setViewport({ zoom, panX, panY, animate })` to restore or project an absolute
|
|
141
|
+
camera state. `setViewport()` clamps zoom through the graph's normal viewport
|
|
142
|
+
rules and cancels stale immediate-camera targets, so deterministic replay must
|
|
143
|
+
prefer it over writing graph fields directly.
|
|
139
144
|
During incremental model growth, `canvas-graph` seeds entering node positions
|
|
140
145
|
near linked visible nodes, starts them with a small effective collision size,
|
|
141
146
|
and restarts the force layout with low initial heat, so live process replay can
|
package/effects/CellBg/CellBg.js
CHANGED
|
@@ -2,6 +2,13 @@ import Symbiote from '@symbiotejs/symbiote';
|
|
|
2
2
|
import template from './CellBg.tpl.js';
|
|
3
3
|
import css from './CellBg.css.js';
|
|
4
4
|
import { CELL_BG_DEFAULTS, readCellBgTheme } from './cell-bg-theme.js';
|
|
5
|
+
import { renderNow } from '../../core/render-clock.js';
|
|
6
|
+
import {
|
|
7
|
+
cellBgRenderFrame,
|
|
8
|
+
cellBgRenderHash,
|
|
9
|
+
createCellBgRenderState,
|
|
10
|
+
seekCellBgRenderState,
|
|
11
|
+
} from './cell-bg-render-state.js';
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Cellular Automaton Background Component
|
|
@@ -17,13 +24,21 @@ import { CELL_BG_DEFAULTS, readCellBgTheme } from './cell-bg-theme.js';
|
|
|
17
24
|
const RULE_B = [3];
|
|
18
25
|
const RULE_S = [2, 3];
|
|
19
26
|
const PALETTE_SIZE = CELL_BG_DEFAULTS.paletteSize;
|
|
20
|
-
const now = () =>
|
|
27
|
+
const now = () => renderNow();
|
|
21
28
|
const requestFrame = (callback) => {
|
|
22
29
|
if (typeof globalThis.requestAnimationFrame === 'function') {
|
|
23
30
|
return globalThis.requestAnimationFrame(callback);
|
|
24
31
|
}
|
|
25
32
|
return setTimeout(() => callback(now()), 16);
|
|
26
33
|
};
|
|
34
|
+
const cancelFrame = (handle) => {
|
|
35
|
+
if (handle === null || handle === undefined) return;
|
|
36
|
+
if (typeof globalThis.cancelAnimationFrame === 'function') {
|
|
37
|
+
globalThis.cancelAnimationFrame(handle);
|
|
38
|
+
} else {
|
|
39
|
+
clearTimeout(handle);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
27
42
|
const getReducedMotionQuery = () => globalThis.matchMedia?.('(prefers-reduced-motion: reduce)') || null;
|
|
28
43
|
|
|
29
44
|
function normalizeCssColor(source, value) {
|
|
@@ -89,7 +104,7 @@ function readCssNumber(source, token, fallback) {
|
|
|
89
104
|
|
|
90
105
|
export class CellBg extends Symbiote {
|
|
91
106
|
static get observedAttributes() {
|
|
92
|
-
return ['active', 'auto-trigger', 'pulse-duration'];
|
|
107
|
+
return ['active', 'auto-trigger', 'pulse-duration', 'seed'];
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
init$ = {
|
|
@@ -112,6 +127,10 @@ export class CellBg extends Symbiote {
|
|
|
112
127
|
this._prefersReducedMotion = Boolean(this._motionQuery?.matches);
|
|
113
128
|
this._motionChangeHandler = (event) => {
|
|
114
129
|
this._prefersReducedMotion = Boolean(event.matches);
|
|
130
|
+
if (this._externalFrameDrive) {
|
|
131
|
+
this.presentFrame();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
115
134
|
if (this._prefersReducedMotion) {
|
|
116
135
|
if (this._pulseTimer) clearTimeout(this._pulseTimer);
|
|
117
136
|
this._pulseTimer = null;
|
|
@@ -137,6 +156,11 @@ export class CellBg extends Symbiote {
|
|
|
137
156
|
this.lastTime = now();
|
|
138
157
|
this.isAnimating = false;
|
|
139
158
|
this._stagnantCount = 0;
|
|
159
|
+
this._frameRequest = null;
|
|
160
|
+
this._externalFrameDrive = false;
|
|
161
|
+
this._externalRenderState = null;
|
|
162
|
+
this._externalRenderFrame = null;
|
|
163
|
+
this._externalRestoreState = null;
|
|
140
164
|
|
|
141
165
|
// We only redraw on rAF if running, or if a single frame is needed after resize
|
|
142
166
|
this.resize = this.resize.bind(this);
|
|
@@ -146,6 +170,7 @@ export class CellBg extends Symbiote {
|
|
|
146
170
|
// Debounce: resize canvas immediately (prevents flash), pulse only after settle
|
|
147
171
|
const onResize = () => {
|
|
148
172
|
this.resize();
|
|
173
|
+
if (this._externalFrameDrive) return;
|
|
149
174
|
if (this._resizeDebounce) clearTimeout(this._resizeDebounce);
|
|
150
175
|
this._resizeDebounce = setTimeout(() => {
|
|
151
176
|
this._resizeDebounce = null;
|
|
@@ -194,6 +219,9 @@ export class CellBg extends Symbiote {
|
|
|
194
219
|
this.$.autoTrigger = this._readAutoTriggerAttribute();
|
|
195
220
|
} else if (name === 'pulse-duration') {
|
|
196
221
|
this.$.pulseDuration = this._readPulseDurationAttribute();
|
|
222
|
+
} else if (name === 'seed' && this._externalFrameDrive) {
|
|
223
|
+
this._externalRenderState = null;
|
|
224
|
+
this.presentFrame();
|
|
197
225
|
}
|
|
198
226
|
}
|
|
199
227
|
|
|
@@ -204,6 +232,10 @@ export class CellBg extends Symbiote {
|
|
|
204
232
|
*/
|
|
205
233
|
toggle(state) {
|
|
206
234
|
let next = !!state;
|
|
235
|
+
if (this._externalFrameDrive) {
|
|
236
|
+
this._recordExternalPersistent(next);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
207
239
|
if (this.$.active !== next) {
|
|
208
240
|
this.$.active = next;
|
|
209
241
|
return;
|
|
@@ -211,7 +243,113 @@ export class CellBg extends Symbiote {
|
|
|
211
243
|
this._setPersistent(next);
|
|
212
244
|
}
|
|
213
245
|
|
|
246
|
+
setFrameDriver(mode = 'self') {
|
|
247
|
+
let nextMode = String(mode || 'self').trim().toLowerCase();
|
|
248
|
+
if (nextMode !== 'self' && nextMode !== 'external') {
|
|
249
|
+
throw new TypeError('CellBg frame driver must be self or external');
|
|
250
|
+
}
|
|
251
|
+
if (nextMode === 'external') {
|
|
252
|
+
if (!this._externalFrameDrive) {
|
|
253
|
+
this._externalRestoreState = {
|
|
254
|
+
toggled: Boolean(this._toggled),
|
|
255
|
+
active: Boolean(this.$?.active),
|
|
256
|
+
running: Boolean(this.running),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
this._externalFrameDrive = true;
|
|
260
|
+
this.running = false;
|
|
261
|
+
this.isAnimating = false;
|
|
262
|
+
this.currentSpeed = 0;
|
|
263
|
+
if (this._frameRequest !== null) cancelFrame(this._frameRequest);
|
|
264
|
+
this._frameRequest = null;
|
|
265
|
+
if (this._pulseTimer) clearTimeout(this._pulseTimer);
|
|
266
|
+
if (this._resizeDebounce) clearTimeout(this._resizeDebounce);
|
|
267
|
+
this._pulseTimer = null;
|
|
268
|
+
this._resizeDebounce = null;
|
|
269
|
+
this._externalRenderState = null;
|
|
270
|
+
this.resize();
|
|
271
|
+
return 'external';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!this._externalFrameDrive) return 'self';
|
|
275
|
+
let restore = this._externalRestoreState || {};
|
|
276
|
+
if (this._externalRenderFrame) {
|
|
277
|
+
this.grid = new Uint8Array(this._externalRenderFrame.grid);
|
|
278
|
+
this.radii = new Float32Array(this._externalRenderFrame.radii);
|
|
279
|
+
}
|
|
280
|
+
this._externalFrameDrive = false;
|
|
281
|
+
this._externalRenderState = null;
|
|
282
|
+
this._externalRenderFrame = null;
|
|
283
|
+
this._externalRestoreState = null;
|
|
284
|
+
this._toggled = Boolean(restore.toggled);
|
|
285
|
+
this.$.active = Boolean(restore.active);
|
|
286
|
+
this.running = false;
|
|
287
|
+
this.isAnimating = false;
|
|
288
|
+
this.currentSpeed = 0;
|
|
289
|
+
this.lastTime = now();
|
|
290
|
+
if (restore.running || restore.toggled || restore.active) this._start('frame-driver-restore');
|
|
291
|
+
else this._draw();
|
|
292
|
+
return 'self';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
presentFrame() {
|
|
296
|
+
if (!this._available) return false;
|
|
297
|
+
this.resize();
|
|
298
|
+
if (!this.canvas?._w || !this.canvas?._h) return false;
|
|
299
|
+
if (!this._externalFrameDrive) {
|
|
300
|
+
this._draw();
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
let options = this._externalRenderOptions();
|
|
304
|
+
this._externalRenderState = seekCellBgRenderState(this._externalRenderState, now(), options);
|
|
305
|
+
this._externalRenderFrame = cellBgRenderFrame(this._externalRenderState);
|
|
306
|
+
this._draw(this._externalRenderFrame);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
getFramePresentation() {
|
|
311
|
+
if (!this._externalRenderState || !this._externalRenderFrame) return null;
|
|
312
|
+
return {
|
|
313
|
+
seed: this._externalRenderState.seed,
|
|
314
|
+
width: this.canvas?._w || 0,
|
|
315
|
+
height: this.canvas?._h || 0,
|
|
316
|
+
cols: this._externalRenderState.cols,
|
|
317
|
+
rows: this._externalRenderState.rows,
|
|
318
|
+
cellSize: this._cellSize,
|
|
319
|
+
stepMs: this._externalRenderState.stepMs,
|
|
320
|
+
stepIndex: this._externalRenderState.stepIndex,
|
|
321
|
+
timeMs: this._externalRenderState.timeMs,
|
|
322
|
+
gridHash: cellBgRenderHash(this._externalRenderState, this._externalRenderFrame),
|
|
323
|
+
activity: 'continuous',
|
|
324
|
+
reducedMotion: false,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_externalRenderOptions() {
|
|
329
|
+
return {
|
|
330
|
+
seed: this.getAttribute('seed') || 'symbiote-cell-bg',
|
|
331
|
+
cols: this.cols,
|
|
332
|
+
rows: this.rows,
|
|
333
|
+
stepMs: this._stepMs,
|
|
334
|
+
minRadius: this._minRadius,
|
|
335
|
+
maxRadius: this._maxRadius,
|
|
336
|
+
fadeRate: this._fadeRate,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_recordExternalPersistent(state) {
|
|
341
|
+
if (!this._externalRestoreState) return;
|
|
342
|
+
let active = Boolean(state);
|
|
343
|
+
this._externalRestoreState.toggled = active;
|
|
344
|
+
this._externalRestoreState.active = active;
|
|
345
|
+
this._externalRestoreState.running = active;
|
|
346
|
+
}
|
|
347
|
+
|
|
214
348
|
_setPersistent(state) {
|
|
349
|
+
if (this._externalFrameDrive) {
|
|
350
|
+
this._recordExternalPersistent(state);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
215
353
|
this._toggled = !!state;
|
|
216
354
|
if (state && this._pulseTimer) {
|
|
217
355
|
clearTimeout(this._pulseTimer);
|
|
@@ -240,6 +378,10 @@ export class CellBg extends Symbiote {
|
|
|
240
378
|
* Stop persistent or timed animation with smooth deceleration.
|
|
241
379
|
*/
|
|
242
380
|
stop() {
|
|
381
|
+
if (this._externalFrameDrive) {
|
|
382
|
+
this._recordExternalPersistent(false);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
243
385
|
if (this._pulseTimer) clearTimeout(this._pulseTimer);
|
|
244
386
|
this._pulseTimer = null;
|
|
245
387
|
this._toggled = false;
|
|
@@ -252,6 +394,7 @@ export class CellBg extends Symbiote {
|
|
|
252
394
|
}
|
|
253
395
|
|
|
254
396
|
suspendLayout({ reason = 'layout-suspend' } = {}) {
|
|
397
|
+
if (this._externalFrameDrive) return;
|
|
255
398
|
this._resumePersistentAfterSuspend = Boolean(this._toggled || this.$.active);
|
|
256
399
|
if (this._pulseTimer) clearTimeout(this._pulseTimer);
|
|
257
400
|
this._pulseTimer = null;
|
|
@@ -276,6 +419,7 @@ export class CellBg extends Symbiote {
|
|
|
276
419
|
}
|
|
277
420
|
|
|
278
421
|
resumeLayout() {
|
|
422
|
+
if (this._externalFrameDrive) return;
|
|
279
423
|
if (this._resumePersistentAfterSuspend) this.start();
|
|
280
424
|
this._resumePersistentAfterSuspend = false;
|
|
281
425
|
}
|
|
@@ -287,6 +431,7 @@ export class CellBg extends Symbiote {
|
|
|
287
431
|
* @param {number} [duration=10000]
|
|
288
432
|
*/
|
|
289
433
|
pulse(duration = 10000) {
|
|
434
|
+
if (this._externalFrameDrive) return;
|
|
290
435
|
if (this._toggled) return; // Already running persistently
|
|
291
436
|
let safeDuration = this._normalizePulseDuration(duration);
|
|
292
437
|
if (this._prefersReducedMotion) {
|
|
@@ -321,6 +466,8 @@ export class CellBg extends Symbiote {
|
|
|
321
466
|
if (this._resizeDebounce) clearTimeout(this._resizeDebounce);
|
|
322
467
|
this._pulseTimer = null;
|
|
323
468
|
this._resizeDebounce = null;
|
|
469
|
+
if (this._frameRequest !== null) cancelFrame(this._frameRequest);
|
|
470
|
+
this._frameRequest = null;
|
|
324
471
|
this._themeObserver?.disconnect();
|
|
325
472
|
this.ownerDocument?.removeEventListener?.('cascade-theme-change', this._themeChangeHandler);
|
|
326
473
|
if (typeof this._motionQuery?.removeEventListener === 'function') {
|
|
@@ -336,13 +483,18 @@ export class CellBg extends Symbiote {
|
|
|
336
483
|
let previousCellSize = this._cellSize;
|
|
337
484
|
let previousMinRadius = this._minRadius;
|
|
338
485
|
let previousMaxRadius = this._maxRadius;
|
|
486
|
+
let previousStepMs = this._stepMs;
|
|
487
|
+
let previousFadeRate = this._fadeRate;
|
|
339
488
|
this._buildPalette();
|
|
340
489
|
|
|
341
490
|
let geometryChanged = previousCellSize !== this._cellSize
|
|
342
491
|
|| previousMinRadius !== this._minRadius
|
|
343
|
-
|| previousMaxRadius !== this._maxRadius
|
|
492
|
+
|| previousMaxRadius !== this._maxRadius
|
|
493
|
+
|| previousStepMs !== this._stepMs
|
|
494
|
+
|| previousFadeRate !== this._fadeRate;
|
|
344
495
|
if (geometryChanged) {
|
|
345
|
-
this.
|
|
496
|
+
if (this._externalFrameDrive) this._externalRenderState = null;
|
|
497
|
+
this.resize({ force: true });
|
|
346
498
|
} else {
|
|
347
499
|
this._draw();
|
|
348
500
|
}
|
|
@@ -350,6 +502,10 @@ export class CellBg extends Symbiote {
|
|
|
350
502
|
}
|
|
351
503
|
|
|
352
504
|
_scheduleThemeRefresh() {
|
|
505
|
+
if (this._externalFrameDrive) {
|
|
506
|
+
this.refreshTheme({ pulse: false });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
353
509
|
if (this._themeRefreshQueued) return;
|
|
354
510
|
this._themeRefreshQueued = true;
|
|
355
511
|
requestFrame(() => {
|
|
@@ -381,6 +537,7 @@ export class CellBg extends Symbiote {
|
|
|
381
537
|
}
|
|
382
538
|
|
|
383
539
|
_triggerIfEnabled() {
|
|
540
|
+
if (this._externalFrameDrive) return;
|
|
384
541
|
if (this.$.autoTrigger === false) return;
|
|
385
542
|
this.trigger(this.$.pulseDuration);
|
|
386
543
|
}
|
|
@@ -402,7 +559,7 @@ export class CellBg extends Symbiote {
|
|
|
402
559
|
return theme;
|
|
403
560
|
}
|
|
404
561
|
|
|
405
|
-
resize() {
|
|
562
|
+
resize({ force = false } = {}) {
|
|
406
563
|
if (!this._available || !this.canvas.parentElement) return;
|
|
407
564
|
let dpr = globalThis.devicePixelRatio || 1;
|
|
408
565
|
let w = this.canvas.parentElement.clientWidth;
|
|
@@ -410,6 +567,9 @@ export class CellBg extends Symbiote {
|
|
|
410
567
|
|
|
411
568
|
if (w === 0 || h === 0) return; // Hidden or not attached
|
|
412
569
|
|
|
570
|
+
let sameSize = this.canvas._w === w && this.canvas._h === h;
|
|
571
|
+
if (sameSize && !force && (!this._externalFrameDrive || this._externalRenderState)) return;
|
|
572
|
+
|
|
413
573
|
this.canvas.width = w * dpr;
|
|
414
574
|
this.canvas.height = h * dpr;
|
|
415
575
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -428,6 +588,16 @@ export class CellBg extends Symbiote {
|
|
|
428
588
|
this.cols = Math.ceil(w / this._cellSize) + 1;
|
|
429
589
|
this.rows = Math.ceil(h / this._cellSize) + 1;
|
|
430
590
|
|
|
591
|
+
if (this._externalFrameDrive) {
|
|
592
|
+
this.grid = new Uint8Array(this.cols * this.rows);
|
|
593
|
+
this.radii = new Float32Array(this.cols * this.rows);
|
|
594
|
+
this.radii.fill(this._minRadius);
|
|
595
|
+
this._externalRenderState = createCellBgRenderState(this._externalRenderOptions());
|
|
596
|
+
this._externalRenderFrame = cellBgRenderFrame(this._externalRenderState);
|
|
597
|
+
this._draw(this._externalRenderFrame);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
431
601
|
this.grid = new Uint8Array(this.cols * this.rows);
|
|
432
602
|
this.radii = new Float32Array(this.cols * this.rows);
|
|
433
603
|
this.radii.fill(this._minRadius);
|
|
@@ -460,6 +630,7 @@ export class CellBg extends Symbiote {
|
|
|
460
630
|
}
|
|
461
631
|
|
|
462
632
|
_start(reason = 'manual') {
|
|
633
|
+
if (this._externalFrameDrive) return;
|
|
463
634
|
if (!this._available) return;
|
|
464
635
|
if (this.running) return;
|
|
465
636
|
this.running = true;
|
|
@@ -471,11 +642,12 @@ export class CellBg extends Symbiote {
|
|
|
471
642
|
if (!this.isAnimating) {
|
|
472
643
|
this.lastTime = now();
|
|
473
644
|
this.isAnimating = true;
|
|
474
|
-
|
|
645
|
+
this._scheduleFrame();
|
|
475
646
|
}
|
|
476
647
|
}
|
|
477
648
|
|
|
478
649
|
_stop(reason = 'manual') {
|
|
650
|
+
if (this._externalFrameDrive) return;
|
|
479
651
|
if (!this.running) return;
|
|
480
652
|
this.running = false;
|
|
481
653
|
this.dispatchEvent(new CustomEvent('cell-bg-animation-stop', {
|
|
@@ -486,6 +658,11 @@ export class CellBg extends Symbiote {
|
|
|
486
658
|
// Loop will smoothly decelerate and stop in renderLoop
|
|
487
659
|
}
|
|
488
660
|
|
|
661
|
+
_scheduleFrame() {
|
|
662
|
+
if (this._externalFrameDrive || this._frameRequest !== null) return;
|
|
663
|
+
this._frameRequest = requestFrame(this.renderLoop);
|
|
664
|
+
}
|
|
665
|
+
|
|
489
666
|
_step() {
|
|
490
667
|
if (!this.cols || !this.rows) return;
|
|
491
668
|
let len = this.cols * this.rows;
|
|
@@ -551,6 +728,8 @@ export class CellBg extends Symbiote {
|
|
|
551
728
|
}
|
|
552
729
|
|
|
553
730
|
renderLoop(ts) {
|
|
731
|
+
this._frameRequest = null;
|
|
732
|
+
if (this._externalFrameDrive) return;
|
|
554
733
|
if (!this.isAnimating) return;
|
|
555
734
|
|
|
556
735
|
let time = now();
|
|
@@ -559,7 +738,8 @@ export class CellBg extends Symbiote {
|
|
|
559
738
|
|
|
560
739
|
// Smoothly accelerate/decelerate
|
|
561
740
|
let targetSpeed = this.running ? 1.0 : 0.0;
|
|
562
|
-
|
|
741
|
+
let speedBlend = 1 - Math.pow(1 - 0.03, dt / (1000 / 60));
|
|
742
|
+
this.currentSpeed += (targetSpeed - this.currentSpeed) * speedBlend;
|
|
563
743
|
|
|
564
744
|
// If we're fully stopped and radii have faded (we just wait for speed to drop near 0)
|
|
565
745
|
if (!this.running && this.currentSpeed < 0.005) {
|
|
@@ -582,17 +762,29 @@ export class CellBg extends Symbiote {
|
|
|
582
762
|
maxSteps--;
|
|
583
763
|
}
|
|
584
764
|
|
|
765
|
+
this._advanceLiveRadii(dt);
|
|
585
766
|
this._draw();
|
|
586
767
|
|
|
587
768
|
if (this.isAnimating) {
|
|
588
|
-
|
|
769
|
+
this._scheduleFrame();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
_advanceLiveRadii(elapsedMs) {
|
|
774
|
+
let frames = Math.max(0, Number(elapsedMs) || 0) / (1000 / 60);
|
|
775
|
+
for (let index = 0; index < this.radii.length; index += 1) {
|
|
776
|
+
let alive = this.grid[index] === 1;
|
|
777
|
+
let target = alive ? this._maxRadius : this._minRadius;
|
|
778
|
+
let factor = alive ? 0.2 : this._fadeRate;
|
|
779
|
+
this.radii[index] = target + (this.radii[index] - target) * Math.pow(1 - factor, frames);
|
|
589
780
|
}
|
|
590
781
|
}
|
|
591
782
|
|
|
592
|
-
_draw() {
|
|
783
|
+
_draw(frame = null) {
|
|
593
784
|
if (!this._available || !this.canvas._w) return;
|
|
594
785
|
let w = this.canvas._w;
|
|
595
786
|
let h = this.canvas._h;
|
|
787
|
+
let radii = frame?.radii || this.radii;
|
|
596
788
|
|
|
597
789
|
this.ctx.clearRect(0, 0, w, h);
|
|
598
790
|
this.ctx.fillStyle = this._bgFill;
|
|
@@ -603,18 +795,7 @@ export class CellBg extends Symbiote {
|
|
|
603
795
|
for (let y = 0; y < this.rows; y++) {
|
|
604
796
|
for (let x = 0; x < this.cols; x++) {
|
|
605
797
|
let idx = y * this.cols + x;
|
|
606
|
-
let
|
|
607
|
-
|
|
608
|
-
let targetR = alive ? this._maxRadius : this._minRadius;
|
|
609
|
-
let currentR = this.radii[idx];
|
|
610
|
-
|
|
611
|
-
if (alive) {
|
|
612
|
-
this.radii[idx] = currentR + (targetR - currentR) * 0.2;
|
|
613
|
-
} else {
|
|
614
|
-
this.radii[idx] = currentR + (targetR - currentR) * this._fadeRate;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
let r = this.radii[idx];
|
|
798
|
+
let r = radii[idx];
|
|
618
799
|
let cx = x * this._cellSize;
|
|
619
800
|
let cy = y * this._cellSize;
|
|
620
801
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const RULE_B = new Set([3]);
|
|
2
|
+
const RULE_S = new Set([2, 3]);
|
|
3
|
+
const LIVE_FRAME_MS = 1000 / 60;
|
|
4
|
+
const DEFAULT_SEED = 'symbiote-cell-bg';
|
|
5
|
+
|
|
6
|
+
function finitePositive(value, fallback) {
|
|
7
|
+
let number = Number(value);
|
|
8
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function seedHash(value) {
|
|
12
|
+
let hash = 2166136261;
|
|
13
|
+
for (let char of String(value || DEFAULT_SEED)) {
|
|
14
|
+
hash ^= char.codePointAt(0);
|
|
15
|
+
hash = Math.imul(hash, 16777619);
|
|
16
|
+
}
|
|
17
|
+
return hash >>> 0 || 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nextRandom(state) {
|
|
21
|
+
state.randomState = (state.randomState + 0x6D2B79F5) >>> 0;
|
|
22
|
+
let value = state.randomState;
|
|
23
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
24
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
25
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function approach(value, target, factor, elapsedMs) {
|
|
29
|
+
let safeFactor = Math.min(0.999999, Math.max(0, Number(factor) || 0));
|
|
30
|
+
let frames = Math.max(0, Number(elapsedMs) || 0) / LIVE_FRAME_MS;
|
|
31
|
+
return target + (value - target) * Math.pow(1 - safeFactor, frames);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function advanceRadii(state, elapsedMs) {
|
|
35
|
+
for (let index = 0; index < state.radii.length; index += 1) {
|
|
36
|
+
let alive = state.grid[index] === 1;
|
|
37
|
+
let target = alive ? state.maxRadius : state.minRadius;
|
|
38
|
+
let factor = alive ? 0.2 : state.fadeRate;
|
|
39
|
+
state.radii[index] = approach(state.radii[index], target, factor, elapsedMs);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function injectNoise(state) {
|
|
44
|
+
let regionW = Math.max(4, Math.floor(state.cols * 0.3));
|
|
45
|
+
let regionH = Math.max(4, Math.floor(state.rows * 0.3));
|
|
46
|
+
let startX = Math.floor(nextRandom(state) * Math.max(1, state.cols - regionW));
|
|
47
|
+
let startY = Math.floor(nextRandom(state) * Math.max(1, state.rows - regionH));
|
|
48
|
+
for (let y = startY; y < Math.min(state.rows, startY + regionH); y += 1) {
|
|
49
|
+
for (let x = startX; x < Math.min(state.cols, startX + regionW); x += 1) {
|
|
50
|
+
if (nextRandom(state) >= 0.15) continue;
|
|
51
|
+
let index = y * state.cols + x;
|
|
52
|
+
state.grid[index] = 1;
|
|
53
|
+
state.radii[index] = state.minRadius;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stepGrid(state) {
|
|
59
|
+
let next = new Uint8Array(state.grid.length);
|
|
60
|
+
let changed = 0;
|
|
61
|
+
for (let y = 0; y < state.rows; y += 1) {
|
|
62
|
+
for (let x = 0; x < state.cols; x += 1) {
|
|
63
|
+
let neighbors = 0;
|
|
64
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
65
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
66
|
+
if (dx === 0 && dy === 0) continue;
|
|
67
|
+
let nx = (x + dx + state.cols) % state.cols;
|
|
68
|
+
let ny = (y + dy + state.rows) % state.rows;
|
|
69
|
+
neighbors += state.grid[ny * state.cols + nx];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let index = y * state.cols + x;
|
|
73
|
+
let alive = state.grid[index] === 1;
|
|
74
|
+
next[index] = alive
|
|
75
|
+
? (RULE_S.has(neighbors) ? 1 : 0)
|
|
76
|
+
: (RULE_B.has(neighbors) ? 1 : 0);
|
|
77
|
+
if (next[index] !== state.grid[index]) changed += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
state.grid = next;
|
|
81
|
+
if (changed < state.grid.length * 0.02) {
|
|
82
|
+
state.stagnantCount += 1;
|
|
83
|
+
if (state.stagnantCount >= 5) {
|
|
84
|
+
injectNoise(state);
|
|
85
|
+
state.stagnantCount = 0;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
state.stagnantCount = 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createCellBgRenderState(options = {}) {
|
|
93
|
+
let cols = Math.max(1, Math.floor(finitePositive(options.cols, 1)));
|
|
94
|
+
let rows = Math.max(1, Math.floor(finitePositive(options.rows, 1)));
|
|
95
|
+
let minRadius = finitePositive(options.minRadius, 2);
|
|
96
|
+
let state = {
|
|
97
|
+
seed: String(options.seed || DEFAULT_SEED),
|
|
98
|
+
cols,
|
|
99
|
+
rows,
|
|
100
|
+
stepMs: finitePositive(options.stepMs, 75),
|
|
101
|
+
minRadius,
|
|
102
|
+
maxRadius: Math.max(minRadius, finitePositive(options.maxRadius, 4)),
|
|
103
|
+
fadeRate: Math.min(1, Math.max(0, Number(options.fadeRate) || 0.04)),
|
|
104
|
+
grid: new Uint8Array(cols * rows),
|
|
105
|
+
radii: new Float32Array(cols * rows),
|
|
106
|
+
randomState: seedHash(`${options.seed || DEFAULT_SEED}:${cols}x${rows}`),
|
|
107
|
+
stagnantCount: 0,
|
|
108
|
+
stepIndex: 0,
|
|
109
|
+
timeMs: 0,
|
|
110
|
+
};
|
|
111
|
+
state.radii.fill(minRadius);
|
|
112
|
+
for (let index = 0; index < state.grid.length; index += 1) {
|
|
113
|
+
state.grid[index] = nextRandom(state) < 0.15 ? 1 : 0;
|
|
114
|
+
}
|
|
115
|
+
return state;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function seekCellBgRenderState(state, timeMs, options = state || {}) {
|
|
119
|
+
let targetTimeMs = Math.max(0, Number(timeMs) || 0);
|
|
120
|
+
let next = state;
|
|
121
|
+
if (!next || targetTimeMs < next.timeMs) {
|
|
122
|
+
next = createCellBgRenderState(options);
|
|
123
|
+
}
|
|
124
|
+
let targetStep = Math.floor(targetTimeMs / next.stepMs);
|
|
125
|
+
while (next.stepIndex < targetStep) {
|
|
126
|
+
advanceRadii(next, next.stepMs);
|
|
127
|
+
stepGrid(next);
|
|
128
|
+
next.stepIndex += 1;
|
|
129
|
+
}
|
|
130
|
+
next.timeMs = targetTimeMs;
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function cellBgRenderFrame(state) {
|
|
135
|
+
let elapsedMs = Math.max(0, state.timeMs - state.stepIndex * state.stepMs);
|
|
136
|
+
let radii = new Float32Array(state.radii.length);
|
|
137
|
+
for (let index = 0; index < radii.length; index += 1) {
|
|
138
|
+
let alive = state.grid[index] === 1;
|
|
139
|
+
let target = alive ? state.maxRadius : state.minRadius;
|
|
140
|
+
let factor = alive ? 0.2 : state.fadeRate;
|
|
141
|
+
radii[index] = approach(state.radii[index], target, factor, elapsedMs);
|
|
142
|
+
}
|
|
143
|
+
return { grid: state.grid, radii };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function cellBgRenderHash(state, frame = cellBgRenderFrame(state)) {
|
|
147
|
+
let hash = 2166136261;
|
|
148
|
+
for (let index = 0; index < frame.grid.length; index += 1) {
|
|
149
|
+
hash ^= frame.grid[index];
|
|
150
|
+
hash = Math.imul(hash, 16777619);
|
|
151
|
+
hash ^= Math.round(frame.radii[index] * 1000);
|
|
152
|
+
hash = Math.imul(hash, 16777619);
|
|
153
|
+
}
|
|
154
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
155
|
+
}
|
package/index.js
CHANGED
|
@@ -136,11 +136,14 @@ export {
|
|
|
136
136
|
} from './canvas/CanvasGraph/CanvasGraphViewport.js';
|
|
137
137
|
export {
|
|
138
138
|
GRAPH_DIRECTORY_FRAME_COLORS,
|
|
139
|
+
GRAPH_PATH_STYLE_MENU_GROUP,
|
|
140
|
+
GRAPH_PATH_STYLE_MENU_ITEMS,
|
|
139
141
|
GRAPH_PATH_STYLES,
|
|
140
142
|
GRAPH_VIEW_MODES,
|
|
141
143
|
addGraphDirectoryFrames,
|
|
142
144
|
applyGraphExplorerViewMode,
|
|
143
145
|
buildFlatPathHash,
|
|
146
|
+
createGraphPathStyleMenuActions,
|
|
144
147
|
createGraphExplorerViewController,
|
|
145
148
|
createGraphViewModeController,
|
|
146
149
|
getFileSelectionNodeId,
|
|
@@ -150,6 +153,7 @@ export {
|
|
|
150
153
|
getNextGraphPathStyle,
|
|
151
154
|
normalizeGraphExplorerViewMode,
|
|
152
155
|
normalizeGraphViewMode,
|
|
156
|
+
resolveGraphPathStyleAction,
|
|
153
157
|
resolveFlatHashChange,
|
|
154
158
|
resolveInitialGraphViewMode,
|
|
155
159
|
shouldClearFocusOnSelection,
|
|
@@ -3219,12 +3219,13 @@ export let COMPONENTS = [
|
|
|
3219
3219
|
status: 'draft',
|
|
3220
3220
|
schemaVersion: 'component-descriptor-v2',
|
|
3221
3221
|
dataSchema: 'schemas/graph-model-v1.json',
|
|
3222
|
-
capabilities: ['node-card', 'ports', 'controls', 'shape-variant', 'media-avatar', 'category-accent', 'type-accent', 'error-state'],
|
|
3222
|
+
capabilities: ['node-card', 'ports', 'controls', 'shape-variant', 'media-avatar', 'media-fit', 'category-accent', 'type-accent', 'error-state'],
|
|
3223
3223
|
attributes: [
|
|
3224
3224
|
{ name: 'node-label', type: 'string', description: 'Rendered node label.' },
|
|
3225
3225
|
{ name: 'node-category', type: 'string', description: 'Semantic node category used for icon and accent selection.' },
|
|
3226
3226
|
{ name: 'node-type', type: 'string', description: 'Semantic node type used for type-specific accent selection when a matching theme token exists.' },
|
|
3227
3227
|
{ name: 'node-shape', type: 'string', description: 'Supported node shape: rect, pill, circle, disc, diamond, comment, or registered SVG preset such as hexagon.' },
|
|
3228
|
+
{ name: 'data-media-fit', type: 'string', description: 'Resolved media object-fit strategy: contain for transparent/logo media, cover for cropped photo media.' },
|
|
3228
3229
|
],
|
|
3229
3230
|
properties: [
|
|
3230
3231
|
{ name: 'mediaSrc', type: 'string', description: 'Media or avatar image URL derived from node params.' },
|
|
@@ -3453,7 +3454,7 @@ export let COMPONENTS = [
|
|
|
3453
3454
|
status: 'draft',
|
|
3454
3455
|
schemaVersion: 'component-descriptor-v2',
|
|
3455
3456
|
dataSchema: 'schemas/graph-model-v1.json',
|
|
3456
|
-
capabilities: ['hierarchical-graph', 'overview-read-renderer', 'force-layout', 'semantic-clusters', 'focus-selection', 'layout-snapshot', 'device-orientation-parallax'],
|
|
3457
|
+
capabilities: ['hierarchical-graph', 'overview-read-renderer', 'force-layout', 'semantic-clusters', 'focus-selection', 'layout-snapshot', 'viewport-control', 'device-orientation-parallax'],
|
|
3457
3458
|
attributes: [
|
|
3458
3459
|
{ name: 'active-node-scale', type: 'number', description: 'Visual scale target for the selected node; defaults to 1.5.' },
|
|
3459
3460
|
{ name: 'info-panel-scale', type: 'number', description: 'Visual scale for the active node information panel; defaults to 1.' },
|
|
@@ -3480,6 +3481,8 @@ export let COMPONENTS = [
|
|
|
3480
3481
|
{ name: 'focusNodes', type: 'function', description: 'Agent-facing alias for fitting one or more graph node ids into the viewport.' },
|
|
3481
3482
|
{ name: 'queueTransitionMarkers', type: 'function', description: 'Queues animated route markers from one graph node to multiple target node ids without changing the host-owned graph model.' },
|
|
3482
3483
|
{ name: 'fitView', type: 'function', description: 'Fits the visible graph into the canvas viewport; accepts optional viewportEase for live replay tracking.' },
|
|
3484
|
+
{ name: 'getViewport', type: 'function', description: 'Returns the current zoom and pan viewport as a portable snapshot.' },
|
|
3485
|
+
{ name: 'setViewport', type: 'function', description: 'Applies an absolute, clamped zoom and pan viewport immediately or through the graph camera easing loop.' },
|
|
3483
3486
|
{ name: 'setVisualOptions', type: 'function', description: 'Applies visual tuning options such as activeNodeScale and infoPanelScale without changing the host-owned graph model.' },
|
|
3484
3487
|
{ name: 'setForceLayoutOptions', type: 'function', description: 'Applies force-layout tuning options, including layoutAlgorithm: organic, oil-cloud, crystal, or spring, without changing the host-owned graph model.' },
|
|
3485
3488
|
{ name: 'pulseNode', type: 'function', description: 'Projects a timed pulse around one node, optionally from an explicit render-clock start time.' },
|
|
@@ -4440,6 +4443,7 @@ export let COMPONENTS = [
|
|
|
4440
4443
|
'resize-aware',
|
|
4441
4444
|
'reduced-motion-friendly',
|
|
4442
4445
|
'layout-lifecycle',
|
|
4446
|
+
'deterministic-render',
|
|
4443
4447
|
],
|
|
4444
4448
|
properties: [
|
|
4445
4449
|
{ name: 'active', type: 'boolean', description: 'Persistent animation state.' },
|
|
@@ -4450,6 +4454,7 @@ export let COMPONENTS = [
|
|
|
4450
4454
|
{ name: 'active', type: 'boolean', description: 'Starts persistent animation when present.' },
|
|
4451
4455
|
{ name: 'auto-trigger', type: 'boolean', description: 'Set to false/off to disable mount and resize pulses.' },
|
|
4452
4456
|
{ name: 'pulse-duration', type: 'number', description: 'Default timed pulse duration in milliseconds.' },
|
|
4457
|
+
{ name: 'seed', type: 'string', description: 'Stable seed used by deterministic external-frame rendering.' },
|
|
4453
4458
|
],
|
|
4454
4459
|
methods: [
|
|
4455
4460
|
{ name: 'start', type: 'function', description: 'Starts persistent animation with smooth acceleration.' },
|
|
@@ -4461,6 +4466,9 @@ export let COMPONENTS = [
|
|
|
4461
4466
|
{ name: 'resize', type: 'function', description: 'Resizes the canvas to its host bounds.' },
|
|
4462
4467
|
{ name: 'suspendLayout', type: 'function', description: 'Immediately stops animation work while the containing layout group is hidden.' },
|
|
4463
4468
|
{ name: 'resumeLayout', type: 'function', description: 'Restores persistent animation only when it was active before layout suspension.' },
|
|
4469
|
+
{ name: 'setFrameDriver', type: 'function', description: 'Selects self-driven live animation or externally driven deterministic rendering.' },
|
|
4470
|
+
{ name: 'presentFrame', type: 'function', description: 'Synchronously paints the current shared render-clock time.' },
|
|
4471
|
+
{ name: 'getFramePresentation', type: 'function', description: 'Returns deterministic frame metrics and the current grid hash.' },
|
|
4464
4472
|
],
|
|
4465
4473
|
events: [
|
|
4466
4474
|
{ name: 'cell-bg-animation-trigger', description: 'Emits when a timed activity pulse is requested.', detail: [{ name: 'duration', type: 'number' }] },
|
|
@@ -229,6 +229,14 @@ export let styles = css`
|
|
|
229
229
|
-webkit-user-drag: none;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
&[data-media-fit='contain'] .sn-node-media-img {
|
|
233
|
+
object-fit: contain;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
&[data-media-fit='cover'] .sn-node-media-img {
|
|
237
|
+
object-fit: cover;
|
|
238
|
+
}
|
|
239
|
+
|
|
232
240
|
& .sn-node-content {
|
|
233
241
|
padding: var(--sn-node-content-padding, 8px 12px 10px);
|
|
234
242
|
min-width: 0;
|
|
@@ -43,6 +43,14 @@ function normalizeNodeTone(value) {
|
|
|
43
43
|
return tone;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function normalizeMediaFit(value) {
|
|
47
|
+
let fit = String(value || '').trim().toLowerCase();
|
|
48
|
+
if (fit === 'fit') return 'contain';
|
|
49
|
+
if (fit === 'crop') return 'cover';
|
|
50
|
+
if (fit === 'contain' || fit === 'cover') return fit;
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
export class GraphNode extends Symbiote {
|
|
47
55
|
destructionDelay = 200;
|
|
48
56
|
|
|
@@ -108,6 +116,7 @@ export class GraphNode extends Symbiote {
|
|
|
108
116
|
this.toggleAttribute('data-header-hidden', Boolean(params.hideHeader || params.headerHidden));
|
|
109
117
|
this.toggleAttribute('data-content-hidden', contentHidden);
|
|
110
118
|
setOptionalAttribute(this, 'data-node-tone', normalizeNodeTone(params.tone || params.nodeTone));
|
|
119
|
+
setOptionalAttribute(this, 'data-media-fit', normalizeMediaFit(params.mediaFit || params.imageFit || params.avatarFit));
|
|
111
120
|
this.set$({
|
|
112
121
|
nodeIcon: node.icon || CATEGORY_ICONS[node.category] || CATEGORY_ICONS.default,
|
|
113
122
|
mediaSrc: params.media || params.image || params.avatar || '',
|
package/package.json
CHANGED
package/ui/index.js
CHANGED
|
@@ -159,11 +159,14 @@ export {
|
|
|
159
159
|
} from '../canvas/graph-layout.js';
|
|
160
160
|
export {
|
|
161
161
|
GRAPH_DIRECTORY_FRAME_COLORS,
|
|
162
|
+
GRAPH_PATH_STYLE_MENU_GROUP,
|
|
163
|
+
GRAPH_PATH_STYLE_MENU_ITEMS,
|
|
162
164
|
GRAPH_PATH_STYLES,
|
|
163
165
|
GRAPH_VIEW_MODES,
|
|
164
166
|
addGraphDirectoryFrames,
|
|
165
167
|
applyGraphExplorerViewMode,
|
|
166
168
|
buildFlatPathHash,
|
|
169
|
+
createGraphPathStyleMenuActions,
|
|
167
170
|
createGraphExplorerViewController,
|
|
168
171
|
createGraphViewModeController,
|
|
169
172
|
getFileSelectionNodeId,
|
|
@@ -175,6 +178,7 @@ export {
|
|
|
175
178
|
normalizeGraphViewMode,
|
|
176
179
|
renderGraphPathStyleButton,
|
|
177
180
|
renderGraphViewModeButton,
|
|
181
|
+
resolveGraphPathStyleAction,
|
|
178
182
|
resolveFlatHashChange,
|
|
179
183
|
resolveInitialGraphViewMode,
|
|
180
184
|
selectGraphLabelMode,
|
package/webmcp.js
CHANGED
|
@@ -23,7 +23,6 @@ export {
|
|
|
23
23
|
export function getModelContext(target = globalThis.document) {
|
|
24
24
|
return target?.modelContext
|
|
25
25
|
|| (typeof target?.registerTool === 'function' ? target : null)
|
|
26
|
-
|| globalThis.navigator?.modelContext
|
|
27
26
|
|| null;
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -65,7 +64,7 @@ export async function createNativeToolDescriptor(options) {
|
|
|
65
64
|
return new ToolDescriptor(options);
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
export async function registerWebMcpTool(options, target = globalThis.document) {
|
|
67
|
+
export async function registerWebMcpTool(options, target = globalThis.document, registrationOptions = {}) {
|
|
69
68
|
let context = getModelContext(target);
|
|
70
69
|
if (!context || typeof context.registerTool !== 'function') {
|
|
71
70
|
return { nativeActive: false, descriptor: createToolDescriptor(options), unregister: () => {} };
|
|
@@ -82,7 +81,7 @@ export async function registerWebMcpTool(options, target = globalThis.document)
|
|
|
82
81
|
nativeActive = false;
|
|
83
82
|
descriptor = createToolDescriptor(options);
|
|
84
83
|
}
|
|
85
|
-
let registration = context.registerTool(descriptor);
|
|
84
|
+
let registration = await context.registerTool(descriptor, registrationOptions);
|
|
86
85
|
let unregister = registrationUnregister(registration);
|
|
87
86
|
|
|
88
87
|
return { nativeActive, descriptor, unregister };
|
|
@@ -703,23 +702,42 @@ export function getWebMcpPresentationActionPhase(value, options = {}) {
|
|
|
703
702
|
return WEBMCP_PRESENTATION_TOUR_PHASES.AFTER_FOCUS;
|
|
704
703
|
}
|
|
705
704
|
|
|
706
|
-
export function
|
|
705
|
+
export function createWebMcpTourTurnActionPlans(turn = {}, options = {}) {
|
|
707
706
|
let actions = webMcpPresentationActionsFromOptions(options);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
707
|
+
return (Array.isArray(turn?.cues) ? turn.cues : [])
|
|
708
|
+
.filter((cue) => cue?.kind === 'interaction' && isObject(cue.interaction?.binding))
|
|
709
|
+
.map((cue) => {
|
|
710
|
+
let binding = cue.interaction.binding;
|
|
711
|
+
let source = { tool: binding.tool, input: { ...(isObject(binding.input) ? binding.input : {}) } };
|
|
712
|
+
let action = webMcpPresentationActionFromValue(source, actions);
|
|
713
|
+
if (cue.targetId && action?.inputSchema?.properties?.targetId) source.input.targetId = cue.targetId;
|
|
714
|
+
let phase = getWebMcpPresentationActionPhase(source, { ...options, actions });
|
|
715
|
+
let command = action && phase !== WEBMCP_PRESENTATION_TOUR_PHASES.NONE ? {
|
|
716
|
+
tool: action.name || action.id,
|
|
717
|
+
input: source.input,
|
|
718
|
+
} : null;
|
|
719
|
+
return {
|
|
720
|
+
cue,
|
|
721
|
+
action,
|
|
722
|
+
command,
|
|
723
|
+
phase,
|
|
724
|
+
runBeforeFocus: phase === WEBMCP_PRESENTATION_TOUR_PHASES.BEFORE_FOCUS,
|
|
725
|
+
runAfterFocus: phase === WEBMCP_PRESENTATION_TOUR_PHASES.AFTER_FOCUS,
|
|
726
|
+
runAsAnnotation: phase === WEBMCP_PRESENTATION_TOUR_PHASES.AFTER_FOCUS_ANNOTATION,
|
|
727
|
+
runAsEffect: phase === WEBMCP_PRESENTATION_TOUR_PHASES.AFTER_FOCUS,
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function createWebMcpTourTurnActionPlan(turn = {}, options = {}) {
|
|
733
|
+
return createWebMcpTourTurnActionPlans(turn, options)[0] || {
|
|
734
|
+
action: null,
|
|
735
|
+
command: null,
|
|
736
|
+
phase: WEBMCP_PRESENTATION_TOUR_PHASES.NONE,
|
|
737
|
+
runBeforeFocus: false,
|
|
738
|
+
runAfterFocus: false,
|
|
739
|
+
runAsAnnotation: false,
|
|
740
|
+
runAsEffect: false,
|
|
723
741
|
};
|
|
724
742
|
}
|
|
725
743
|
|