227 lines
8.9 KiB
JavaScript
227 lines
8.9 KiB
JavaScript
|
|
/* ===== 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);
|
|||
|
|
}
|
|||
|
|
}
|