update animation

This commit is contained in:
Jidong Xiao
2025-04-25 20:12:30 -04:00
committed by JamesFlare1212
parent 083ae76743
commit 643c7f1038
3 changed files with 125 additions and 149 deletions

View File

@@ -1,6 +1,6 @@
@import url('https://fonts.googleapis.com/css?family=Roboto:400,500&display=swap'); @import url('https://fonts.googleapis.com/css?family=Roboto:400,500&display=swap');
/*base styling */ /* Base styling */
body { body {
margin: 0; margin: 0;
background-color: #f7f8fa; background-color: #f7f8fa;
@@ -8,7 +8,7 @@ body {
color: #333; color: #333;
} }
/*layout containers */ /* Layout containers */
#main-container { #main-container {
display: flex; display: flex;
width: 90%; width: 90%;
@@ -36,7 +36,7 @@ body {
box-sizing: border-box; box-sizing: border-box;
} }
/*stack title */ /* Stack title */
#stack-title { #stack-title {
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
@@ -44,7 +44,7 @@ body {
color: #555; color: #555;
} }
/*controls */ /* Controls */
#tabs { #tabs {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -100,7 +100,7 @@ button:active {
transform: translateY(1px); transform: translateY(1px);
} }
/*status & logs */ /* Status & logs */
#status, #status,
#log, #log,
#final-seq { #final-seq {

View File

@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Iterative Traversal (Step-by-Step)</title> <title>In-Order Traversal (Step-by-Step)</title>
<script src="konva.js"></script> <script src="konva.js"></script>
<link rel="stylesheet" href="traversal.css"> <link rel="stylesheet" href="traversal.css">
</head> </head>

View File

