<p class="hint">Pick a rational function below. Each denominator factor (x − r) is a root; the cover-up method finds the constant that belongs over each one.</p>
<div class="controls">
<label for="preset">Function:</label>
<select id="preset"></select>
<button id="decompose" type="button">Decompose</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="frac-row">
<div class="frac-block" id="original"></div>
</div>
<div class="arrow" id="arrow" hidden>↓</div>
<div class="frac-row split" id="splitRow" hidden></div>
<div class="status" id="status">Pick a function and press Decompose.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
.controls label { font-size: .85rem; font-weight: 600; color: #1d3557; }
select { font: 600 14px system-ui, sans-serif; padding: .4rem .5rem; border-radius: 8px;
border: 1px solid #cdd9e3; background: #fff; color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.frac-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
gap: .5rem; min-height: 3.4rem; margin: .3rem 0; }
.frac-row.split { gap: .8rem; }
.frac-block { display: flex; align-items: center; gap: .35rem; font: 600 1.15rem ui-monospace, monospace; }
.frac { display: inline-flex; flex-direction: column; align-items: center; padding: 0 .3rem; }
.frac .num { padding: 0 .25rem .1rem; border-bottom: 2px solid #1d3557; }
.frac .den { padding: .1rem .25rem 0; }
.term { display: flex; align-items: center; gap: .3rem; padding: .5rem .6rem; border-radius: 10px;
background: #e8eef3; border: 1px solid #cdd9e3; opacity: 0; transform: translateY(6px);
transition: opacity .35s ease, transform .35s ease; }
.term.show { opacity: 1; transform: translateY(0); }
.term .root-tag { font: 600 .75rem system-ui, sans-serif; color: #457b9d; margin-right: .3rem; }
.sign { font: 700 1.15rem ui-monospace, monospace; color: #1d3557; }
.arrow { text-align: center; font-size: 1.4rem; color: #457b9d; margin: .2rem 0; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #0a7d33; }
.status.info { color: #444; font-weight: 500; font-size: .88rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Greatest common divisor, used to reduce a fraction to lowest terms.
function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { [a, b] = [b, a % b]; } return a || 1; }
// Approximate a decimal as a small fraction n/d (search denominators up to 12).
function toFraction(x, maxDen) {
maxDen = maxDen || 12;
for (let den = 1; den <= maxDen; den++) {
const num = x * den;
if (Math.abs(num - Math.round(num)) < 1e-6) {
let n = Math.round(num), d = den;
const g = gcd(n, d);
n /= g; d /= g;
if (d < 0) { d = -d; n = -n; }
return { n, d };
}
}
return { n: Math.round(x * 1000) / 1000, d: 1 };
}
// A few rational functions with simple, distinct real roots (numerator degree < denominator degree).
const PRESETS = [
{ roots: [1, -2], num: [3, 5], label: '(3x + 5) / [(x - 1)(x + 2)]' },
{ roots: [2, -1], num: [1, 5], label: '(x + 5) / [(x - 2)(x + 1)]' },
{ roots: [1, -2, 3], num: [-3, -6, -5], label: '(-3x^2 - 6x - 5) / [(x - 1)(x + 2)(x - 3)]' },
{ roots: [-1, 2, 4], num: [1, -2, 3], label: '(x^2 - 2x + 3) / [(x + 1)(x - 2)(x - 4)]' }
];
// Evaluate a polynomial given as an array of coefficients (highest degree first) at x.
function evalPoly(coeffs, x) { return coeffs.reduce((acc, c) => acc * x + c, 0); }
// Cover-up method: A_i = N(r_i) / product of (r_i - r_j) over every other root r_j.
function decompose(roots, num) {
const n = roots.length;
return roots.map((r, i) => {
let denom = 1;
for (let j = 0; j < n; j++) if (j !== i) denom *= (r - roots[j]);
return evalPoly(num, r) / denom;
});
}
function denomLabel(roots) {
return roots.map(r => r === 0 ? 'x' : (r > 0 ? ('(x - ' + r + ')') : ('(x + ' + Math.abs(r) + ')'))).join('');
}
function numLabel(num) {
const deg = num.length - 1;
const parts = [];
num.forEach((c, i) => {
if (c === 0) return;
const p = deg - i;
let term = p === 0 ? String(Math.abs(c)) : (Math.abs(c) === 1 ? '' : String(Math.abs(c))) + 'x' + (p > 1 ? ('^' + p) : '');
parts.push((c < 0 ? '-' : (parts.length ? '+' : '')) + term);
});
return parts.length ? parts.join(' ') : '0';
}
function fracHtml(numHtml, denHtml) {
return '<span class="frac"><span class="num">' + numHtml + '</span><span class="den">' + denHtml + '</span></span>';
}
let sel = document.getElementById('preset');
PRESETS.forEach((p, i) => {
const o = document.createElement('option'); o.value = i; o.textContent = p.label; sel.appendChild(o);
});
function renderOriginal() {
const p = PRESETS[sel.value];
document.getElementById('original').innerHTML = fracHtml(numLabel(p.num), denomLabel(p.roots));
}
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || '');
}
function clearSplit() {
document.getElementById('arrow').hidden = true;
const row = document.getElementById('splitRow');
row.hidden = true; row.innerHTML = '';
setStatus('Pick a function and press Decompose.', '');
}
function runDecompose() {
const p = PRESETS[sel.value];
const A = decompose(p.roots, p.num);
const row = document.getElementById('splitRow');
row.innerHTML = '';
A.forEach((a, i) => {
if (i > 0) {
const sign = document.createElement('span');
sign.className = 'sign'; sign.textContent = a < 0 ? '−' : '+';
row.appendChild(sign);
}
const frac = toFraction(Math.abs(a));
const term = document.createElement('div');
term.className = 'term';
const r = p.roots[i];
const denTxt = r === 0 ? 'x' : (r > 0 ? ('x - ' + r) : ('x + ' + Math.abs(r)));
term.innerHTML = '<span class="root-tag">' + i18nFmt('root %r%', { r: r }) + '</span>' +
fracHtml(String(frac.n), (frac.d === 1 ? denTxt : (frac.d + '(' + denTxt + ')')));
row.appendChild(term);
});
row.hidden = false;
document.getElementById('arrow').hidden = false;
requestAnimationFrame(() => {
[...row.querySelectorAll('.term')].forEach((el, i) => setTimeout(() => el.classList.add('show'), i * 220));
});
// Log the raw decimal coefficients for anyone checking the arithmetic by hand.
console.log(i18nFmt('Cover-up coefficients: %list%', { list: A.map((a, i) => 'A' + (i + 1) + '=' + a.toFixed(4)).join(', ') }));
setStatus(i18nFmt('Split into %n% term(s) — one fraction per root.', { n: A.length }), 'info');
}
sel.addEventListener('change', () => { renderOriginal(); clearSplit(); });
document.getElementById('decompose').addEventListener('click', runDecompose);
document.getElementById('reset').addEventListener('click', clearSplit);
renderOriginal();
clearSplit();
<p class="hint">Pick a rational function below. Each denominator factor (x − r) is a root; the cover-up method finds the constant that belongs over each one.</p>
<div class="controls">
<label for="preset">Function:</label>
<select id="preset"></select>
<button id="decompose" type="button">Decompose</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="frac-row">
<div class="frac-block" id="original"></div>
</div>
<div class="arrow" id="arrow" hidden>↓</div>
<div class="frac-row split" id="splitRow" hidden></div>
<div class="status" id="status">Pick a function and press Decompose.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
.controls label { font-size: .85rem; font-weight: 600; color: #1d3557; }
select { font: 600 14px system-ui, sans-serif; padding: .4rem .5rem; border-radius: 8px;
border: 1px solid #cdd9e3; background: #fff; color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.frac-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
gap: .5rem; min-height: 3.4rem; margin: .3rem 0; }
.frac-row.split { gap: .8rem; }
.frac-block { display: flex; align-items: center; gap: .35rem; font: 600 1.15rem ui-monospace, monospace; }
.frac { display: inline-flex; flex-direction: column; align-items: center; padding: 0 .3rem; }
.frac .num { padding: 0 .25rem .1rem; border-bottom: 2px solid #1d3557; }
.frac .den { padding: .1rem .25rem 0; }
.term { display: flex; align-items: center; gap: .3rem; padding: .5rem .6rem; border-radius: 10px;
background: #e8eef3; border: 1px solid #cdd9e3; opacity: 0; transform: translateY(6px);
transition: opacity .35s ease, transform .35s ease; }
.term.show { opacity: 1; transform: translateY(0); }
.term .root-tag { font: 600 .75rem system-ui, sans-serif; color: #457b9d; margin-right: .3rem; }
.sign { font: 700 1.15rem ui-monospace, monospace; color: #1d3557; }
.arrow { text-align: center; font-size: 1.4rem; color: #457b9d; margin: .2rem 0; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #0a7d33; }
.status.info { color: #444; font-weight: 500; font-size: .88rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Greatest common divisor, used to reduce a fraction to lowest terms.
function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { [a, b] = [b, a % b]; } return a || 1; }
// Approximate a decimal as a small fraction n/d (search denominators up to 12).
function toFraction(x, maxDen) {
maxDen = maxDen || 12;
for (let den = 1; den <= maxDen; den++) {
const num = x * den;
if (Math.abs(num - Math.round(num)) < 1e-6) {
let n = Math.round(num), d = den;
const g = gcd(n, d);
n /= g; d /= g;
if (d < 0) { d = -d; n = -n; }
return { n, d };
}
}
return { n: Math.round(x * 1000) / 1000, d: 1 };
}
// A few rational functions with simple, distinct real roots (numerator degree < denominator degree).
const PRESETS = [
{ roots: [1, -2], num: [3, 5], label: '(3x + 5) / [(x - 1)(x + 2)]' },
{ roots: [2, -1], num: [1, 5], label: '(x + 5) / [(x - 2)(x + 1)]' },
{ roots: [1, -2, 3], num: [-3, -6, -5], label: '(-3x^2 - 6x - 5) / [(x - 1)(x + 2)(x - 3)]' },
{ roots: [-1, 2, 4], num: [1, -2, 3], label: '(x^2 - 2x + 3) / [(x + 1)(x - 2)(x - 4)]' }
];
// Evaluate a polynomial given as an array of coefficients (highest degree first) at x.
function evalPoly(coeffs, x) { return coeffs.reduce((acc, c) => acc * x + c, 0); }
// Cover-up method: A_i = N(r_i) / product of (r_i - r_j) over every other root r_j.
function decompose(roots, num) {
const n = roots.length;
return roots.map((r, i) => {
let denom = 1;
for (let j = 0; j < n; j++) if (j !== i) denom *= (r - roots[j]);
return evalPoly(num, r) / denom;
});
}
function denomLabel(roots) {
return roots.map(r => r === 0 ? 'x' : (r > 0 ? ('(x - ' + r + ')') : ('(x + ' + Math.abs(r) + ')'))).join('');
}
function numLabel(num) {
const deg = num.length - 1;
const parts = [];
num.forEach((c, i) => {
if (c === 0) return;
const p = deg - i;
let term = p === 0 ? String(Math.abs(c)) : (Math.abs(c) === 1 ? '' : String(Math.abs(c))) + 'x' + (p > 1 ? ('^' + p) : '');
parts.push((c < 0 ? '-' : (parts.length ? '+' : '')) + term);
});
return parts.length ? parts.join(' ') : '0';
}
function fracHtml(numHtml, denHtml) {
return '<span class="frac"><span class="num">' + numHtml + '</span><span class="den">' + denHtml + '</span></span>';
}
let sel = document.getElementById('preset');
PRESETS.forEach((p, i) => {
const o = document.createElement('option'); o.value = i; o.textContent = p.label; sel.appendChild(o);
});
function renderOriginal() {
const p = PRESETS[sel.value];
document.getElementById('original').innerHTML = fracHtml(numLabel(p.num), denomLabel(p.roots));
}
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || '');
}
function clearSplit() {
document.getElementById('arrow').hidden = true;
const row = document.getElementById('splitRow');
row.hidden = true; row.innerHTML = '';
setStatus('Pick a function and press Decompose.', '');
}
function runDecompose() {
const p = PRESETS[sel.value];
const A = decompose(p.roots, p.num);
const row = document.getElementById('splitRow');
row.innerHTML = '';
A.forEach((a, i) => {
if (i > 0) {
const sign = document.createElement('span');
sign.className = 'sign'; sign.textContent = a < 0 ? '−' : '+';
row.appendChild(sign);
}
const frac = toFraction(Math.abs(a));
const term = document.createElement('div');
term.className = 'term';
const r = p.roots[i];
const denTxt = r === 0 ? 'x' : (r > 0 ? ('x - ' + r) : ('x + ' + Math.abs(r)));
term.innerHTML = '<span class="root-tag">' + i18nFmt('root %r%', { r: r }) + '</span>' +
fracHtml(String(frac.n), (frac.d === 1 ? denTxt : (frac.d + '(' + denTxt + ')')));
row.appendChild(term);
});
row.hidden = false;
document.getElementById('arrow').hidden = false;
requestAnimationFrame(() => {
[...row.querySelectorAll('.term')].forEach((el, i) => setTimeout(() => el.classList.add('show'), i * 220));
});
// Log the raw decimal coefficients for anyone checking the arithmetic by hand.
console.log(i18nFmt('Cover-up coefficients: %list%', { list: A.map((a, i) => 'A' + (i + 1) + '=' + a.toFixed(4)).join(', ') }));
setStatus(i18nFmt('Split into %n% term(s) — one fraction per root.', { n: A.length }), 'info');
}
sel.addEventListener('change', () => { renderOriginal(); clearSplit(); });
document.getElementById('decompose').addEventListener('click', runDecompose);
document.getElementById('reset').addEventListener('click', clearSplit);
renderOriginal();
clearSplit();
Comments
Loading comments...