noding / static /canvas.js
broadfield-dev's picture
Update static/canvas.js
fa14350 verified
raw
history blame
7.91 kB
// static/js/canvas.js
// --- Configuration ---
const CONFIG = {
// Only render nodes within this padding of the viewport
viewportPadding: 200,
// Throttle scroll events to run only every 16ms (60fps)
throttleMs: 16,
// Visual settings
nodeWidth: 200,
nodeHeight: 50,
indentWidth: 60,
rowHeight: 80
};
// --- Setup Stage ---
const width = window.innerWidth - 400; // Adjust for sidebar
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true
});
const layer = new Konva.Layer();
stage.add(layer);
// Global State
let allNodes = []; // Array of { group, x, y, visible }
let sourceLines = []; // Cached source code lines
let isTicking = false; // For throttling
// --- 1. Optimization Core: Viewport Culling ---
function updateVisibleNodes() {
isTicking = false;
// Get the visible viewport in "World Coordinates" (accounting for zoom/pan)
const scale = stage.scaleX();
const stageX = stage.x();
const stageY = stage.y();
// The logic: Invert the transform to find what part of the world is visible
const viewX = -(stageX / scale) - CONFIG.viewportPadding;
const viewY = -(stageY / scale) - CONFIG.viewportPadding;
const viewW = (stage.width() / scale) + (CONFIG.viewportPadding * 2);
const viewH = (stage.height() / scale) + (CONFIG.viewportPadding * 2);
const viewRight = viewX + viewW;
const viewBottom = viewY + viewH;
// Batch updates to prevent multiple redraws
let nodesChanged = false;
// Fast loop (standard for-loop is faster than .forEach for massive arrays)
for (let i = 0; i < allNodes.length; i++) {
const node = allNodes[i];
// Simple Bounding Box Collision Check
const isVisible = (
node.x < viewRight &&
node.x + CONFIG.nodeWidth > viewX &&
node.y < viewBottom &&
node.y + CONFIG.nodeHeight > viewY
);
// Only touch the DOM/Konva object if state changes (Save CPU)
if (node.visible !== isVisible) {
node.group.visible(isVisible);
node.visible = isVisible;
nodesChanged = true;
}
}
if (nodesChanged) {
// layer.batchDraw() is more efficient than layer.draw()
layer.batchDraw();
}
}
// Request Animation Frame Wrapper (Throttling)
function requestUpdate() {
if (!isTicking) {
requestAnimationFrame(updateVisibleNodes);
isTicking = true;
}
}
// Bind optimization to interactions
stage.on('dragmove', requestUpdate);
stage.on('wheel', (e) => {
// ... (Zoom logic below) ...
// After zoom, we must update visibility
requestUpdate();
});
// --- 2. Standard Logic (Zoom & Draw) ---
// Zoom Logic
stage.on('wheel', (e) => {
e.evt.preventDefault();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const scaleBy = 1.05;
const newScale = e.evt.deltaY > 0 ? oldScale / scaleBy : oldScale * scaleBy;
stage.scale({ x: newScale, y: newScale });
const newPos = {
x: pointer.x - (pointer.x - stage.x()) / oldScale * newScale,
y: pointer.y - (pointer.y - stage.y()) / oldScale * newScale
};
stage.position(newPos);
});
// Resize Handler
window.addEventListener('resize', () => {
stage.width(window.innerWidth - 400);
stage.height(window.innerHeight);
requestUpdate();
});
// API Listener
document.getElementById('btnVisualize').addEventListener('click', () => {
const code = document.getElementById('codeInput').value;
log('Parsing...');
fetch('/parse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: code })
})
.then(res => res.json())
.then(data => {
if(data.error) log(data.error, 'error');
else {
drawGraph(data, code);
log(`Graph: ${data.nodes.length} nodes`);
}
});
});
function drawGraph(data, fullSourceCode) {
// Cleanup old memory
layer.destroyChildren();
allNodes = [];
// Cache source lines
sourceLines = fullSourceCode.split('\n');
const nodeMap = {};
// --- Batch Create Nodes ---
data.nodes.forEach((node, index) => {
const x = 100 + (node.lvl * CONFIG.indentWidth);
const y = 50 + (index * CONFIG.rowHeight);
// Styling
const colors = {
'function': '#a29bfe', 'class': '#e84393', 'if': '#fab1a0',
'for': '#fdcb6e', 'while': '#fdcb6e', 'return': '#55efc4',
'assigned_variable': '#74b9ff', 'import': '#b2bec3'
};
const group = new Konva.Group({
x: x,
y: y,
// Optimization: Stop this group from catching mouse events if not needed
listening: true
});
const rect = new Konva.Rect({
width: CONFIG.nodeWidth,
height: CONFIG.nodeHeight,
fill: '#2d3436',
stroke: colors[node.type] || '#636e72',
strokeWidth: 2,
cornerRadius: 8,
shadowColor: 'black',
shadowBlur: 10,
shadowOpacity: 0.3,
// Optimization: Perfect bounding box for hit detection
hitStrokeWidth: 0
});
const text = new Konva.Text({
x: 10, y: 10,
text: node.lbl,
fontSize: 14, fontFamily: 'JetBrains Mono', fill: '#fff',
width: 180, ellipsis: true,
// Optimization: Text doesn't need to listen to clicks, the Group/Rect handles it
listening: false
});
const vecText = new Konva.Text({
x: 10, y: 32,
text: `V:[${node.vec[0]}, ${node.vec[2]}...]`,
fontSize: 10, fontFamily: 'JetBrains Mono', fill: '#636e72',
listening: false
});
group.add(rect);
group.add(text);
group.add(vecText);
// Interaction
group.on('click', () => {
const start = node.loc[0] - 1;
const end = node.loc[1];
const snippet = sourceLines.slice(start, end).join('\n');
log(`Source:\n${snippet}`);
});
// Hover
group.on('mouseover', () => { document.body.style.cursor = 'pointer'; rect.fill('#353b48'); layer.batchDraw(); });
group.on('mouseout', () => { document.body.style.cursor = 'default'; rect.fill('#2d3436'); layer.batchDraw(); });
layer.add(group);
// Save Reference for Culling Logic
nodeMap[node.id] = { x, y };
allNodes.push({
group: group,
x: x,
y: y,
visible: true
});
});
// Draw Connections
// Optimization: Draw connections on a generic "listening: false" shape to avoid hit-detection overhead
data.connections.forEach(conn => {
const f = nodeMap[conn.f];
const t = nodeMap[conn.t];
if (f && t) {
const line = new Konva.Line({
points: [f.x+20, f.y+50, f.x+20, t.y-10, t.x+20, t.y-10, t.x+20, t.y],
stroke: '#636e72', strokeWidth: 2, tension: 0.2, opacity: 0.5,
listening: false // Critical: Don't calculate mouse hits for lines
});
layer.add(line);
line.moveToBottom();
}
});
layer.batchDraw();
// Initial cull calculation
updateVisibleNodes();
}
function log(msg, type='info') {
const consoleBody = document.getElementById('logOutput');
const color = type === 'error' ? '#ff7675' : '#55efc4';
if(consoleBody) {
consoleBody.innerHTML += `<div style="color:${color}">> ${msg}</div>`;
consoleBody.scrollTop = consoleBody.scrollHeight;
}
}