@@ -1,10 +1,9 @@
// In-Order / Pre-Order / Post-Order Traversal Visualization (combined)
//canvas dimensions for tree visualization // Canvas dimensions
const width = window.innerWidth * 0.9 * 0.75; const width = window.innerWidth * 0.9 * 0.75;
const height = 600; const height = 600;
//create the main stage and layer for the tree // Create Konva stage & layer
const stage = new Konva.Stage({ const stage = new Konva.Stage({
container: 'tree-container', container: 'tree-container',
width: width, width: width,
@@ -13,7 +12,7 @@ const stage = new Konva.Stage({
const layer = new Konva.Layer(); const layer = new Konva.Layer();
stage.add(layer); stage.add(layer);
//adjusted positions: node 9 is left child of 8 // Node positions (9 as left child of 8)
const positions = { const positions = {
1: { x: width/2, y: 80 }, 1: { x: width/2, y: 80 },
2: { x: width/2 -200, y:180 }, 2: { x: width/2 -200, y:180 },
@@ -23,10 +22,10 @@ const positions = {
6: { x: width/2 -150, y:380 }, 6: { x: width/2 -150, y:380 },
7: { x: width/2 -50, y:380 }, 7: { x: width/2 -50, y:380 },
8: { x: width/2 +300, y:280 }, 8: { x: width/2 +300, y:280 },
9: { x: width / 2 + 250, y: 380 } // left child of 8 9: { x: width/2 +250, y:380 }
}; };
//tree structure // Tree data
const tree = { const tree = {
val: 1, val: 1,
left: { left: {
@@ -49,205 +48,185 @@ const tree = {
} }
}; };
//store node references for highlighting // Store node visuals
const nodes = {}; const nodes = {};
//recursive tree draw with shortened lines // Draw a single node
function drawTree(node, parent = null) { function drawNode(x, y, label) {
if (!node) return; const circle = new Konva.Circle({ x, y, radius: 20, fill: 'white', stroke: '#333', strokeWidth: 2 });
const pos = positions[node.val]; const text = new Konva.Text({ x: x - 5, y: y - 8, text: label.toString(), fontSize: 16, fill: '#333' });
if (parent) { layer.add(circle, text);
const p = positions[parent.val]; nodes[label] = { circle, text };
const dx = pos.x - p.x, dy = pos.y - p.y;
const dist = Math.sqrt(dx*dx + dy*dy);
const offX = (dx/dist)*20, offY = (dy/dist)*20;
layer.add(new Konva.Line({
points: [p.x + offX, p.y + offY, pos.x - offX, pos.y - offY],
stroke: 'black', strokeWidth: 2
}));
}
const circle = new Konva.Circle({ x: pos.x, y: pos.y, radius: 20, fill: 'white', stroke: 'black', strokeWidth: 2 });
const text = new Konva.Text({ x: pos.x - 5, y: pos.y - 8, text: node.val.toString(), fontSize: 16, fill: 'black' });
layer.add(circle);
layer.add(text);
nodes[node.val] = { circle, text };
drawTree(node.left, node);
drawTree(node.right, node);
} }
//stack visualization setup // Draw edge between parent and child
function drawEdge(parentVal, childVal) {
const p = positions[parentVal], c = positions[childVal];
const dx = c.x - p.x, dy = c.y - p.y;
const d = Math.hypot(dx, dy), ox = (dx/d) * 20, oy = (dy/d) * 20;
const line = new Konva.Line({ points:[p.x+ox,p.y+oy, c.x-ox,c.y-oy], stroke: '#333', strokeWidth: 2 });
layer.add(line);
}
// Make tree
function renderTree(node, parent = null) {
if (!node) return;
drawNode(positions[node.val].x, positions[node.val].y, node.val);
if (parent) drawEdge(parent.val, node.val);
renderTree(node.left, node);
renderTree(node.right, node);
}
renderTree(tree);
// Stack visualization
const stackStage = new Konva.Stage({ container: 'stack-visual', width: 200, height: 500 }); const stackStage = new Konva.Stage({ container: 'stack-visual', width: 200, height: 500 });
const stackLayer = new Konva.Layer(); const stackLayer = new Konva.Layer();
stackStage.add(stackLayer); stackStage.add(stackLayer);
let stackVisual = []; let stackVisual = [];
function pushStack(val) { function pushStack(val) {
const box = new Konva.Rect({ x: 50, y: 400 - stackVisual.length * 30, width: 40, height: 25, fill: '#89CFF0', stroke: 'black' }); const y = 400 - stackVisual.length * 30;
const label = new Konva.Text({ x: 60, y: 405 - stackVisual.length * 30, text: val.toString(), fontSize: 16, fill: 'black' }); const box = new Konva.Rect({ x:50, y, width:40, height:25, fill:'#007bff', stroke:'#0056b3', cornerRadius:4 });
stackLayer.add(box); const txt = new Konva.Text({ x:58, y: y+4, text: val.toString(), fontSize:14, fill:'#fff' });
stackLayer.add(label); stackLayer.add(box, txt);
stackVisual.push({ box, label }); stackVisual.push({ box, txt });
stackLayer.draw(); stackLayer.draw();
} }
function popStack() { function popStack() {
const item = stackVisual.pop(); const it = stackVisual.pop();
if (item) { if (it) { it.box.destroy(); it.txt.destroy(); stackLayer.draw(); }
item.box.destroy();
item.label.destroy();
stackLayer.draw();
}
} }
function highlight(val, color = 'yellow') { // Highlight current node
const n = nodes[val]; function highlight(val) {
if (n) { for (const v in nodes) nodes[v].circle.fill('white');
n.circle.fill(color); if (val != null && nodes[val]) nodes[val].circle.fill('#ffc107');
layer.draw();
}
}
function resetHighlights() {
for (let v in nodes) {
nodes[v].circle.fill('white');
}
layer.draw(); layer.draw();
} }
function updateStatus(current, reason) { function updateStatus(current, reason) {
document.getElementById('currentVal').innerText = current ?? 'None'; document.getElementById('currentVal').innerText = current != null ? current : 'None';
document.getElementById('reason').innerText = reason; const reasonEl = document.getElementById('reason');
reasonEl.innerText = reason;
reasonEl.style.whiteSpace = 'normal';
reasonEl.style.display = 'inline-block';
reasonEl.style.maxWidth = '70%';
reasonEl.style.overflowWrap = 'break-word';
} }
function logVisit(val) { document.getElementById('seq').innerText += ` ${val}`; }
function logVisit(val) { // Traversal state
document.getElementById('seq').innerText += ` ${val}`;
}
//traversal state
let current = tree; let current = tree;
let lastVisited = null;
const stack = []; const stack = [];
let stepReady = true; let stepReady = true;
let lastVisited = null;
//final sequences for display // Final sequences for display
const finalSeqs = { const finalSeqs = {
'In-Order': '4 2 6 5 7 1 3 8 9', 'In-Order': '4 2 6 5 7 1 3 9 8',
'Pre-Order': '1 2 4 5 6 7 3 8 9', 'Pre-Order': '1 2 4 5 6 7 3 8 9',
'Post-Order': '4 6 7 5 2 9 8 3 1' 'Post-Order': '4 6 7 5 2 9 8 3 1'
}; };
//create tabs dynamically // Tabs
const modes = ['In-Order', 'Pre-Order', 'Post-Order']; const modes = ['In-Order', 'Pre-Order', 'Post-Order'];
let currentMode = modes[0]; let currentMode = 'In-Order';
const tabsDiv = document.createElement('div'); const tabsDiv = document.createElement('div');
tabsDiv.id = 'tabs'; tabsDiv.id = 'tabs'; tabsDiv.style.textAlign = 'center'; tabsDiv.style.margin = '10px';
tabsDiv.style.textAlign = 'center'; modes.forEach(mode => {
tabsDiv.style.margin = '10px';
modes.forEach((mode) => {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.innerText = mode; btn.innerText = mode; btn.className = 'tab-btn';
btn.className = 'tab-btn';
btn.style.margin = '0 5px';
btn.onclick = () => { btn.onclick = () => {
currentMode = mode; currentMode = mode;
document.querySelectorAll('.tab-btn').forEach((b) => (b.style.fontWeight = 'normal')); document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.style.fontWeight = 'bold'; btn.classList.add('active');
document.getElementById('final-seq').innerHTML = `<strong>Final Sequence (${mode}):</strong> ${finalSeqs[mode]}`; // Update final sequence display
finalSeqEl.innerHTML = `<strong>Final Sequence (${mode}):</strong> ${finalSeqs[mode]}`;
resetAll(); resetAll();
}; };
if (mode === currentMode) btn.style.fontWeight = 'bold'; if (mode === currentMode) btn.classList.add('active');
tabsDiv.appendChild(btn); tabsDiv.appendChild(btn);
}); });
document.body.insertBefore(tabsDiv, document.getElementById('main-container')); document.body.insertBefore(tabsDiv, document.getElementById('controls'));
document.getElementById('final-seq').innerHTML = `<strong>Final Sequence (${currentMode}):</strong> ${finalSeqs[currentMode]}`;
//step functions for each traversal //Initialize final sequence display
const finalSeqEl = document.getElementById('final-seq');
finalSeqEl.innerHTML = `<strong>Final Sequence (${currentMode}):</strong> ${finalSeqs[currentMode]}`;
// Step functions
function stepInOrder() { function stepInOrder() {
if (current) { if (current) {
highlight(current.val, 'yellow'); highlight(current.val);
updateStatus(current.val, `Push ${current.val} into stack`); updateStatus(current.val, `We have reached node ${current.val}. Push it onto the stack to remember it for later, then move to its left child to continue traversing the left subtree.`);
stack.push(current); stack.push(current); pushStack(current.val);
pushStack(current.val);
current = current.left; current = current.left;
stepReady = true; stepReady = true;
return; return;
} }
if (stack.length > 0) { if (stack.length) {
const peek = stack[stack.length - 1]; current = stack.pop(); popStack();
updateStatus(peek.val, `Peek stack (current = ${peek.val})`); highlight(current.val);
current = stack.pop(); updateStatus(current.val, `Left subtree complete. Now visit node ${current.val} and output it in the in-order sequence, then move to its right child to explore its right subtree.`);
popStack();
updateStatus(current.val, `Visit ${current.val}`);
highlight(current.val, 'orange');
logVisit(current.val); logVisit(current.val);
setTimeout(() => { setTimeout(() => { current = current.right; stepReady = true; }, 500);
resetHighlights();
current = current.right;
stepReady = true;
}, 500);
} else { } else {
updateStatus('None', 'Done'); highlight(null);
updateStatus(null, 'In-order traversal complete! All nodes have been visited.');
} }
} }
function stepPreOrder() { function stepPreOrder() {
if (current) { if (current) {
updateStatus(current.val, `Visit ${current.val}`); highlight(current.val);
highlight(current.val, 'orange'); updateStatus(current.val, `Output node ${current.val} as the next in pre-order sequence, then push it onto the stack so we can revisit its right subtree later, and move to its left child.`);
logVisit(current.val); logVisit(current.val);
updateStatus(current.val, `Push ${current.val} into stack`); stack.push(current); pushStack(current.val);
stack.push(current);
pushStack(current.val);
current = current.left; current = current.left;
stepReady = true; stepReady = true;
return; return;
} }
if (stack.length > 0) { if (stack.length) {
const node = stack.pop(); current = stack.pop(); popStack();
popStack(); highlight(current.val);
updateStatus(node.val, `Move to right of ${node.val}`); updateStatus(current.val, `Finished left subtree of node ${current.val}. Now proceed to explore its right subtree.`);
current = node.right; current = current.right;
stepReady = true;
} else { } else {
updateStatus('None', 'Done'); highlight(null);
updateStatus(null, 'Pre-order traversal complete! All nodes have been visited.');
} }
stepReady = true;
} }
function stepPostOrder() { function stepPostOrder() {
if (current) { if (current) {
highlight(current.val, 'yellow'); highlight(current.val);
updateStatus(current.val, `Push ${current.val} into stack`); updateStatus(current.val, `Push node ${current.val} onto the stack and move to its left child. In post-order, we process children before the node itself.`);
stack.push(current); stack.push(current); pushStack(current.val);
pushStack(current.val);
current = current.left; current = current.left;
stepReady = true; stepReady = true;
return; return;
} }
if (stack.length > 0) { if (stack.length) {
const peek = stack[stack.length - 1]; const peek = stack[stack.length - 1];
if (peek.right && lastVisited !== peek.right) { if (peek.right && lastVisited !== peek.right) {
updateStatus(peek.val, `Move to right of ${peek.val}`); updateStatus(peek.val, `Left subtree of node ${peek.val} done. Now move to its right subtree to continue processing children first.`);
current = peek.right; current = peek.right;
stepReady = true; stepReady = true;
} else { } else {
const node = stack.pop(); current = stack.pop(); popStack();
popStack(); highlight(current.val);
lastVisited = node; updateStatus(current.val, `Both children of node ${current.val} have been visited. Now output node ${current.val} for post-order sequence.`);
updateStatus(node.val, `Visit ${node.val}`); logVisit(current.val);
highlight(node.val, 'orange'); lastVisited = current;
logVisit(node.val); current = null;
current = null; // avoid re-visiting the same node
setTimeout(() => {
resetHighlights();
stepReady = true; stepReady = true;
}, 500);
} }
} else { } else {
updateStatus('None', 'Done'); highlight(null);
updateStatus(null, 'Post-order traversal complete! All nodes have been visited.');
} }
} }
//bind the step button // Handles step function
document.getElementById('stepBtn').onclick = () => { document.getElementById('stepBtn').onclick = () => {
if (!stepReady) return; if (!stepReady) return;
stepReady = false; stepReady = false;
@@ -256,7 +235,8 @@ document.getElementById('stepBtn').onclick = () => {
else if (currentMode === 'Post-Order') stepPostOrder(); else if (currentMode === 'Post-Order') stepPostOrder();
}; };
//reset button and function // Clear/Reset
document.getElementById('resetBtn').onclick = resetAll;
function resetAll() { function resetAll() {
while (stackVisual.length) popStack(); while (stackVisual.length) popStack();
stack.length = 0; stack.length = 0;
@@ -264,12 +244,8 @@ function resetAll() {
lastVisited = null; lastVisited = null;
stepReady = true; stepReady = true;
document.getElementById('seq').innerText = ''; document.getElementById('seq').innerText = '';
updateStatus('None', 'Ready'); updateStatus(null, 'Ready to begin traversal. Click "Step" to start.');
resetHighlights(); highlight(null);
} }
document.getElementById('resetBtn').onclick = resetAll;
//initial render of the tree
drawTree(tree);
layer.draw(); layer.draw();