import { createGraphEngine } from '@graphora/engine' import { attachGraphInput } from '@graphora/interactions' import { CanvasGraphRenderer, createViewport, worldToScreen } from '@graphora/renderer' import './style.css' const host = document.querySelector('#graph')! const status = document.querySelector('#status')! const renderer = new CanvasGraphRenderer() const engine = createGraphEngine({ renderer, graph: { nodes: [ { id: 'a', label: 'API', position: { x: 0, y: 0 } }, { id: 'b', label: 'Worker', position: { x: 160, y: 0 } } ], edges: [{ id: 'ab', source: 'a', target: 'b', directed: true }] }, viewport: createViewport({ width: host.clientWidth, height: host.clientHeight, center: { x: 80, y: 0 }, zoom: 1 }), theme: { backgroundColor: '#f8fafc', node: { radius: 18, fill: '#0f766e' } } }) let mounts = 0 let cleanups = 0 let pulses = 0 let disposed = false const background = renderer.addLayer({ id: 'team-band', type: 'canvas', order: 'background', coordinates: 'world', draw(context) { context.fillStyle = '#c4b5fd' context.fillRect(-45, -45, 250, 90) } }) renderer.addLayer({ id: 'screen-marker', type: 'canvas', coordinates: 'screen', draw(context) { context.fillStyle = '#22c55e' context.fillRect(12, 12, 16, 16) } }) const card = renderer.addLayer({ id: 'annotation', type: 'html', coordinates: 'world', input: 'exclusive', mount(root, scope) { mounts++ const label = document.createElement('label') label.textContent = 'Annotation ' label.style.cssText = 'position:absolute;left:0;top:-100px;width:170px;padding:8px;background:white;border:1px solid #64748b;' const input = document.createElement('input') input.setAttribute('aria-label', 'Annotation text') input.value = 'Team platform' input.style.width = '140px' label.append(input) root.append(label) const pulse = () => { pulses++ expose() } window.addEventListener('graphora-layer-pulse', pulse) scope.onCleanup(() => { window.removeEventListener('graphora-layer-pulse', pulse) cleanups++ }) }, dispose() { disposed = true } }) function expose() { const viewport = engine.getViewport() window.__GRAPHORA_LAYERS__ = { mounts, cleanups, pulses, disposed, zoom: viewport.zoom, node: worldToScreen(viewport, { x: 0, y: 0 }), band: worldToScreen(viewport, { x: -30, y: 0 }), annotation: worldToScreen(viewport, { x: 0, y: -100 }) } status.value = `${mounts} mounts · ${cleanups} cleanups · ${pulses} pulses` } engine.onLifecycle('render:after', expose) engine.mount(host) const detach = attachGraphInput(host, engine, renderer, { onClick(pick) { document.querySelector('#selection')!.textContent = pick?.type === 'node' ? `Selected ${String(pick.nodeId)}` : pick?.type === 'edge' ? `Selected ${String(pick.edgeId)}` : 'No selection' } }) document.querySelector('#toggle')!.addEventListener('click', () => { if (card.enabled) { card.disable() background.disable() } else { card.enable() background.enable() } }) document.querySelector('#remount')!.addEventListener('click', () => { engine.unmount() engine.mount(host) }) document .querySelector('#zoom')! .addEventListener('click', () => engine.setViewport({ zoom: engine.getViewport().zoom === 1 ? 1.5 : 1 }) ) document.querySelector('#export')!.addEventListener('click', () => { const result = renderer.exportImage() const link = document.querySelector('#download')! link.href = result.dataUrl link.hidden = false link.dataset.omitted = result.omittedLayerIds?.join(',') ?? '' }) function destroy() { if (disposed) return detach() engine.destroy() document.querySelectorAll('button').forEach((button) => { button.disabled = true }) window.__GRAPHORA_LAYERS__ = { ...window.__GRAPHORA_LAYERS__!, mounts, cleanups, pulses, disposed } } document.querySelector('#destroy')!.addEventListener('click', destroy) window.addEventListener('pagehide', destroy, { once: true }) declare global { interface Window { __GRAPHORA_LAYERS__?: { mounts: number cleanups: number pulses: number disposed: boolean zoom: number node: { x: number; y: number } band: { x: number; y: number } annotation: { x: number; y: number } } } }