Files
illustrated-algorithm/shared/algo-viz.js
T
2026-08-24 04:35:13 +00:00

227 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===== Illustrated Algorithm – Shared Visualization Library ===== */
"use strict";
/* --- Utility --- */
const $ = id => document.getElementById(id);
const $$ = sel => document.querySelectorAll(sel);
/* --- StepController --- */
class StepController {
constructor(steps, opts = {}) {
this.steps = steps;
this.idx = -1;
this.autoTimer = null;
this.onRender = opts.onRender || (() => {});
this.onDone = opts.onDone || (() => {});
this.autoDelay = opts.autoDelay || 800;
}
get current() { return this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null; }
get isEnd() { return this.idx >= this.steps.length - 1; }
get total() { return this.steps.length; }
next() {
if (this.idx < this.steps.length - 1) { this.idx++; this._render(); }
else this.stopAuto();
}
prev() {
if (this.idx > 0) { this.idx--; this._render(); }
}
jumpEnd() {
this.idx = this.steps.length - 1;
this._render();
this.stopAuto();
}
jumpTo(i) {
if (i >= 0 && i < this.steps.length) { this.idx = i; this._render(); }
}
reset() {
this.idx = -1;
this.stopAuto();
this._renderPipeline();
if (this.onRender) this.onRender(null);
}
startAuto() {
this.stopAuto();
if (this.idx < 0) this.idx = 0;
this._render();
this.autoTimer = setInterval(() => {
if (this.idx < this.steps.length - 1) { this.idx++; this._render(); }
else { this.stopAuto(); if (this.onDone) this.onDone(); }
}, this.autoDelay);
}
stopAuto() {
if (this.autoTimer) { clearInterval(this.autoTimer); this.autoTimer = null; }
}
_render() {
if (this.onRender) this.onRender(this.current);
this._renderPipeline();
}
_renderPipeline() {
const el = $('pipeline');
if (!el) return;
el.innerHTML = '';
for (let i = 0; i < this.steps.length; i++) {
const d = document.createElement('div');
d.className = 'step-dot' + (i < this.idx ? ' done' : i === this.idx ? ' active' : '');
d.title = `Step ${i + 1}`;
d.style.cursor = 'pointer';
d.addEventListener('click', () => this.jumpTo(i));
el.appendChild(d);
}
}
}
/* --- Render helpers --- */
function renderArray(container, items, opts = {}) {
const { barMaxH = 120, barMinW = 28, barGap = 4, classes = [], tags = [], idxLabels = true } = opts;
const maxVal = Math.max(...items.map(it => typeof it === 'object' ? it.val : it), 1);
container.innerHTML = '';
const row = document.createElement('div');
row.className = 'arr-row';
items.forEach((item, i) => {
const val = typeof item === 'object' ? item.val : item;
const cls = typeof item === 'object' ? (item.cls || '') : (classes[i] || '');
const tag = typeof item === 'object' ? (item.tag || '') : (tags[i] || '');
const h = Math.max(8, (val / maxVal) * barMaxH);
const cell = document.createElement('div');
cell.className = 'arr-cell';
cell.innerHTML = `
<span class="arr-val">${val}</span>
<div class="arr-bar ${cls}" style="height:${h}px;width:${barMinW}px;"></div>
${idxLabels ? `<span class="arr-idx">${i}</span>` : ''}
${tag ? `<span class="tag ${cls === 'min-price' || cls === 'best' ? 'tag-green' : cls === 'current' ? 'tag-orange' : cls === 'checking' ? 'tag-blue' : 'tag-orange'}">${tag}</span>` : ''}
`;
row.appendChild(cell);
});
container.appendChild(row);
}
function renderGrid(container, rows, cols, cellData, opts = {}) {
/* cellData: 2D array of {val, cls} or null */
const { cellSize = 48 } = opts;
container.innerHTML = '';
const grid = document.createElement('div');
grid.className = 'grid-vis';
grid.style.gridTemplateColumns = `repeat(${cols}, ${cellSize}px)`;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const d = cellData && cellData[i] && cellData[i][j] ? cellData[i][j] : { val: '', cls: '' };
const cell = document.createElement('div');
cell.className = 'grid-cell ' + (d.cls || '');
cell.textContent = d.val !== undefined && d.val !== null ? d.val : '';
grid.appendChild(cell);
}
}
container.appendChild(grid);
}
function renderCode(container, code, highlightLine = -1) {
container.innerHTML = '';
const pre = document.createElement('pre');
pre.className = 'code-block';
const lines = code.split('\n');
lines.forEach((line, i) => {
if (i === highlightLine) {
const span = document.createElement('span');
span.className = 'hl';
span.textContent = line;
pre.appendChild(span);
} else {
pre.appendChild(document.createTextNode(line + '\n'));
}
});
container.appendChild(pre);
}
function renderLinkedList(container, nodes, opts = {}) {
/* nodes: [{val, cls}] */
container.innerHTML = '';
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:center;';
nodes.forEach((n, i) => {
const box = document.createElement('div');
box.style.cssText = `padding:6px 12px;border-radius:6px;font-family:var(--mono);font-size:.85rem;border:2px solid var(--border);background:var(--surface2);transition:all .2s;${n.cls === 'highlight' ? 'border-color:var(--orange);background:var(--orange-dim);' : n.cls === 'done' ? 'border-color:var(--green);background:var(--green-dim);' : ''}`;
box.textContent = n.val;
row.appendChild(box);
if (i < nodes.length - 1) {
const arrow = document.createElement('span');
arrow.textContent = '→';
arrow.style.cssText = 'color:var(--text2);font-size:1.1rem;';
row.appendChild(arrow);
}
});
container.appendChild(row);
}
function renderBinaryTree(container, tree, opts = {}) {
/* simple SVG tree rendering */
container.innerHTML = '';
if (!tree) return;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 500 280');
svg.style.width = '100%';
svg.style.maxWidth = '500px';
const nodeEls = [];
function draw(node, x, y, spread, depth) {
if (!node) return;
const leftSpread = spread / 2;
if (node.left) {
const lx = x - leftSpread, ly = y + 60;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x); line.setAttribute('y1', y);
line.setAttribute('x2', lx); line.setAttribute('y2', ly);
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
svg.appendChild(line);
draw(node.left, lx, ly, leftSpread, depth + 1);
}
if (node.right) {
const rx = x + leftSpread, ry = y + 60;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x); line.setAttribute('y1', y);
line.setAttribute('x2', rx); line.setAttribute('y2', ry);
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
svg.appendChild(line);
draw(node.right, rx, ry, leftSpread, depth + 1);
}
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', x); circle.setAttribute('cy', y); circle.setAttribute('r', '18');
const cls = node.cls || '';
circle.setAttribute('fill', cls === 'highlight' ? '#f5a623' : cls === 'done' ? '#3dd68c' : '#242836');
circle.setAttribute('stroke', cls === 'highlight' ? '#f5a623' : cls === 'done' ? '#3dd68c' : '#6c7eff');
circle.setAttribute('stroke-width', '2');
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x); text.setAttribute('y', y + 5);
text.setAttribute('text-anchor', 'middle'); text.setAttribute('fill', '#e4e6f0');
text.setAttribute('font-size', '12'); text.setAttribute('font-family', 'var(--mono)');
text.textContent = node.val;
g.appendChild(circle); g.appendChild(text);
svg.appendChild(g);
}
draw(tree, 250, 30, 200, 0);
container.appendChild(svg);
}
function renderStack(container, items, opts = {}) {
container.innerHTML = '';
const stack = document.createElement('div');
stack.style.cssText = 'display:flex;flex-direction:column-reverse;align-items:center;gap:3px;';
items.forEach((item, i) => {
const d = typeof item === 'object' ? item : { val: item, cls: '' };
const el = document.createElement('div');
el.style.cssText = `padding:6px 20px;border-radius:4px;font-family:var(--mono);font-size:.82rem;border:1px solid var(--border);background:var(--surface2);min-width:60px;text-align:center;${d.cls === 'top' ? 'border-color:var(--orange);background:var(--orange-dim);' : ''}`;
el.textContent = d.val;
stack.appendChild(el);
});
container.appendChild(stack);
}
function highlightRange(container, start, end, cls) {
/* highlight child elements in range [start, end) with class */
const children = container.children;
for (let i = 0; i < children.length; i++) {
if (i >= start && i < end) children[i].classList.add(cls);
else children[i].classList.remove(cls);
}
}