- 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
287 lines
9.4 KiB
JavaScript
287 lines
9.4 KiB
JavaScript
/**
|
||
* 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 };
|
||
})();
|