Files
algebra-webapp/js/components/matrix-builder.js
renato97 45582e5f59 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
2026-05-20 01:26:40 -03:00

287 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };
})();