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);
|
||
}
|
||
}
|