This commit is contained in:
+358
-48
@@ -1,24 +1,54 @@
|
||||
/* ===== Illustrated Algorithm – Shared Visualization Library ===== */
|
||||
/* Supports BOTH API styles:
|
||||
* Style A (string-returning): renderArray(arr, {highlights}) → HTML string
|
||||
* Style B (DOM-mutating): renderArray(container, items, opts) → mutates container
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/* --- Utility --- */
|
||||
const $ = id => document.getElementById(id);
|
||||
const $$ = sel => document.querySelectorAll(sel);
|
||||
|
||||
/* --- StepController --- */
|
||||
/* --- CSS variable defaults --- */
|
||||
const _CSS = {
|
||||
blue: '#3b82f6', green: '#16a34a', purple: '#8b5cf6',
|
||||
red: '#ef4444', orange: '#f59e0b', cyan: '#06b6d4',
|
||||
text: '#0f172a', text2: '#475569', muted: '#94a3b8',
|
||||
border: '#e2e8f0', surface2: '#f1f5f9',
|
||||
mono: 'Menlo,Consolas,monospace',
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
StepController — supports BOTH constructor signatures:
|
||||
new StepController(opts) → Style A (deferred steps)
|
||||
new StepController(steps, opts) → Style B (immediate steps)
|
||||
================================================================ */
|
||||
class StepController {
|
||||
constructor(steps, opts = {}) {
|
||||
this.steps = steps;
|
||||
constructor(stepsOrOpts, maybeOpts) {
|
||||
// Detect which style
|
||||
if (Array.isArray(stepsOrOpts)) {
|
||||
// Style B: new StepController(steps, opts)
|
||||
this.steps = stepsOrOpts;
|
||||
this.opts = maybeOpts || {};
|
||||
} else {
|
||||
// Style A: new StepController(opts)
|
||||
this.steps = [];
|
||||
this.opts = stepsOrOpts || {};
|
||||
}
|
||||
this.idx = -1;
|
||||
this.autoTimer = null;
|
||||
this.onRender = opts.onRender || (() => {});
|
||||
this.onDone = opts.onDone || (() => {});
|
||||
this.autoDelay = opts.autoDelay || 800;
|
||||
this.autoDelay = this.opts.autoDelay || this.opts.autoInterval || 800;
|
||||
this.onRender = this.opts.onRender || this.opts.onStep || (() => {});
|
||||
this.onDone = this.opts.onDone || this.opts.onComplete || (() => {});
|
||||
this.stepCounterEl = this.opts.stepCounterEl || null;
|
||||
}
|
||||
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; }
|
||||
|
||||
/* --- Style A helpers --- */
|
||||
setSteps(steps) { this.steps = steps; this.idx = -1; this.updateCounter(); }
|
||||
set onStep(fn) { this.onRender = fn; }
|
||||
get onStep() { return this.onRender; }
|
||||
|
||||
/* --- Navigation --- */
|
||||
next() {
|
||||
if (this.idx < this.steps.length - 1) { this.idx++; this._render(); }
|
||||
else this.stopAuto();
|
||||
@@ -26,7 +56,8 @@ class StepController {
|
||||
prev() {
|
||||
if (this.idx > 0) { this.idx--; this._render(); }
|
||||
}
|
||||
jumpEnd() {
|
||||
jumpToEnd() { this.jumpEnd(); } // Style A alias
|
||||
jumpEnd() { // Style B name
|
||||
this.idx = this.steps.length - 1;
|
||||
this._render();
|
||||
this.stopAuto();
|
||||
@@ -39,6 +70,14 @@ class StepController {
|
||||
this.stopAuto();
|
||||
this._renderPipeline();
|
||||
if (this.onRender) this.onRender(null);
|
||||
this.updateCounter();
|
||||
}
|
||||
|
||||
/* --- Auto-play --- */
|
||||
toggleAuto() { // Style A name
|
||||
if (this.autoTimer) { this.stopAuto(); return false; }
|
||||
this.startAuto();
|
||||
return true;
|
||||
}
|
||||
startAuto() {
|
||||
this.stopAuto();
|
||||
@@ -52,28 +91,102 @@ class StepController {
|
||||
stopAuto() {
|
||||
if (this.autoTimer) { clearInterval(this.autoTimer); this.autoTimer = null; }
|
||||
}
|
||||
|
||||
/* --- Internal --- */
|
||||
_render() {
|
||||
if (this.onRender) this.onRender(this.current);
|
||||
const step = this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null;
|
||||
if (this.onRender) {
|
||||
// Style A pages pass (index, step), Style B passes just step
|
||||
// Try calling with both and let the page's handler decide
|
||||
try { this.onRender(this.idx, step); } catch(e) {
|
||||
try { this.onRender(step); } catch(e2) {}
|
||||
}
|
||||
}
|
||||
this._renderPipeline();
|
||||
this.updateCounter();
|
||||
}
|
||||
_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);
|
||||
// Don't overwrite if the page manages its own pipeline HTML
|
||||
// Only render step-dots if pipeline is empty or previously rendered by us
|
||||
if (el.dataset.managed === 'page') return;
|
||||
}
|
||||
updateCounter() {
|
||||
if (this.stepCounterEl) {
|
||||
this.stepCounterEl.textContent = `${this.idx + 1} / ${this.steps.length}`;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- State queries --- */
|
||||
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; }
|
||||
isFirst() { return this.idx <= 0; }
|
||||
isLast() { return this.idx >= this.steps.length - 1; }
|
||||
}
|
||||
|
||||
/* --- Render helpers --- */
|
||||
function renderArray(container, items, opts = {}) {
|
||||
const { barMaxH = 120, barMinW = 28, barGap = 4, classes = [], tags = [], idxLabels = true } = opts;
|
||||
/* ================================================================
|
||||
renderArray — supports BOTH signatures:
|
||||
Style A: renderArray(arr, {highlights, pointers, indices, ...}) → HTML string
|
||||
Style B: renderArray(container, items, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderArray(a, b, c) {
|
||||
// Style B: first arg is DOM element
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderArrayB(a, b, c);
|
||||
}
|
||||
// Style A: first arg is plain array, returns HTML string
|
||||
return _renderArrayA(a, b);
|
||||
}
|
||||
|
||||
/* Style A: returns HTML string */
|
||||
function _renderArrayA(arr, options = {}) {
|
||||
const {
|
||||
highlights = {},
|
||||
pointers = {},
|
||||
indices = true,
|
||||
separator = null,
|
||||
customClass = '',
|
||||
width = null,
|
||||
} = options;
|
||||
|
||||
let html = '<div class="nums-line ' + customClass + '">';
|
||||
arr.forEach((val, i) => {
|
||||
const hlClass = highlights[i] || 'default';
|
||||
const widthStyle = width ? `width:${width}px;` : '';
|
||||
html += '<span class="chip-group">';
|
||||
html += `<span class="chip ${hlClass}" style="${widthStyle}">${val}</span>`;
|
||||
if (indices) html += `<span class="chip-index">${i}</span>`;
|
||||
html += '</span>';
|
||||
if (separator !== null && i === separator - 1) {
|
||||
html += '<span class="split">|</span>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
if (Object.keys(pointers).length > 0) {
|
||||
html += '<div class="nums-line" style="margin-top:-4px;">';
|
||||
const w = width || 40;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const label = Object.entries(pointers).find(([_, idx]) => idx === i);
|
||||
if (label) {
|
||||
html += `<span class="pointer-arrow" style="min-width:${w}px;">${label[0]}</span>`;
|
||||
} else {
|
||||
html += `<span style="min-width:${w}px;"></span>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/* Style B: mutates container */
|
||||
function _renderArrayB(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');
|
||||
@@ -96,8 +209,45 @@ function renderArray(container, items, opts = {}) {
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function renderGrid(container, rows, cols, cellData, opts = {}) {
|
||||
/* cellData: 2D array of {val, cls} or null */
|
||||
/* ================================================================
|
||||
renderGrid — supports BOTH:
|
||||
Style A: renderGrid(matrix, {highlights, cellClass, ...}) → HTML string
|
||||
Style B: renderGrid(container, rows, cols, cellData, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderGrid(a, b, c, d, e) {
|
||||
// Style B: first arg is DOM element
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderGridB(a, b, c, d, e);
|
||||
}
|
||||
// Style A: first arg is 2D array
|
||||
return _renderGridA(a, b);
|
||||
}
|
||||
|
||||
function _renderGridA(matrix, options = {}) {
|
||||
const {
|
||||
cellClass = (val, r, c) => '',
|
||||
cellStyle = (val, r, c) => '',
|
||||
highlights = {},
|
||||
cellSize = 40
|
||||
} = options;
|
||||
const rows = matrix.length;
|
||||
const cols = matrix[0]?.length || 0;
|
||||
let html = `<div class="grid-viz" style="grid-template-columns:repeat(${cols}, ${cellSize}px);">`;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const val = matrix[r][c];
|
||||
const key = `${r},${c}`;
|
||||
const hlc = highlights[key] || '';
|
||||
const extraCls = cellClass(val, r, c);
|
||||
const extraStyle = cellStyle(val, r, c);
|
||||
html += `<div class="grid-cell ${extraCls} ${hlc}" style="width:${cellSize}px;height:${cellSize}px;${extraStyle}">${val}</div>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderGridB(container, rows, cols, cellData, opts = {}) {
|
||||
const { cellSize = 48 } = opts;
|
||||
container.innerHTML = '';
|
||||
const grid = document.createElement('div');
|
||||
@@ -115,7 +265,35 @@ function renderGrid(container, rows, cols, cellData, opts = {}) {
|
||||
container.appendChild(grid);
|
||||
}
|
||||
|
||||
function renderCode(container, code, highlightLine = -1) {
|
||||
/* ================================================================
|
||||
renderCode — supports BOTH:
|
||||
Style A: renderCode(code, {lang, highlightLines}) → HTML string
|
||||
Style B: renderCode(container, code, highlightLine) → mutates container
|
||||
================================================================ */
|
||||
function renderCode(a, b, c) {
|
||||
// Style B: first arg is DOM element
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderCodeB(a, b, c);
|
||||
}
|
||||
// Style A: first arg is string
|
||||
return _renderCodeA(a, b);
|
||||
}
|
||||
|
||||
function _renderCodeA(code, options = {}) {
|
||||
const { highlightLines = [], lang = '' } = options;
|
||||
const lines = code.split('\n');
|
||||
let html = `<div class="code-block">`;
|
||||
if (lang) html += `<div class="lang-label">${lang}</div>`;
|
||||
html += `<pre><code>`;
|
||||
lines.forEach((line, i) => {
|
||||
const cls = highlightLines.includes(i + 1) ? 'highlight' : '';
|
||||
html += `<span class="code-line ${cls}">${_escapeHTML(line)}</span>`;
|
||||
});
|
||||
html += '</code></pre></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderCodeB(container, code, highlightLine = -1) {
|
||||
container.innerHTML = '';
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'code-block';
|
||||
@@ -133,55 +311,139 @@ function renderCode(container, code, highlightLine = -1) {
|
||||
container.appendChild(pre);
|
||||
}
|
||||
|
||||
function renderLinkedList(container, nodes, opts = {}) {
|
||||
/* nodes: [{val, cls}] */
|
||||
function _escapeHTML(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
renderLinkedList — supports BOTH:
|
||||
Style A: renderLinkedList(nodes, {highlights}) → HTML string
|
||||
Style B: renderLinkedList(container, nodes, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderLinkedList(a, b, c) {
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderLinkedListB(a, b, c);
|
||||
}
|
||||
return _renderLinkedListA(a, b);
|
||||
}
|
||||
|
||||
function _renderLinkedListA(nodes, options = {}) {
|
||||
const { highlights = {}, showNext = true, nullLabel = 'NULL' } = options;
|
||||
let html = '<div class="linked-list">';
|
||||
nodes.forEach((node, i) => {
|
||||
const cls = highlights[i] || '';
|
||||
html += `<div class="ll-node ${cls}">`;
|
||||
html += `<span class="val">${node}</span>`;
|
||||
if (showNext) {
|
||||
html += cls === 'null-node'
|
||||
? `<span class="arrow">∅</span>`
|
||||
: `<span class="arrow">→</span>`;
|
||||
}
|
||||
html += '</div>';
|
||||
if (i < nodes.length - 1 && showNext) {
|
||||
html += '<span class="ll-arrow">→</span>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderLinkedListB(container, nodes, opts = {}) {
|
||||
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.style.cssText = `padding:6px 12px;border-radius:6px;font-family:${_CSS.mono};font-size:.85rem;border:2px solid ${_CSS.border};background:${_CSS.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;';
|
||||
arrow.style.cssText = `color:${_CSS.text2};font-size:1.1rem;`;
|
||||
row.appendChild(arrow);
|
||||
}
|
||||
});
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function renderBinaryTree(container, tree, opts = {}) {
|
||||
/* simple SVG tree rendering */
|
||||
/* ================================================================
|
||||
renderBinaryTree — supports BOTH:
|
||||
Style A: renderBinaryTree(root, {highlights}) → HTML string
|
||||
Style B: renderBinaryTree(container, tree, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderBinaryTree(a, b, c) {
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderBinaryTreeB(a, b, c);
|
||||
}
|
||||
return _renderBinaryTreeA(a, b);
|
||||
}
|
||||
|
||||
function _renderBinaryTreeA(root, options = {}) {
|
||||
const { highlights = {}, maxWidth = 900 } = options;
|
||||
if (!root) return '<div class="tree-container"><em>空树</em></div>';
|
||||
function buildLevels(node) {
|
||||
const levels = [];
|
||||
const queue = [node];
|
||||
while (queue.length) {
|
||||
const level = [];
|
||||
const nextQueue = [];
|
||||
let hasNonNull = false;
|
||||
for (const n of queue) {
|
||||
if (n) { level.push(n.val); nextQueue.push(n.left, n.right); if (n.left || n.right) hasNonNull = true; }
|
||||
else { level.push(null); nextQueue.push(null, null); }
|
||||
}
|
||||
levels.push(level);
|
||||
queue.length = 0;
|
||||
if (hasNonNull) queue.push(...nextQueue);
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
const levels = buildLevels(root);
|
||||
let html = `<div class="tree-container"><div style="max-width:${maxWidth}px;width:100%;">`;
|
||||
levels.forEach((level, li) => {
|
||||
const gap = Math.max(4, 60 / (li + 1));
|
||||
html += `<div style="display:flex;justify-content:center;gap:${gap}px;margin-bottom:${li === 0 ? 0 : 12}px;">`;
|
||||
level.forEach(val => {
|
||||
if (val === null) { html += `<div style="width:40px;height:40px;"></div>`; }
|
||||
else {
|
||||
const cls = highlights[val] || '';
|
||||
html += `<div class="tree-node"><div class="node-circle ${cls}">${val}</div></div>`;
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderBinaryTreeB(container, tree, opts = {}) {
|
||||
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) {
|
||||
function draw(node, x, y, spread) {
|
||||
if (!node) return;
|
||||
const leftSpread = spread / 2;
|
||||
const ls = spread / 2;
|
||||
if (node.left) {
|
||||
const lx = x - leftSpread, ly = y + 60;
|
||||
const lx = x - ls, 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);
|
||||
draw(node.left, lx, ly, ls);
|
||||
}
|
||||
if (node.right) {
|
||||
const rx = x + leftSpread, ry = y + 60;
|
||||
const rx = x + ls, 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);
|
||||
draw(node.right, rx, ry, ls);
|
||||
}
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
@@ -193,34 +455,82 @@ function renderBinaryTree(container, tree, opts = {}) {
|
||||
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.setAttribute('font-size', '12'); text.setAttribute('font-family', _CSS.mono);
|
||||
text.textContent = node.val;
|
||||
g.appendChild(circle); g.appendChild(text);
|
||||
svg.appendChild(g);
|
||||
}
|
||||
draw(tree, 250, 30, 200, 0);
|
||||
draw(tree, 250, 30, 200);
|
||||
container.appendChild(svg);
|
||||
}
|
||||
|
||||
function renderStack(container, items, opts = {}) {
|
||||
/* ================================================================
|
||||
renderStack — supports BOTH:
|
||||
Style A: renderStack(items, {topIndex, poppedIndex}) → HTML string
|
||||
Style B: renderStack(container, items, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderStack(a, b, c) {
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderStackB(a, b, c);
|
||||
}
|
||||
return _renderStackA(a, b);
|
||||
}
|
||||
|
||||
function _renderStackA(items, options = {}) {
|
||||
const { topIndex = items.length - 1, poppedIndex = -1 } = options;
|
||||
let html = '<div class="stack-container">';
|
||||
if (items.length === 0) html += '<span style="color:var(--text-muted);font-style:italic;">空栈</span>';
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
let cls = '';
|
||||
if (i === topIndex) cls = 'top';
|
||||
if (i === poppedIndex) cls = 'popped';
|
||||
html += `<div class="stack-item ${cls}">${items[i]}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderStackB(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.style.cssText = `padding:6px 20px;border-radius:4px;font-family:${_CSS.mono};font-size:.82rem;border:1px solid ${_CSS.border};background:${_CSS.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);
|
||||
/* ================================================================
|
||||
highlightRange — returns an highlights object for array indices
|
||||
================================================================ */
|
||||
function highlightRange(arr, start, end, cls = 'active') {
|
||||
const highlights = {};
|
||||
for (let i = start; i <= end && i < arr.length; i++) {
|
||||
highlights[i] = cls;
|
||||
}
|
||||
return highlights;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
escapeHTML — utility
|
||||
================================================================ */
|
||||
function escapeHTML(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/* --- Global exports --- */
|
||||
window.StepController = StepController;
|
||||
window.renderArray = renderArray;
|
||||
window.renderLinkedList = renderLinkedList;
|
||||
window.renderBinaryTree = renderBinaryTree;
|
||||
window.renderGrid = renderGrid;
|
||||
window.renderStack = renderStack;
|
||||
window.renderCode = renderCode;
|
||||
window.highlightRange = highlightRange;
|
||||
window.escapeHTML = escapeHTML;
|
||||
window.$ = $;
|
||||
window.$$ = $$;
|
||||
|
||||
+390
-134
@@ -1,150 +1,406 @@
|
||||
/* ===== Illustrated Algorithm – Shared Styles ===== */
|
||||
/* ============================================================
|
||||
LeetCode Hot 100 图解 — 公共样式
|
||||
浅色渐变卡片风格 + 深色主题兼容
|
||||
============================================================ */
|
||||
|
||||
/* ---------- CSS Variables (Light Theme — Default) ---------- */
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--surface2: #242836;
|
||||
--border: #2e3347;
|
||||
--text: #e4e6f0;
|
||||
--text2: #9499b3;
|
||||
--accent: #6c7eff;
|
||||
--accent2: #5a6adf;
|
||||
--green: #3dd68c;
|
||||
--green-dim: rgba(61,214,140,.15);
|
||||
--red: #f06060;
|
||||
--red-dim: rgba(240,96,96,.15);
|
||||
--orange: #f5a623;
|
||||
--orange-dim: rgba(245,166,35,.15);
|
||||
--blue: #4da6ff;
|
||||
--blue-dim: rgba(77,166,255,.15);
|
||||
--purple: #b47aff;
|
||||
--purple-dim: rgba(180,122,255,.15);
|
||||
--cyan: #22d3ee;
|
||||
--yellow: #facc15;
|
||||
--font: 'Inter','SF Pro Display',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||||
--mono: 'JetBrains Mono','Fira Code','SF Mono',Menlo,Consolas,monospace;
|
||||
--radius: 8px;
|
||||
--shadow: 0 2px 12px rgba(0,0,0,.35);
|
||||
/* Light theme vars (used by Style A pages) */
|
||||
--blue: #3b82f6; --blue-light: #dbeafe; --blue-dark: #1e40af;
|
||||
--green: #16a34a; --green-light: #dcfce7; --green-dark: #166534;
|
||||
--purple: #8b5cf6; --purple-light: #ede9fe; --purple-dark: #5b21b6;
|
||||
--red: #ef4444; --red-light: #fee2e2; --red-dark: #991b1b;
|
||||
--orange: #f59e0b; --orange-light: #fff7ed; --orange-dark: #9a3412;
|
||||
--cyan: #06b6d4; --cyan-light: #cffafe;
|
||||
--bg: #f1f5f9; --card: #ffffff; --border: #e2e8f0;
|
||||
--text: #0f172a; --text-secondary: #475569; --text-muted: #94a3b8;
|
||||
--surface: #ffffff; --surface2: #f1f5f9;
|
||||
--accent: #3b82f6; --accent2: #2563eb;
|
||||
--radius: 12px; --radius-lg: 16px;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||||
--transition: 0.25s ease;
|
||||
--mono: 'Menlo','Consolas','JetBrains Mono','Fira Code',monospace;
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
|
||||
/* Dark theme compatible vars (used by Style B pages) */
|
||||
--green-dim: rgba(22,163,74,.15);
|
||||
--red-dim: rgba(239,68,68,.15);
|
||||
--orange-dim: rgba(245,158,11,.15);
|
||||
--blue-dim: rgba(59,130,246,.15);
|
||||
--purple-dim: rgba(139,92,246,.15);
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
html{font-size:15px;-webkit-font-smoothing:antialiased;}
|
||||
body{background:var(--bg);color:var(--text);font-family:var(--font);line-height:1.6;padding:0 0 48px;}
|
||||
.container{max-width:1080px;margin:0 auto;padding:24px 20px;}
|
||||
h1{font-size:1.7rem;font-weight:700;margin-bottom:6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
|
||||
.badge{font-size:.7rem;font-weight:600;padding:2px 10px;border-radius:20px;letter-spacing:.5px;text-transform:uppercase;vertical-align:middle;}
|
||||
.badge.easy{background:var(--green-dim);color:var(--green);}
|
||||
.badge.medium{background:var(--orange-dim);color:var(--orange);}
|
||||
.badge.hard{background:var(--red-dim);color:var(--red);}
|
||||
.subtitle{color:var(--text2);font-size:.85rem;margin-bottom:18px;}
|
||||
|
||||
/* Controls */
|
||||
.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:14px;background:var(--surface);padding:10px 14px;border-radius:var(--radius);border:1px solid var(--border);}
|
||||
.controls label{font-size:.82rem;color:var(--text2);}
|
||||
.controls input[type=text]{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:.82rem;width:180px;}
|
||||
.controls select{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:5px 8px;border-radius:6px;font-size:.82rem;}
|
||||
.controls button{padding:6px 14px;border:none;border-radius:6px;font-size:.82rem;cursor:pointer;font-weight:600;transition:all .15s;}
|
||||
.controls button.primary{background:var(--accent);color:#fff;}
|
||||
.controls button.primary:hover{background:var(--accent2);}
|
||||
.controls button:not(.primary){background:var(--surface2);color:var(--text);border:1px solid var(--border);}
|
||||
.controls button:not(.primary):hover{background:var(--border);}
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
/* Pipeline */
|
||||
.pipeline{display:flex;gap:3px;margin-bottom:10px;flex-wrap:wrap;}
|
||||
.pipeline .step-dot{width:28px;height:6px;border-radius:3px;background:var(--surface2);transition:background .2s;}
|
||||
.pipeline .step-dot.done{background:var(--accent);}
|
||||
.pipeline .step-dot.active{background:var(--orange);}
|
||||
body {
|
||||
font-family: var(--font);
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e9f2 50%, #dfe6ed 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Hint */
|
||||
.hint{padding:10px 14px;border-radius:var(--radius);margin-bottom:14px;font-size:.85rem;line-height:1.6;}
|
||||
.hint.info{background:var(--blue-dim);color:var(--blue);border:1px solid rgba(77,166,255,.25);}
|
||||
.hint #stepInfo{font-weight:700;}
|
||||
/* ---------- Layout ---------- */
|
||||
.container { max-width: 1440px; margin: 0 auto; padding: 20px; }
|
||||
|
||||
/* Panels */
|
||||
.panels{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:14px;}
|
||||
.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;}
|
||||
.panel h3{font-size:.9rem;font-weight:700;margin-bottom:10px;color:var(--text2);}
|
||||
.panel-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
||||
@media(max-width:700px){.panel-grid{grid-template-columns:1fr;}}
|
||||
/* ---------- Typography ---------- */
|
||||
h1 { font-size: 25px; margin: 0 0 8px; }
|
||||
h2 { font-size: 20px; margin: 0 0 12px; color: #1e293b; }
|
||||
h3 { margin: 0 0 10px; font-size: 16px; }
|
||||
.subtitle { color: var(--text-secondary); margin-bottom: 16px; font-size: 1.1rem; }
|
||||
|
||||
/* Vis area */
|
||||
.vis-area{min-height:120px;position:relative;}
|
||||
.vis-area svg{display:block;margin:0 auto;}
|
||||
/* ---------- Badge ---------- */
|
||||
.badge {
|
||||
display: inline-block; padding: 2px 10px; border-radius: 999px;
|
||||
font-size: 12px; font-weight: 600; margin-left: 8px; vertical-align: middle;
|
||||
}
|
||||
.badge.easy { background: #dcfce7; color: #166534; }
|
||||
.badge.medium { background: #fef3c7; color: #92400e; }
|
||||
.badge.hard { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
/* Array rendering */
|
||||
.arr-row{display:flex;align-items:flex-end;gap:4px;justify-content:center;margin:8px 0;}
|
||||
.arr-cell{display:flex;flex-direction:column;align-items:center;min-width:36px;}
|
||||
.arr-val{font-family:var(--mono);font-size:.82rem;padding:3px 0;color:var(--text);}
|
||||
.arr-bar{width:30px;border-radius:4px 4px 0 0;background:var(--accent);transition:all .25s;}
|
||||
.arr-idx{font-family:var(--mono);font-size:.68rem;color:var(--text2);margin-top:2px;}
|
||||
.arr-bar.min-price{background:var(--green);box-shadow:0 0 8px var(--green);}
|
||||
.arr-bar.current{background:var(--orange);box-shadow:0 0 8px var(--orange);}
|
||||
.arr-bar.checking{background:var(--purple);box-shadow:0 0 6px var(--purple);}
|
||||
.arr-bar.covered{background:var(--blue);opacity:.7;}
|
||||
.arr-bar.best{background:var(--green);box-shadow:0 0 8px var(--green);}
|
||||
.arr-cell .tag{font-size:.6rem;font-weight:700;margin-top:1px;}
|
||||
/* ---------- Controls ---------- */
|
||||
.controls {
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.controls label { font-size: .82rem; color: var(--text-secondary); }
|
||||
.controls input, .controls select {
|
||||
padding: 8px 10px; border: 1px solid #cbd5e1; border-radius: 8px;
|
||||
font-family: var(--mono); font-size: 14px; background: #f8fafc; color: var(--text);
|
||||
}
|
||||
.controls input:focus, .controls select:focus { outline: 2px solid var(--blue); outline-offset: -1px; }
|
||||
.controls input[type="text"] { width: 260px; }
|
||||
.controls button {
|
||||
padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 8px;
|
||||
background: #f8fafc; font-size: 14px; cursor: pointer; color: var(--text);
|
||||
transition: background var(--transition), transform 0.1s;
|
||||
}
|
||||
.controls button:hover { background: #e2e8f0; }
|
||||
.controls button:active { transform: scale(0.97); }
|
||||
.controls button:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.controls button.primary {
|
||||
background: var(--blue); color: white; border-color: var(--blue);
|
||||
}
|
||||
.controls button.primary:hover { background: #2563eb; }
|
||||
|
||||
/* Grid rendering */
|
||||
.grid-vis{display:inline-grid;gap:3px;margin:8px auto;}
|
||||
.grid-cell{width:48px;height:48px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:.82rem;border-radius:4px;background:var(--surface2);border:1px solid var(--border);transition:all .25s;position:relative;}
|
||||
.grid-cell.highlight{background:var(--accent);color:#fff;border-color:var(--accent);}
|
||||
.grid-cell.path{background:var(--green);color:#fff;border-color:var(--green);box-shadow:0 0 6px var(--green);}
|
||||
.grid-cell.computing{background:var(--orange-dim);border-color:var(--orange);}
|
||||
.grid-cell.origin{background:var(--purple-dim);border-color:var(--purple);}
|
||||
/* ---------- Pipeline / Step Indicator ---------- */
|
||||
.pipeline {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
||||
background: #eef2ff; border-radius: var(--radius); padding: 10px 16px; margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.pipe-step {
|
||||
background: white; border: 1px solid #c7d2fe; border-radius: 999px;
|
||||
padding: 4px 10px; color: #3730a3; transition: all var(--transition);
|
||||
}
|
||||
.pipe-step.active {
|
||||
background: #4f46e5; color: white; border-color: #4f46e5; font-weight: 600;
|
||||
box-shadow: 0 0 0 3px rgba(79,70,229,0.2);
|
||||
}
|
||||
.pipeline i { color: var(--text-muted); font-style: normal; }
|
||||
|
||||
/* String rendering */
|
||||
.str-row{display:flex;gap:2px;justify-content:center;margin:8px 0;flex-wrap:wrap;}
|
||||
.str-cell{width:32px;height:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:var(--mono);font-size:.9rem;border-radius:4px;background:var(--surface2);border:2px solid transparent;transition:all .2s;}
|
||||
.str-cell.center{border-color:var(--orange);background:var(--orange-dim);}
|
||||
.str-cell.expand{border-color:var(--purple);background:var(--purple-dim);}
|
||||
.str-cell.palindrome{background:var(--green-dim);border-color:var(--green);}
|
||||
.str-cell.best{background:var(--green);color:#fff;border-color:var(--green);}
|
||||
.str-cell .str-idx{font-size:.55rem;color:var(--text2);}
|
||||
/* Step dots (Style B) */
|
||||
.step-dot {
|
||||
width: 28px; height: 6px; border-radius: 3px; background: #cbd5e1;
|
||||
display: inline-block; transition: background .2s; cursor: pointer;
|
||||
}
|
||||
.step-dot.done { background: var(--blue); }
|
||||
.step-dot.active { background: var(--orange); }
|
||||
|
||||
/* Partition colors */
|
||||
.part-0{background:var(--accent)!important;border-color:var(--accent)!important;}
|
||||
.part-1{background:var(--green)!important;border-color:var(--green)!important;}
|
||||
.part-2{background:var(--orange)!important;border-color:var(--orange)!important;}
|
||||
.part-3{background:var(--purple)!important;border-color:var(--purple)!important;}
|
||||
.part-4{background:var(--cyan)!important;border-color:var(--cyan)!important;}
|
||||
.part-5{background:var(--yellow)!important;border-color:var(--yellow)!important;color:#222!important;}
|
||||
.part-6{background:var(--red)!important;border-color:var(--red)!important;}
|
||||
.part-7{background:var(--blue)!important;border-color:var(--blue)!important;}
|
||||
/* ---------- Hint Box ---------- */
|
||||
.hint {
|
||||
background: var(--orange-light); border-left: 4px solid var(--orange); border-radius: 8px;
|
||||
padding: 10px 16px; margin-bottom: 12px; line-height: 1.7; font-size: 14px;
|
||||
}
|
||||
.hint.info { background: #eff6ff; border-left-color: var(--blue); }
|
||||
.hint.success { background: #f0fdf4; border-left-color: var(--green); }
|
||||
.hint #stepInfo { font-weight: 700; }
|
||||
|
||||
/* Jump vis */
|
||||
.jump-row{display:flex;gap:3px;align-items:center;justify-content:center;margin:8px 0;flex-wrap:wrap;}
|
||||
.jump-cell{display:flex;flex-direction:column;align-items:center;min-width:38px;padding:4px 2px;border-radius:6px;transition:all .2s;}
|
||||
.jump-cell .j-val{font-family:var(--mono);font-size:.9rem;font-weight:600;}
|
||||
.jump-cell .j-idx{font-size:.6rem;color:var(--text2);}
|
||||
.jump-cell.reachable{background:var(--blue-dim);border:1px solid var(--blue);}
|
||||
.jump-cell.current{background:var(--orange-dim);border:2px solid var(--orange);}
|
||||
.jump-cell.max-reach{background:var(--green-dim);border:2px solid var(--green);}
|
||||
.jump-cell.step-start{border-bottom:3px solid var(--orange);}
|
||||
.jump-cell .j-chip{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;background:var(--surface2);border:2px solid var(--border);}
|
||||
/* ---------- Cards & Panels ---------- */
|
||||
.card, .panel {
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
||||
padding: 20px; margin-bottom: 12px; box-shadow: var(--shadow);
|
||||
}
|
||||
.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
@media (max-width: 900px) { .panel-grid { grid-template-columns: 1fr; } }
|
||||
.panels { display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
/* Code */
|
||||
.code-section{margin-top:16px;}
|
||||
pre.code-block{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:.8rem;line-height:1.7;white-space:pre;color:var(--text2);}
|
||||
pre.code-block .hl{background:var(--orange-dim);display:inline-block;width:calc(100% + 24px);margin:0 -12px;padding:0 12px;border-left:3px solid var(--orange);}
|
||||
pre.code-block .kw{color:var(--purple);}
|
||||
pre.code-block .fn{color:var(--blue);}
|
||||
pre.code-block .str{color:var(--green);}
|
||||
pre.code-block .num{color:var(--orange);}
|
||||
pre.code-block .cmt{color:#555;font-style:italic;}
|
||||
/* ---------- Formula / Code Box ---------- */
|
||||
.formula-box {
|
||||
background: #f0f9ff; border-radius: var(--radius); padding: 12px 16px;
|
||||
font-size: 15px; line-height: 1.8; margin-bottom: 12px; color: #0c4a6e;
|
||||
}
|
||||
.calc-block {
|
||||
background: #f8fafc; border-radius: var(--radius); padding: 12px 16px;
|
||||
font-size: 15px; line-height: 1.9; margin-bottom: 10px; border: 1px solid var(--border);
|
||||
}
|
||||
code {
|
||||
background: #f1f5f9; padding: 0.15em 0.4em; border-radius: 4px;
|
||||
font-size: 0.9em; color: #7c3aed; font-family: var(--mono);
|
||||
}
|
||||
pre {
|
||||
background: #f8fafc; border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 1rem; overflow-x: auto; margin: 0.75rem 0;
|
||||
}
|
||||
pre code { background: none; padding: 0; color: var(--text); }
|
||||
|
||||
/* Result */
|
||||
.result-box{background:var(--green-dim);border:1px solid rgba(61,214,140,.3);border-radius:var(--radius);padding:12px 16px;font-size:.9rem;color:var(--green);}
|
||||
.result-box .val{font-size:1.3rem;font-weight:700;}
|
||||
/* Code block with highlight */
|
||||
.code-block {
|
||||
margin: 12px 0; border-radius: var(--radius); overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.code-block .lang-label {
|
||||
background: #e2e8f0; padding: 4px 12px; font-size: 12px; color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.code-block pre { margin: 0; border: none; border-radius: 0; }
|
||||
.code-block .hl { background: #fef3c7; display: inline; }
|
||||
.code-line { display: block; padding: 0 1rem; transition: background 0.2s; }
|
||||
.code-line.highlight { background: #fef3c7; border-left: 3px solid var(--orange); }
|
||||
|
||||
/* Footer */
|
||||
footer{text-align:center;color:var(--text2);font-size:.72rem;margin-top:32px;opacity:.5;}
|
||||
/* ---------- Tables ---------- */
|
||||
.table-wrap { overflow-x: auto; margin-top: 8px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
||||
th, td { border: 1px solid var(--border); padding: 5px 8px; text-align: center; }
|
||||
th { background: #f8fafc; color: var(--text-secondary); font-weight: 600; }
|
||||
|
||||
/* Misc */
|
||||
.flex-center{display:flex;align-items:center;justify-content:center;}
|
||||
.mt-8{margin-top:8px;}
|
||||
.mb-8{margin-bottom:8px;}
|
||||
.tag-green{color:var(--green);font-weight:700;font-size:.75rem;}
|
||||
.tag-orange{color:var(--orange);font-weight:700;font-size:.75rem;}
|
||||
.tag-red{color:var(--red);font-weight:700;font-size:.75rem;}
|
||||
.tag-blue{color:var(--blue);font-weight:700;font-size:.75rem;}
|
||||
.legend{display:flex;gap:12px;flex-wrap:wrap;margin:8px 0;font-size:.75rem;color:var(--text2);}
|
||||
.legend-item{display:flex;align-items:center;gap:4px;}
|
||||
.legend-dot{width:10px;height:10px;border-radius:3px;}
|
||||
/* ---------- Chips / Array Cells (Style A) ---------- */
|
||||
.nums-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 8px 0; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 40px; height: 36px; padding: 4px 10px;
|
||||
border-radius: 10px; font-family: var(--mono); font-weight: 600; font-size: 14px;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.chip.default { background: #e2e8f0; color: var(--text); }
|
||||
.chip.blue { background: var(--blue-light); color: var(--blue-dark); }
|
||||
.chip.green { background: var(--green-light); color: var(--green-dark); }
|
||||
.chip.red { background: var(--red-light); color: var(--red-dark); }
|
||||
.chip.orange { background: var(--orange-light); color: var(--orange-dark); }
|
||||
.chip.purple { background: var(--purple-light); color: var(--purple-dark); }
|
||||
.chip.cyan { background: var(--cyan-light); color: #155e75; }
|
||||
.chip.active { box-shadow: 0 0 0 3px rgba(59,130,246,0.3); transform: scale(1.08); }
|
||||
.chip.selected { background: #bbf7d0; color: var(--green-dark); box-shadow: 0 0 0 2px var(--green); }
|
||||
.chip.swapped { background: #fef3c7; color: #92400e; }
|
||||
.chip.pointer { border-bottom: 3px solid var(--blue); }
|
||||
.chip.sorted { border-bottom: 3px solid var(--green); }
|
||||
.chip-group { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
|
||||
.chip-index { font-size: 11px; color: var(--text-muted); }
|
||||
|
||||
/* ---------- Array Rendering (Style B) ---------- */
|
||||
.arr-row { display: flex; align-items: flex-end; gap: 4px; justify-content: center; margin: 8px 0; }
|
||||
.arr-cell { display: flex; flex-direction: column; align-items: center; min-width: 36px; }
|
||||
.arr-val { font-family: var(--mono); font-size: .82rem; padding: 3px 0; color: var(--text); }
|
||||
.arr-bar {
|
||||
width: 30px; border-radius: 4px 4px 0 0; background: var(--blue); transition: all .25s;
|
||||
}
|
||||
.arr-idx { font-family: var(--mono); font-size: .68rem; color: var(--text-secondary); margin-top: 2px; }
|
||||
.arr-bar.min-price { background: var(--green); box-shadow: 0 0 8px var(--green); }
|
||||
.arr-bar.current { background: var(--orange); box-shadow: 0 0 8px var(--orange); }
|
||||
.arr-bar.checking { background: var(--purple); box-shadow: 0 0 6px var(--purple); }
|
||||
.arr-bar.covered { background: var(--blue); opacity: .7; }
|
||||
.arr-bar.best { background: var(--green); box-shadow: 0 0 8px var(--green); }
|
||||
.arr-cell .tag { font-size: .6rem; font-weight: 700; margin-top: 1px; }
|
||||
.tag-green { color: var(--green); }
|
||||
.tag-orange { color: var(--orange); }
|
||||
.tag-blue { color: var(--blue); }
|
||||
|
||||
/* ---------- Arrows / Pointers ---------- */
|
||||
.pointer-arrow {
|
||||
display: inline-flex; flex-direction: column; align-items: center;
|
||||
font-size: 11px; color: var(--blue); font-weight: 700;
|
||||
}
|
||||
.pointer-arrow::before { content: '▼'; font-size: 10px; }
|
||||
.pointer-arrow.green { color: var(--green); }
|
||||
.split { font-size: 22px; font-weight: 800; margin: 0 8px; color: var(--text-muted); }
|
||||
|
||||
/* ---------- Result / Answer Box ---------- */
|
||||
.current-answer {
|
||||
margin-top: 12px; padding: 10px 16px; background: #ecfdf5; border-radius: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.final-answer {
|
||||
margin-top: 16px; padding: 16px 20px; background: #ecfdf5;
|
||||
border: 2px solid #86efac; border-radius: var(--radius); line-height: 1.9;
|
||||
}
|
||||
.final-answer b { color: var(--green); }
|
||||
|
||||
/* ---------- Complexity Box ---------- */
|
||||
.complexity {
|
||||
margin-top: 10px; font-size: 13px; color: var(--text-secondary);
|
||||
padding: 8px 12px; background: #f1f5f9; border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ---------- Tree / Node Visualization ---------- */
|
||||
.tree-container {
|
||||
display: flex; justify-content: center; padding: 20px 0; overflow-x: auto;
|
||||
}
|
||||
.tree-node {
|
||||
display: inline-flex; flex-direction: column; align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.tree-node .node-circle {
|
||||
width: 40px; height: 40px; border-radius: 50%; display: flex;
|
||||
align-items: center; justify-content: center; font-weight: 700;
|
||||
font-size: 14px; background: var(--blue-light); color: var(--blue-dark);
|
||||
border: 2px solid var(--blue); transition: all var(--transition);
|
||||
}
|
||||
.tree-node .node-circle.active {
|
||||
background: #4f46e5; color: white; box-shadow: 0 0 0 4px rgba(79,70,229,0.25);
|
||||
}
|
||||
.tree-node .node-circle.visited {
|
||||
background: var(--green-light); border-color: var(--green); color: var(--green-dark);
|
||||
}
|
||||
.tree-node .node-circle.current {
|
||||
background: #fef3c7; border-color: var(--orange); color: var(--orange-dark);
|
||||
box-shadow: 0 0 0 4px rgba(245,158,11,0.25);
|
||||
}
|
||||
|
||||
/* ---------- Linked List Visualization ---------- */
|
||||
.linked-list {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 4px; padding: 16px 0;
|
||||
}
|
||||
.ll-node {
|
||||
display: inline-flex; align-items: center; gap: 0;
|
||||
border: 2px solid var(--blue); border-radius: 8px; overflow: hidden;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.ll-node .val {
|
||||
min-width: 48px; height: 40px; display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: 14px; padding: 0 12px; background: var(--blue-light); color: var(--blue-dark);
|
||||
}
|
||||
.ll-node .arrow {
|
||||
width: 28px; height: 40px; display: flex; align-items: center; justify-content: center;
|
||||
background: #f1f5f9; border-left: 1px solid var(--border); font-size: 16px; color: var(--text-muted);
|
||||
}
|
||||
.ll-node.active { border-color: var(--green); box-shadow: 0 0 0 3px rgba(22,163,74,0.2); }
|
||||
.ll-node.active .val { background: var(--green-light); color: var(--green-dark); }
|
||||
.ll-node.current { border-color: var(--orange); box-shadow: 0 0 0 3px rgba(245,158,11,0.2); }
|
||||
.ll-node.current .val { background: #fef3c7; color: var(--orange-dark); }
|
||||
.ll-node.null-node { border-style: dashed; border-color: var(--text-muted); }
|
||||
.ll-node.null-node .val { background: #f1f5f9; color: var(--text-muted); font-style: italic; font-weight: 400; }
|
||||
.ll-arrow { font-size: 18px; color: var(--text-muted); margin: 0 2px; }
|
||||
|
||||
/* ---------- Grid/Matrix Visualization ---------- */
|
||||
.grid-viz, .grid-vis {
|
||||
display: inline-grid; gap: 2px; padding: 8px 0;
|
||||
}
|
||||
.grid-cell {
|
||||
width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
|
||||
border: 1px solid var(--border); border-radius: 6px; font-size: 14px;
|
||||
font-weight: 600; transition: all var(--transition); background: white;
|
||||
}
|
||||
.grid-cell.island { background: var(--blue-light); color: var(--blue-dark); }
|
||||
.grid-cell.water { background: #e0f2fe; color: #0369a1; }
|
||||
.grid-cell.visited { background: var(--green-light); border-color: var(--green); }
|
||||
.grid-cell.current { background: #fef3c7; border-color: var(--orange); box-shadow: 0 0 0 2px rgba(245,158,11,0.3); }
|
||||
.grid-cell.rotten { background: var(--orange-light); border-color: var(--orange); color: var(--orange-dark); }
|
||||
.grid-cell.fresh { background: #dcfce7; border-color: var(--green); }
|
||||
.grid-cell.empty { background: #f1f5f9; color: var(--text-muted); }
|
||||
.grid-cell.wall { background: #1e293b; color: white; }
|
||||
.grid-cell.path { background: #dbeafe; border-color: var(--blue); }
|
||||
/* Style B grid classes */
|
||||
.grid-cell.highlight { background: var(--blue); color: #fff; border-color: var(--blue); }
|
||||
.grid-cell.computing { background: var(--orange-light); border-color: var(--orange); }
|
||||
.grid-cell.origin { background: var(--purple-light); border-color: var(--purple); }
|
||||
|
||||
/* String rendering (Style B) */
|
||||
.str-row { display: flex; gap: 2px; justify-content: center; margin: 8px 0; flex-wrap: wrap; }
|
||||
|
||||
/* ---------- Stack / Queue Visualization ---------- */
|
||||
.stack-container, .queue-container {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 2px; min-height: 200px;
|
||||
padding: 8px; background: #f8fafc; border: 2px dashed var(--border); border-radius: var(--radius);
|
||||
}
|
||||
.stack-item, .queue-item {
|
||||
min-width: 80px; padding: 8px 16px; text-align: center; font-weight: 600;
|
||||
border-radius: 8px; font-size: 14px; transition: all var(--transition);
|
||||
background: var(--blue-light); color: var(--blue-dark); border: 1px solid var(--blue);
|
||||
}
|
||||
.stack-item.top, .queue-item.front {
|
||||
background: #4f46e5; color: white; border-color: #4f46e5;
|
||||
}
|
||||
.stack-item.popped {
|
||||
background: var(--red-light); color: var(--red-dark); border-color: var(--red);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ---------- Step Controls ---------- */
|
||||
.step-controls {
|
||||
display: flex; gap: 8px; align-items: center; justify-content: center;
|
||||
margin: 16px 0; flex-wrap: wrap;
|
||||
}
|
||||
.step-controls button {
|
||||
padding: 10px 16px; border: 1px solid #cbd5e1; border-radius: 8px;
|
||||
background: #f8fafc; font-size: 14px; cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.step-controls button:hover { background: #e2e8f0; }
|
||||
.step-controls button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.step-controls button.primary { background: var(--blue); color: white; border-color: var(--blue); }
|
||||
.step-controls button.primary:hover { background: #2563eb; }
|
||||
.step-counter {
|
||||
font-size: 14px; color: var(--text-secondary); min-width: 100px; text-align: center;
|
||||
}
|
||||
|
||||
/* ---------- Vis Area ---------- */
|
||||
.vis-area { min-height: 120px; padding: 16px 0; position: relative; }
|
||||
.vis-area svg { display: block; margin: 0 auto; max-width: 100%; }
|
||||
|
||||
/* ---------- Animation Helpers ---------- */
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }
|
||||
@keyframes slideIn { from { opacity: 0; transform: translateX(-12px); } to { opacity: 1; transform: none; } }
|
||||
.animate-fade { animation: fadeIn 0.3s ease; }
|
||||
.animate-pulse { animation: pulse 1s infinite; }
|
||||
.animate-slide { animation: slideIn 0.3s ease; }
|
||||
|
||||
/* ---------- Footer ---------- */
|
||||
footer {
|
||||
margin-top: 3rem; text-align: center; color: var(--text-muted);
|
||||
font-size: 0.85rem; padding: 16px 0;
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (max-width: 768px) {
|
||||
.container { padding: 12px; }
|
||||
h1 { font-size: 20px; }
|
||||
.controls { padding: 8px 10px; }
|
||||
.controls input[type="text"] { width: 100%; }
|
||||
.panel { padding: 12px; }
|
||||
.chip { min-width: 32px; height: 30px; font-size: 12px; padding: 2px 6px; }
|
||||
.grid-cell { width: 32px; height: 32px; font-size: 12px; }
|
||||
}
|
||||
|
||||
/* ---------- Print ---------- */
|
||||
@media print {
|
||||
body { background: white; }
|
||||
.controls, .step-controls { display: none; }
|
||||
.card, .panel { box-shadow: none; border: 1px solid #ccc; }
|
||||
}
|
||||
|
||||
/* ---------- Nav page styles ---------- */
|
||||
.hero { text-align: center; padding: 3rem 1rem; }
|
||||
.hero h1 { font-size: 2.5rem; margin-bottom: .5rem; justify-content: center; }
|
||||
.hero p { color: var(--text-secondary); font-size: 1.1rem; }
|
||||
.stats { display: flex; gap: 1rem; justify-content: center; margin-top: 1.5rem; flex-wrap: wrap; }
|
||||
.stat-card { background: white; border: 1px solid var(--border); border-radius: 12px; padding: 1rem 1.5rem; text-align: center; box-shadow: var(--shadow); min-width: 100px; }
|
||||
.stat-card .num { font-size: 2rem; font-weight: 800; }
|
||||
.stat-card .label { font-size: .85rem; color: var(--text-secondary); }
|
||||
.category { margin-bottom: 2rem; }
|
||||
.category h2 { font-size: 1.3rem; margin-bottom: .75rem; padding-bottom: .5rem; border-bottom: 2px solid var(--border); }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(240px,1fr)); gap: 10px; }
|
||||
.card-link { display: block; background: white; border: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; text-decoration: none; color: var(--text); transition: all .2s; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
|
||||
.card-link:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--blue); }
|
||||
.card-link .id { font-size: .8rem; color: var(--text-muted); }
|
||||
.card-link .title { font-weight: 600; font-size: .95rem; margin-top: 2px; }
|
||||
|
||||
/* ---------- Misc Helpers ---------- */
|
||||
.processed { opacity: .85; }
|
||||
.dim { opacity: .3; }
|
||||
.best-diff { font-weight: 800; color: var(--green); }
|
||||
.nearest-val { color: var(--green); font-weight: 700; }
|
||||
.sign-plus { color: var(--green); font-weight: 800; }
|
||||
.sign-minus { color: var(--red); font-weight: 800; }
|
||||
|
||||
Reference in New Issue
Block a user