/* ===== 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 = ` ${val}
${idxLabels ? `${i}` : ''} ${tag ? `${tag}` : ''} `; 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); } }