- 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
311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
/**
|
|
* SystemSolver — System input, method selector, step-by-step solution display
|
|
*/
|
|
const SystemSolver = (() => {
|
|
let container = null;
|
|
let currentSize = 3;
|
|
let currentMethod = 'gauss';
|
|
|
|
function render() {
|
|
container.innerHTML = `
|
|
<div class="system-solver">
|
|
<div class="system-solver__controls">
|
|
<label>Incógnitas:</label>
|
|
<select class="system-solver__size-select" id="systemSize">
|
|
<option value="2" ${currentSize === 2 ? 'selected' : ''}>2</option>
|
|
<option value="3" ${currentSize === 3 ? 'selected' : ''}>3</option>
|
|
</select>
|
|
|
|
<label>Método:</label>
|
|
<select class="system-solver__method-select" id="systemMethod">
|
|
<option value="gauss" ${currentMethod === 'gauss' ? 'selected' : ''}>Gauss</option>
|
|
<option value="gauss-jordan" ${currentMethod === 'gauss-jordan' ? 'selected' : ''}>Gauss-Jordan</option>
|
|
<option value="cramer" ${currentMethod === 'cramer' ? 'selected' : ''}>Cramer</option>
|
|
<option value="rouche-frobenius" ${currentMethod === 'rouche-frobenius' ? 'selected' : ''}>Rouché-Frobenius</option>
|
|
</select>
|
|
|
|
<button class="matrix-ops__btn" data-action="clear">Limpiar</button>
|
|
<button class="matrix-ops__btn" data-action="example">Ejemplo</button>
|
|
</div>
|
|
|
|
<p class="system-solver__matrix-label">Matriz de coeficientes | Términos independientes</p>
|
|
<div id="systemGridContainer"></div>
|
|
|
|
<button class="system-solver__solve-btn" id="solveBtn">Resolver</button>
|
|
|
|
<div class="error-message" id="systemError"></div>
|
|
<div class="matrix-result" id="systemResult">
|
|
<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('systemGridContainer');
|
|
if (!gridContainer) return;
|
|
const n = currentSize;
|
|
// n columns for coefficients + divider + 1 column for constants
|
|
let html = `<div class="augmented-grid" style="grid-template-columns: repeat(${n}, 1fr) auto 1fr; padding-left: 24px; padding-right: 24px; gap: 4px;">`;
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
for (let j = 0; j < n; j++) {
|
|
html += `<input type="text" class="matrix-grid__cell" id="sys-coeff-${i}-${j}" data-row="${i}" data-col="${j}" placeholder="a${i + 1}${j + 1}">`;
|
|
}
|
|
// Divider column (one per row, but we only need 1 actual divider)
|
|
if (i === 0) {
|
|
html += `<div class="augmented-grid__divider" style="grid-row: span ${n};"></div>`;
|
|
} else {
|
|
// Empty cell for grid alignment — no, the divider spans all rows
|
|
}
|
|
html += `<input type="text" class="matrix-grid__cell" id="sys-const-${i}" data-row="${i}" placeholder="b${i + 1}" style="border-color: var(--color-accent);">`;
|
|
}
|
|
html += '</div>';
|
|
gridContainer.innerHTML = html;
|
|
}
|
|
|
|
function getSystem() {
|
|
const n = currentSize;
|
|
const A = [];
|
|
const b = [];
|
|
let hasError = false;
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const row = [];
|
|
for (let j = 0; j < n; j++) {
|
|
const cell = document.getElementById(`sys-coeff-${i}-${j}`);
|
|
if (!cell) return null;
|
|
const val = cell.value.trim();
|
|
if (val === '') {
|
|
cell.classList.add('matrix-grid__cell--error');
|
|
hasError = true;
|
|
} else {
|
|
cell.classList.remove('matrix-grid__cell--error');
|
|
}
|
|
const num = parseFloat(val);
|
|
if (isNaN(num)) {
|
|
cell.classList.add('matrix-grid__cell--error');
|
|
hasError = true;
|
|
}
|
|
row.push(num || 0);
|
|
}
|
|
A.push(row);
|
|
|
|
const bCell = document.getElementById(`sys-const-${i}`);
|
|
if (!bCell) return null;
|
|
const bVal = bCell.value.trim();
|
|
if (bVal === '') {
|
|
bCell.classList.add('matrix-grid__cell--error');
|
|
hasError = true;
|
|
} else {
|
|
bCell.classList.remove('matrix-grid__cell--error');
|
|
}
|
|
b.push(parseFloat(bVal) || 0);
|
|
}
|
|
|
|
if (hasError) {
|
|
showError('Completá todas las celdas con valores numéricos.');
|
|
return null;
|
|
}
|
|
hideError();
|
|
return { A, b };
|
|
}
|
|
|
|
function showError(msg) {
|
|
const el = document.getElementById('systemError');
|
|
if (el) {
|
|
el.textContent = msg;
|
|
el.classList.add('error-message--visible');
|
|
}
|
|
}
|
|
|
|
function hideError() {
|
|
const el = document.getElementById('systemError');
|
|
if (el) el.classList.remove('error-message--visible');
|
|
}
|
|
|
|
function showResult(title, value, steps) {
|
|
const resultEl = document.getElementById('systemResult');
|
|
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 === 'object' && value !== null && value.type) {
|
|
// Classification result
|
|
let text = '';
|
|
if (value.type === 'CD') text = 'Compatible Determinado';
|
|
else if (value.type === 'CI') text = `Compatible Indeterminado (${value.freeVars || '?'} parámetros libres)`;
|
|
else if (value.type === 'SI') text = 'Sistema Incompatible';
|
|
else if (value.type === 'indeterminate') text = 'Compatible Indeterminado';
|
|
else if (value.type === 'trivial') text = 'Solución trivial';
|
|
valueEl.textContent = text;
|
|
|
|
if (value.solution) {
|
|
const solDiv = document.createElement('div');
|
|
solDiv.style.marginTop = '8px';
|
|
if (typeof KatexRenderer !== 'undefined') {
|
|
KatexRenderer.renderDisplay(solDiv, 'x = ' + MathEngine.vecToLatex(value.solution));
|
|
} else {
|
|
solDiv.textContent = 'x = ' + JSON.stringify(value.solution);
|
|
}
|
|
valueEl.appendChild(solDiv);
|
|
}
|
|
} else if (Array.isArray(value)) {
|
|
if (typeof KatexRenderer !== 'undefined') {
|
|
KatexRenderer.renderDisplay(valueEl, 'x = ' + MathEngine.vecToLatex(value));
|
|
} else {
|
|
valueEl.textContent = 'x = ' + JSON.stringify(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) => {
|
|
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' && (step.latex || step.expression)) {
|
|
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 solve() {
|
|
const system = getSystem();
|
|
if (!system) return;
|
|
|
|
const { A, b } = system;
|
|
let result;
|
|
|
|
try {
|
|
const ME = MathEngine;
|
|
switch (currentMethod) {
|
|
case 'gauss':
|
|
result = ME.system.gaussElimination(A, b);
|
|
break;
|
|
case 'gauss-jordan':
|
|
result = ME.system.gaussJordan(A, b);
|
|
break;
|
|
case 'cramer':
|
|
result = ME.system.cramer(A, b);
|
|
break;
|
|
case 'rouche-frobenius':
|
|
result = ME.system.roucheFrobenius(A, b);
|
|
break;
|
|
default:
|
|
showError('Método no reconocido.');
|
|
return;
|
|
}
|
|
|
|
if (result.error) {
|
|
showError(result.error);
|
|
// Still show steps
|
|
if (result.steps && result.steps.length > 0) {
|
|
showResult('Clasificación', null, result.steps);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const titles = {
|
|
'gauss': 'Eliminación de Gauss',
|
|
'gauss-jordan': 'Gauss-Jordan',
|
|
'cramer': 'Regla de Cramer',
|
|
'rouche-frobenius': 'Rouché-Frobenius'
|
|
};
|
|
showResult(titles[currentMethod] || currentMethod, result.value, result.steps);
|
|
} catch (e) {
|
|
showError('Error: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function clearGrid() {
|
|
const n = currentSize;
|
|
for (let i = 0; i < n; i++) {
|
|
for (let j = 0; j < n; j++) {
|
|
const cell = document.getElementById(`sys-coeff-${i}-${j}`);
|
|
if (cell) { cell.value = ''; cell.classList.remove('matrix-grid__cell--error'); }
|
|
}
|
|
const bCell = document.getElementById(`sys-const-${i}`);
|
|
if (bCell) { bCell.value = ''; bCell.classList.remove('matrix-grid__cell--error'); }
|
|
}
|
|
const resultEl = document.getElementById('systemResult');
|
|
if (resultEl) resultEl.classList.remove('matrix-result--visible');
|
|
hideError();
|
|
}
|
|
|
|
function fillExample() {
|
|
const examples = {
|
|
2: { A: [[2, 1], [1, 1]], b: [7, 4] },
|
|
3: { A: [[2, 1, -1], [-3, -1, 2], [-2, 1, 2]], b: [8, -11, -3] }
|
|
};
|
|
const ex = examples[currentSize];
|
|
const n = currentSize;
|
|
for (let i = 0; i < n; i++) {
|
|
for (let j = 0; j < n; j++) {
|
|
const cell = document.getElementById(`sys-coeff-${i}-${j}`);
|
|
if (cell) cell.value = ex.A[i][j];
|
|
}
|
|
const bCell = document.getElementById(`sys-const-${i}`);
|
|
if (bCell) bCell.value = ex.b[i];
|
|
}
|
|
}
|
|
|
|
function init(el) {
|
|
container = el;
|
|
render();
|
|
|
|
container.addEventListener('change', (e) => {
|
|
if (e.target.id === 'systemSize') {
|
|
currentSize = parseInt(e.target.value);
|
|
renderGrid();
|
|
const resultEl = document.getElementById('systemResult');
|
|
if (resultEl) resultEl.classList.remove('matrix-result--visible');
|
|
hideError();
|
|
}
|
|
if (e.target.id === 'systemMethod') {
|
|
currentMethod = e.target.value;
|
|
}
|
|
});
|
|
|
|
container.addEventListener('click', (e) => {
|
|
if (e.target.id === 'solveBtn') solve();
|
|
});
|
|
|
|
container.addEventListener('click', (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 destroy() {
|
|
if (container) container.innerHTML = '';
|
|
container = null;
|
|
}
|
|
|
|
return { init, destroy, clearGrid, fillExample };
|
|
})();
|