feat: interactive linear algebra practice web app

- 68 exercises from UBA FCE chapters 1-3
- Step-by-step solutions with KaTeX rendering
- Theory panels (26 topics) expandable per exercise
- Matrix builder (2x2/3x3/4x4) with 7 operations
- System solver (Gauss, Gauss-Jordan, Cramer, Rouché-Frobenius)
- Glassmorphism UI with dark mode
- Canvas particle background
- ARIA accessibility (keyboard nav, screen reader)
- Zero build step - open index.html directly
This commit is contained in:
renato97
2026-05-20 01:26:40 -03:00
commit 45582e5f59
16 changed files with 5116 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
/**
* MatrixBuilder — Size selector (2x2/3x3/4x4), input grid, operation buttons
*/
const MatrixBuilder = (() => {
let container = null;
let currentSize = 3;
function render() {
container.innerHTML = `
<div class="matrix-builder">
<div class="matrix-builder__controls">
<label>Tamaño:</label>
<select class="matrix-builder__size-select" id="matrixSize">
<option value="2" ${currentSize === 2 ? 'selected' : ''}>2×2</option>
<option value="3" ${currentSize === 3 ? 'selected' : ''}>3×3</option>
<option value="4" ${currentSize === 4 ? 'selected' : ''}>4×4</option>
</select>
<button class="matrix-ops__btn" data-action="clear">Limpiar</button>
<button class="matrix-ops__btn" data-action="example">Ejemplo</button>
</div>
<div id="matrixGridContainer"></div>
<div class="matrix-ops">
<button class="matrix-ops__btn" data-op="determinant">Determinante</button>
<button class="matrix-ops__btn" data-op="transpose">Traspuesta</button>
<button class="matrix-ops__btn" data-op="trace">Traza</button>
<button class="matrix-ops__btn" data-op="rank">Rango</button>
<button class="matrix-ops__btn" data-op="inverse">Inversa</button>
<button class="matrix-ops__btn" data-op="det-sarrus">Sarrus (3×3)</button>
<button class="matrix-ops__btn" data-op="det-triangular">Triangularización</button>
</div>
<div class="error-message" id="matrixError"></div>
<div class="matrix-result" id="matrixResult">
<div class="matrix-result__title" id="resultTitle"></div>
<div class="matrix-result__value" id="resultValue"></div>
<div id="resultSteps"></div>
</div>
</div>
`;
renderGrid();
}
function renderGrid() {
const gridContainer = document.getElementById('matrixGridContainer');
if (!gridContainer) return;
let html = `<div class="matrix-grid" style="grid-template-columns: repeat(${currentSize}, 1fr); padding-left: 20px; padding-right: 20px;">`;
for (let i = 0; i < currentSize; i++) {
for (let j = 0; j < currentSize; j++) {
html += `<input type="text" class="matrix-grid__cell" id="cell-${i}-${j}" data-row="${i}" data-col="${j}">`;
}
}
html += '</div>';
gridContainer.innerHTML = html;
}
function getMatrix() {
const M = [];
let hasEmpty = false;
for (let i = 0; i < currentSize; i++) {
const row = [];
for (let j = 0; j < currentSize; j++) {
const cell = document.getElementById(`cell-${i}-${j}`);
if (!cell) return null;
const val = cell.value.trim();
if (val === '') {
cell.classList.add('matrix-grid__cell--error');
hasEmpty = true;
} else {
cell.classList.remove('matrix-grid__cell--error');
}
const num = parseFloat(val);
if (isNaN(num)) {
cell.classList.add('matrix-grid__cell--error');
hasEmpty = true;
}
row.push(num || 0);
}
M.push(row);
}
if (hasEmpty) {
showError('Completá todas las celdas con valores numéricos.');
return null;
}
hideError();
return M;
}
function showError(msg) {
const el = document.getElementById('matrixError');
if (el) {
el.textContent = msg;
el.classList.add('error-message--visible');
}
}
function hideError() {
const el = document.getElementById('matrixError');
if (el) el.classList.remove('error-message--visible');
}
function showResult(title, value, steps) {
const resultEl = document.getElementById('matrixResult');
const titleEl = document.getElementById('resultTitle');
const valueEl = document.getElementById('resultValue');
const stepsEl = document.getElementById('resultSteps');
titleEl.textContent = title;
resultEl.classList.add('matrix-result--visible');
// Render value
if (typeof value === 'number') {
if (typeof KatexRenderer !== 'undefined') {
KatexRenderer.renderDisplay(valueEl, `\\text{${title}} = ${MathEngine.fmtNum(value)}`);
} else {
valueEl.textContent = `${title} = ${value}`;
}
} else if (Array.isArray(value) && Array.isArray(value[0])) {
if (typeof KatexRenderer !== 'undefined') {
KatexRenderer.renderDisplay(valueEl, MathEngine.matToLatex(value));
} else {
valueEl.textContent = JSON.stringify(value);
}
} else if (Array.isArray(value)) {
if (typeof KatexRenderer !== 'undefined') {
KatexRenderer.renderDisplay(valueEl, MathEngine.vecToLatex(value));
} else {
valueEl.textContent = JSON.stringify(value);
}
} else if (typeof value === 'object' && value !== null) {
valueEl.textContent = JSON.stringify(value);
} else if (typeof KatexRenderer !== 'undefined') {
KatexRenderer.renderDisplay(valueEl, String(value));
} else {
valueEl.textContent = String(value);
}
// Render steps
stepsEl.innerHTML = '';
if (steps && steps.length > 0) {
const ol = document.createElement('ol');
ol.className = 'matrix-result__steps';
steps.forEach((step, idx) => {
const li = document.createElement('li');
li.className = 'matrix-result__step';
const descDiv = document.createElement('div');
descDiv.textContent = step.desc;
const exprDiv = document.createElement('div');
if (typeof KatexRenderer !== 'undefined') {
KatexRenderer.renderDisplay(exprDiv, step.latex || step.expression || '');
} else {
exprDiv.textContent = step.latex || step.expression || '';
}
li.appendChild(descDiv);
li.appendChild(exprDiv);
ol.appendChild(li);
});
stepsEl.appendChild(ol);
}
}
function executeOperation(op) {
const M = getMatrix();
if (!M) return;
let result;
try {
const ME = MathEngine;
switch (op) {
case 'determinant':
if (currentSize === 2) result = ME.determinant.det2x2(M);
else if (currentSize === 3) result = ME.determinant.det3x3Sarrus(M);
else result = ME.determinant.det(M);
break;
case 'det-sarrus':
if (currentSize !== 3) { showError('Sarrus solo aplica a matrices 3×3.'); return; }
result = ME.determinant.det3x3Sarrus(M);
break;
case 'det-triangular':
result = ME.determinant.detByTriangularization(M);
break;
case 'transpose':
result = ME.matrix.transpose(M);
break;
case 'trace':
result = ME.matrix.trace(M);
break;
case 'rank':
result = ME.system.rank(M);
break;
case 'inverse':
if (currentSize === 2) result = ME.inverse.inverse2x2(M);
else result = ME.inverse.inverse(M);
break;
default:
showError('Operación no reconocida.');
return;
}
if (result.error) {
showError(result.error);
return;
}
const titles = {
'determinant': 'Determinante',
'det-sarrus': 'Determinante (Sarrus)',
'det-triangular': 'Determinante (Triangularización)',
'transpose': 'Traspuesta',
'trace': 'Traza',
'rank': 'Rango',
'inverse': 'Inversa'
};
showResult(titles[op] || op, result.value, result.steps);
} catch (e) {
showError('Error: ' + e.message);
}
}
function clearGrid() {
for (let i = 0; i < currentSize; i++)
for (let j = 0; j < currentSize; j++) {
const cell = document.getElementById(`cell-${i}-${j}`);
if (cell) { cell.value = ''; cell.classList.remove('matrix-grid__cell--error'); }
}
const resultEl = document.getElementById('matrixResult');
if (resultEl) resultEl.classList.remove('matrix-result--visible');
hideError();
}
function fillExample() {
const examples = {
2: [[1, 2], [3, 4]],
3: [[1, 2, 3], [4, 5, 2], [6, 3, 1]],
4: [[1, 2, 0, 1], [3, 0, 1, 2], [1, 1, 2, 0], [2, 3, 1, 1]]
};
const ex = examples[currentSize];
for (let i = 0; i < currentSize; i++)
for (let j = 0; j < currentSize; j++) {
const cell = document.getElementById(`cell-${i}-${j}`);
if (cell) cell.value = ex[i][j];
}
}
function handleSizeChange(e) {
currentSize = parseInt(e.target.value);
renderGrid();
const resultEl = document.getElementById('matrixResult');
if (resultEl) resultEl.classList.remove('matrix-result--visible');
hideError();
}
function handleOpClick(e) {
const btn = e.target.closest('[data-op]');
if (!btn) return;
executeOperation(btn.dataset.op);
}
function handleActionClick(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'clear') clearGrid();
else if (action === 'example') fillExample();
}
function init(el) {
container = el;
render();
container.addEventListener('change', (e) => {
if (e.target.id === 'matrixSize') handleSizeChange(e);
});
container.addEventListener('click', handleOpClick);
container.addEventListener('click', handleActionClick);
}
function destroy() {
if (container) {
container.innerHTML = '';
}
container = null;
}
return { init, destroy, clearGrid, fillExample };
})();