Files
cabrits-math/standard-form-explorer.html
2026-03-01 18:50:29 -04:00

837 lines
30 KiB
HTML
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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Standard Form Explorer</title>
<style>
:root {
--primary: #1a237e;
--secondary: #3f51b5;
--accent: #e91e63;
--orange: #ff6f00;
--green: #2e7d32;
--digit-w: 68px;
--digit-h: 88px;
--digit-gap: 10px;
--dot-size: 18px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Tahoma, sans-serif;
background: #f0f2f8;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ── HEADER ───────────────────────────────────── */
header {
background: var(--primary);
color: white;
padding: 14px 28px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
header h1 { font-size: 1.4rem; letter-spacing: .3px; }
.mode-tabs { display: flex; gap: 8px; }
.tab {
background: rgba(255,255,255,.15);
border: 2px solid rgba(255,255,255,.4);
color: white;
padding: 8px 18px;
border-radius: 20px;
cursor: pointer;
font-size: .9rem;
font-family: inherit;
transition: all .2s;
}
.tab.active { background: white; color: var(--primary); border-color: white; font-weight: 700; }
.tab:hover:not(.active) { background: rgba(255,255,255,.25); }
/* ── MAIN ─────────────────────────────────────── */
main {
flex: 1;
max-width: 900px;
width: 100%;
margin: 0 auto;
padding: 20px 20px 32px;
display: flex;
flex-direction: column;
gap: 14px;
}
.card {
background: white;
border-radius: 12px;
padding: 18px 24px;
box-shadow: 0 2px 10px rgba(0,0,0,.09);
}
/* ── INPUT ────────────────────────────────────── */
.input-heading {
font-size: .78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .7px;
color: var(--secondary);
margin-bottom: 12px;
}
.input-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.input-row input {
font-size: 1.25rem;
padding: 10px 14px;
border: 2px solid #c5cae9;
border-radius: 8px;
outline: none;
font-family: inherit;
transition: border-color .2s;
}
.input-row input:focus { border-color: var(--secondary); }
#ordinary-input { max-width: 220px; }
#coeff-input { max-width: 140px; }
#power-input { max-width: 110px; }
.input-row label { font-size: 1.15rem; font-weight: 600; color: var(--primary); }
.btn-go {
background: var(--secondary);
color: white;
border: none;
padding: 11px 26px;
border-radius: 8px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
font-family: inherit;
transition: background .2s;
}
.btn-go:hover { background: var(--primary); }
.error-msg { color: #e53935; font-size: .92rem; margin-top: 8px; }
.hidden { display: none !important; }
/* ── DISPLAY ──────────────────────────────────── */
#display-card {
min-height: 260px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 24px;
}
#placeholder {
color: #bdbdbd;
font-size: 1.15rem;
text-align: center;
}
#step-info {
font-size: .88rem;
color: #9e9e9e;
min-height: 20px;
letter-spacing: .2px;
}
/* Digit row */
#number-row {
position: relative;
display: inline-flex;
align-items: flex-end;
gap: var(--digit-gap);
padding: 8px 16px 28px 16px;
}
.digit-box {
width: var(--digit-w);
height: var(--digit-h);
display: flex;
align-items: center;
justify-content: center;
font-size: 3.2rem;
font-weight: 700;
color: var(--primary);
background: var(--light);
border: 2px solid #c5cae9;
border-radius: 10px;
flex-shrink: 0;
transition: background .35s, border-color .35s, color .35s;
user-select: none;
}
.digit-box.dim {
color: #c0c0c0;
background: #f5f5f5;
border-color: #e0e0e0;
}
.digit-box.highlight {
background: #e3f2fd;
border-color: var(--secondary);
color: var(--primary);
}
/* Decimal dot */
#decimal-dot {
position: absolute;
bottom: 7px;
width: var(--dot-size);
height: var(--dot-size);
background: var(--accent);
border-radius: 50%;
transform: translateX(-50%);
transition: left .52s cubic-bezier(.34,1.56,.64,1), opacity .3s;
box-shadow: 0 2px 8px rgba(233,30,99,.45);
z-index: 5;
}
#decimal-dot.no-anim { transition: none; }
/* Direction label */
#direction-label {
font-size: .95rem;
font-weight: 600;
min-height: 22px;
}
.dir-left { color: var(--accent); }
.dir-right { color: var(--orange); }
.dir-done { color: var(--green); }
/* Power counter */
#power-row {
display: flex;
align-items: baseline;
gap: 5px;
font-size: 1.7rem;
color: #616161;
}
#power-row .base { font-weight: 700; color: var(--primary); }
#power-row sup { font-size: 2rem; font-weight: 800; color: var(--orange);
min-width: 38px; display: inline-block; }
#power-val { transition: all .15s; display: inline-block; }
#power-val.pop { animation: pop .28s ease; }
@keyframes pop {
0% { transform: scale(.5); }
65% { transform: scale(1.35); }
100%{ transform: scale(1); }
}
/* ── CONTROLS ─────────────────────────────────── */
#controls {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
padding: 14px 20px;
}
.ctrl {
padding: 12px 22px;
border: 2px solid;
border-radius: 9px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
font-family: inherit;
transition: all .15s;
}
.ctrl:disabled { opacity: .35; cursor: not-allowed; }
#btn-back { background: white; border-color: var(--secondary); color: var(--secondary); }
#btn-back:hover:not(:disabled) { background: var(--secondary); color: white; }
#btn-play { background: var(--green); border-color: var(--green); color: white; min-width: 115px; }
#btn-play:hover:not(:disabled) { background: #1b5e20; border-color: #1b5e20; }
#btn-step { background: var(--secondary); border-color: var(--secondary); color: white;
font-size: 1.05rem; padding: 12px 28px; }
#btn-step:hover:not(:disabled) { background: var(--primary); border-color: var(--primary); }
#btn-reset { background: white; border-color: #e53935; color: #e53935; }
#btn-reset:hover:not(:disabled) { background: #e53935; color: white; }
/* ── RESULT ───────────────────────────────────── */
#result-card {
text-align: center;
min-height: 90px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
}
#result-label { font-size: .82rem; color: #9e9e9e; text-transform: uppercase; letter-spacing: .5px; }
#result-value { font-size: 2.6rem; font-weight: 800; }
#result-value .r-coeff { color: var(--primary); }
#result-value .r-times { color: #616161; margin: 0 8px; }
#result-value .r-base { color: var(--primary); }
#result-value .r-exp { color: var(--accent); font-size: 1.9rem; }
#result-placeholder { color: #c5c5c5; font-size: 1rem; }
/* ── KEYBOARD HINT ────────────────────────────── */
#kb-hint {
text-align: center;
font-size: .78rem;
color: #b0b0b0;
padding: 4px 0 0;
}
#kb-hint kbd {
background: #ececec;
border: 1px solid #ccc;
border-radius: 4px;
padding: 1px 5px;
font-size: .75rem;
font-family: monospace;
}
/* ── RESPONSIVE ───────────────────────────────── */
@media (max-width: 580px) {
:root { --digit-w: 52px; --digit-h: 70px; --digit-gap: 6px; }
.digit-box { font-size: 2.4rem; }
#result-value { font-size: 1.9rem; }
}
</style>
</head>
<body>
<!-- ═══════════════════════════════════════════════ HEADER -->
<header>
<h1>🔢 Standard Form Explorer</h1>
<div class="mode-tabs">
<button class="tab active" id="tab-sf" onclick="setMode('toSF')">Ordinary → Standard Form</button>
<button class="tab" id="tab-ord" onclick="setMode('toOrd')">Standard Form → Ordinary</button>
</div>
</header>
<!-- ═══════════════════════════════════════════════ MAIN -->
<main>
<!-- INPUT: Ordinary → Standard Form -->
<div class="card" id="panel-sf">
<div class="input-heading">Enter an ordinary number</div>
<div class="input-row">
<input type="text" id="ordinary-input"
placeholder="e.g. 7438 or 0.0055"
maxlength="18" autocomplete="off">
<button class="btn-go" onclick="convertToSF()">Convert →</button>
</div>
<div class="error-msg hidden" id="err-sf"></div>
</div>
<!-- INPUT: Standard Form → Ordinary -->
<div class="card hidden" id="panel-ord">
<div class="input-heading">Enter a number in standard form &nbsp;A × 10<sup>n</sup></div>
<div class="input-row">
<input type="text" id="coeff-input" placeholder="A e.g. 3.6" maxlength="12" autocomplete="off">
<label>× 10<sup>n</sup> &nbsp; where n =</label>
<input type="number" id="power-input" placeholder="e.g. 4 or 3" autocomplete="off">
<button class="btn-go" onclick="convertToOrd()">Convert →</button>
</div>
<div class="error-msg hidden" id="err-ord"></div>
</div>
<!-- DISPLAY -->
<div class="card" id="display-card">
<div id="placeholder">Enter a number above and click <strong>Convert</strong></div>
<div id="step-info" class="hidden"></div>
<div id="number-row" class="hidden">
<div id="decimal-dot"></div>
</div>
<div id="direction-label" class="hidden"></div>
<div id="power-row" class="hidden">
<span>×</span>
<span class="base">10</span>
<sup><span id="power-val">0</span></sup>
</div>
</div>
<!-- CONTROLS -->
<div class="card" id="controls">
<button class="ctrl" id="btn-back" onclick="stepBack()" disabled>◀ Back</button>
<button class="ctrl" id="btn-play" onclick="togglePlay()" disabled>▶ Play</button>
<button class="ctrl" id="btn-step" onclick="stepForward()" disabled>Next Step ▶</button>
<button class="ctrl" id="btn-reset" onclick="resetAll()" disabled>↺ Reset</button>
</div>
<!-- RESULT -->
<div class="card" id="result-card">
<div id="result-placeholder">Result will appear here when steps are complete</div>
<div id="result-label" class="hidden"></div>
<div id="result-value" class="hidden"></div>
</div>
<!-- KEYBOARD HINT -->
<div id="kb-hint">
Keyboard: <kbd>Space</kbd> or <kbd></kbd> next step &nbsp;·&nbsp;
<kbd></kbd> back &nbsp;·&nbsp; <kbd>P</kbd> play/pause &nbsp;·&nbsp; <kbd>R</kbd> reset
</div>
</main>
<!-- ═══════════════════════════════════════════════ SCRIPT -->
<script>
'use strict';
// ─── STATE ────────────────────────────────────────────────────────────────────
const S = {
mode: 'toSF', // 'toSF' | 'toOrd'
digits: [], // char array, e.g. ['7','4','3','8']
decimalPos: 0, // current position of decimal point
initPos: 0, // starting position (reset target)
targetPos: 0, // where we want to get to
origPower: 0, // original power stored for toOrd mode
ready: false,
done: false,
playing: false,
timer: null,
};
// ─── MODE SWITCH ──────────────────────────────────────────────────────────────
function setMode(m) {
S.mode = m;
show('panel-sf', m === 'toSF');
show('panel-ord', m === 'toOrd');
id('tab-sf') .classList.toggle('active', m === 'toSF');
id('tab-ord').classList.toggle('active', m === 'toOrd');
resetAll();
}
// ─── HELPERS ──────────────────────────────────────────────────────────────────
const id = s => document.getElementById(s);
const show = (el, vis) => id(el).classList.toggle('hidden', !vis);
function setErr(panelId, msg) {
const el = id(panelId);
el.textContent = msg;
el.classList.toggle('hidden', !msg);
}
// ─── PARSING ──────────────────────────────────────────────────────────────────
/**
* Parse an ordinary-form string into { digits, decimalPos }.
* decimalPos = number of digits before the decimal point.
* "7438" → digits=['7','4','3','8'], decimalPos=4
* "0.0055" → digits=['0','0','0','5','5'], decimalPos=1
* "15.78" → digits=['1','5','7','8'], decimalPos=2
*/
function parseOrdinary(raw) {
let s = raw.trim().replace(/[\s,]/g, '');
if (s.startsWith('-')) throw new Error('Please enter a positive number.');
if (!/^\d*\.?\d+$/.test(s) || s === '') throw new Error('Invalid number. Try something like 7438 or 0.0055');
if (s.length > 16) throw new Error('Number too long (max 16 digits).');
let dot = s.indexOf('.');
let digits, decimalPos;
if (dot === -1) {
digits = s.split('');
decimalPos = s.length;
} else {
digits = (s.slice(0, dot) + s.slice(dot + 1)).split('');
decimalPos = dot;
}
if (digits.length === 0) throw new Error('Invalid number.');
return { digits, decimalPos };
}
/** Index of the first non-zero digit, or -1 if all zeros. */
function firstNonZero(digits) {
return digits.findIndex(d => d !== '0');
}
/** Target decimal position for standard form: just after first non-zero digit. */
function sfTargetPos(digits) {
const i = firstNonZero(digits);
return i === -1 ? 1 : i + 1;
}
/** Current power based on mode and current decimalPos. */
function currentPower() {
if (S.mode === 'toSF') {
return S.initPos - S.decimalPos; // increases as decimal moves left
} else {
return S.origPower - (S.decimalPos - S.initPos); // decreases toward 0
}
}
// ─── CONVERT BUTTONS ──────────────────────────────────────────────────────────
function convertToSF() {
setErr('err-sf', '');
try {
let { digits, decimalPos } = parseOrdinary(id('ordinary-input').value);
let target = sfTargetPos(digits);
S.origPower = 0; // not used in toSF
initDisplay(digits, decimalPos, target);
} catch(e) { setErr('err-sf', e.message); }
}
function convertToOrd() {
setErr('err-ord', '');
try {
let coeffRaw = id('coeff-input').value.trim();
let n = parseInt(id('power-input').value, 10);
if (!coeffRaw) throw new Error('Enter the coefficient A.');
if (isNaN(n)) throw new Error('Enter the power n as a whole number.');
if (Math.abs(n) > 12) throw new Error('Power too large for display (max ±12).');
if (n === 0) throw new Error('Power is 0 — the number is already in ordinary form.');
let { digits, decimalPos } = parseOrdinary(coeffRaw);
// Validate 1 ≤ A < 10
let A = parseFloat(coeffRaw);
if (isNaN(A) || A < 1 || A >= 10)
throw new Error(`Coefficient must satisfy 1 ≤ A < 10 (got ${coeffRaw}).`);
// decimalPos for a valid coefficient is always 1
decimalPos = 1;
let expandedDigits = [...digits];
let startPos, targetPos;
if (n > 0) {
// Move decimal RIGHT n steps — need trailing zeros
targetPos = decimalPos + n; // = 1 + n
startPos = decimalPos; // = 1
while (expandedDigits.length < targetPos) expandedDigits.push('0');
} else {
// Move decimal LEFT |n| steps — need leading zeros
let absN = Math.abs(n);
let zeros = Array(absN).fill('0');
expandedDigits = zeros.concat(expandedDigits);
startPos = decimalPos + absN; // = 1 + |n| (decimal is now deeper in)
targetPos = startPos + n; // = 1 + |n| + n = 1 (for n negative)
}
S.origPower = n;
initDisplay(expandedDigits, startPos, targetPos);
} catch(e) { setErr('err-ord', e.message); }
}
// ─── INITIALISE DISPLAY ───────────────────────────────────────────────────────
function initDisplay(digits, start, target) {
stopPlay();
S.digits = digits;
S.decimalPos = start;
S.initPos = start;
S.targetPos = target;
S.ready = true;
S.done = (start === target);
// Show display elements
show('placeholder', false);
show('step-info', true);
show('number-row', true);
show('direction-label', true);
show('power-row', true);
// Render
renderDigits();
refreshPower(false);
refreshDirection();
refreshStepInfo();
refreshButtons();
id('decimal-dot').style.opacity = '1';
// Clear result
show('result-placeholder', true);
show('result-label', false);
show('result-value', false);
// Position dot instantly (after DOM paints)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
placeDot(false);
refreshDigitColors();
if (S.done) revealResult();
});
});
}
// ─── RENDERING ────────────────────────────────────────────────────────────────
function renderDigits() {
const row = id('number-row');
row.querySelectorAll('.digit-box').forEach(el => el.remove());
S.digits.forEach((ch, i) => {
const box = document.createElement('div');
box.className = 'digit-box';
box.id = `db-${i}`;
box.textContent = ch;
row.insertBefore(box, id('decimal-dot'));
});
}
function refreshDigitColors() {
const fz = firstNonZero(S.digits);
S.digits.forEach((_, i) => {
const box = id(`db-${i}`);
if (!box) return;
box.className = 'digit-box';
if (i < fz) box.classList.add('dim'); // leading zeros
else if (i === S.targetPos - 1) box.classList.add('highlight'); // coefficient lead digit
});
}
function placeDot(animate = true) {
const row = id('number-row');
const dot = id('decimal-dot');
const boxes = row.querySelectorAll('.digit-box');
if (!boxes.length) return;
let leftPx;
const dp = S.decimalPos;
if (dp <= 0) {
leftPx = boxes[0].offsetLeft - 6;
} else if (dp >= boxes.length) {
const last = boxes[boxes.length - 1];
leftPx = last.offsetLeft + last.offsetWidth - 4;
} else {
const prev = boxes[dp - 1];
const next = boxes[dp];
leftPx = Math.round((prev.offsetLeft + prev.offsetWidth + next.offsetLeft) / 2);
}
if (!animate) {
dot.classList.add('no-anim');
dot.style.left = leftPx + 'px';
// Re-enable transitions after reflow
requestAnimationFrame(() => requestAnimationFrame(() => dot.classList.remove('no-anim')));
} else {
dot.style.left = leftPx + 'px';
}
// Hide dot if it's sitting at trailing end and we are done
dot.style.opacity = (S.done && dp >= boxes.length) ? '0' : '1';
}
function refreshPower(animate = true) {
const p = currentPower();
const el = id('power-val');
el.textContent = p;
if (animate) {
el.classList.remove('pop');
void el.offsetWidth; // reflow
el.classList.add('pop');
}
}
function refreshDirection() {
const el = id('direction-label');
const rem = S.targetPos - S.decimalPos;
if (!S.ready || S.done || rem === 0) {
el.textContent = '';
el.className = '';
return;
}
if (rem < 0) {
el.innerHTML = `← Moving <strong>left</strong> &nbsp;·&nbsp; ${Math.abs(rem)} step${Math.abs(rem)!==1?'s':''} remaining`;
el.className = 'dir-left';
} else {
el.innerHTML = `→ Moving <strong>right</strong> &nbsp;·&nbsp; ${rem} step${rem!==1?'s':''} remaining`;
el.className = 'dir-right';
}
}
function refreshStepInfo() {
const el = id('step-info');
const total = Math.abs(S.targetPos - S.initPos);
const done = Math.abs(S.decimalPos - S.initPos);
if (total === 0) {
el.textContent = 'Number is already in standard form position — power = 0';
} else {
el.textContent = `Step ${done} of ${total}`;
}
}
function refreshButtons() {
const atStart = S.decimalPos === S.initPos;
const atTarget = S.decimalPos === S.targetPos;
id('btn-back') .disabled = !S.ready || atStart;
id('btn-step') .disabled = !S.ready || atTarget;
id('btn-play') .disabled = !S.ready || atTarget;
id('btn-reset').disabled = !S.ready;
}
// ─── STEPPING ─────────────────────────────────────────────────────────────────
function stepForward() {
if (!S.ready || S.decimalPos === S.targetPos) return;
const dir = S.targetPos > S.initPos ? 1 : -1;
S.decimalPos += dir;
placeDot(true);
refreshPower(true);
refreshDigitColors();
refreshDirection();
refreshStepInfo();
if (S.decimalPos === S.targetPos) {
S.done = true;
stopPlay();
// Wait for dot transition to settle before showing result
setTimeout(revealResult, 560);
setTimeout(() => placeDot(false), 580); // update opacity if trailing
}
refreshButtons();
}
function stepBack() {
if (!S.ready || S.decimalPos === S.initPos) return;
if (S.done) {
S.done = false;
id('decimal-dot').style.opacity = '1';
show('result-placeholder', true);
show('result-label', false);
show('result-value', false);
}
const dir = S.targetPos > S.initPos ? -1 : 1;
S.decimalPos += dir;
placeDot(true);
refreshPower(true);
refreshDigitColors();
refreshDirection();
refreshStepInfo();
refreshButtons();
}
// ─── AUTO-PLAY ────────────────────────────────────────────────────────────────
function togglePlay() {
S.playing ? stopPlay() : startPlay();
}
function startPlay() {
if (!S.ready || S.done) return;
S.playing = true;
id('btn-play').textContent = '⏸ Pause';
id('btn-step').disabled = true;
id('btn-back').disabled = true;
S.timer = setInterval(() => {
stepForward();
if (S.done) stopPlay();
}, 950);
}
function stopPlay() {
S.playing = false;
clearInterval(S.timer);
S.timer = null;
id('btn-play').textContent = '▶ Play';
refreshButtons();
}
// ─── RESET ────────────────────────────────────────────────────────────────────
function resetAll() {
stopPlay();
S.ready = S.done = false;
show('placeholder', true);
show('step-info', false);
show('number-row', false);
show('direction-label', false);
show('power-row', false);
show('result-placeholder', true);
show('result-label', false);
show('result-value', false);
id('number-row').querySelectorAll('.digit-box').forEach(el => el.remove());
id('decimal-dot').style.opacity = '1';
id('power-val').textContent = '0';
id('step-info').textContent = '';
id('direction-label').textContent = '';
['btn-back','btn-play','btn-step','btn-reset'].forEach(b => id(b).disabled = true);
id('btn-play').textContent = '▶ Play';
}
// ─── RESULT ───────────────────────────────────────────────────────────────────
function revealResult() {
if (S.mode === 'toSF') {
const fz = firstNonZero(S.digits);
const sigDigs = S.digits.slice(fz === -1 ? 0 : fz);
const power = S.initPos - S.targetPos;
const coeff = buildCoeff(sigDigs);
id('result-label').textContent = 'Standard Form';
id('result-value').innerHTML =
`<span class="r-coeff">${coeff}</span>` +
`<span class="r-times">×</span>` +
`<span class="r-base">10</span>` +
`<sup class="r-exp">${power}</sup>`;
} else {
const ord = buildOrdinary(S.digits, S.targetPos);
const power = S.origPower;
const fz = firstNonZero(S.digits);
const sig = S.digits.slice(fz === -1 ? 0 : fz);
const coeff = buildCoeff(sig);
id('result-label').textContent = 'Ordinary Form';
id('result-value').innerHTML =
`<span class="r-coeff">${ord}</span>`;
}
show('result-placeholder', false);
show('result-label', true);
show('result-value', true);
// Animate result card briefly
const rc = id('result-card');
rc.style.transition = 'box-shadow .3s';
rc.style.boxShadow = `0 0 0 4px ${getComputedStyle(document.documentElement)
.getPropertyValue('--green').trim()}`;
setTimeout(() => rc.style.boxShadow = '', 1200);
}
/** Build coefficient string: "7.438", "5.5", "1.0" */
function buildCoeff(sigDigs) {
if (sigDigs.length === 0) return '0';
if (sigDigs.length === 1) return sigDigs[0] + '.0';
const rest = sigDigs.slice(1).join('').replace(/0+$/, '') || '0';
return sigDigs[0] + '.' + rest;
}
/** Build ordinary number string from digits + decimal position. */
function buildOrdinary(digits, pos) {
let s;
if (pos <= 0) {
s = '0.' + '0'.repeat(-pos) + digits.join('');
} else if (pos >= digits.length) {
s = digits.join('') + '0'.repeat(pos - digits.length);
} else {
s = digits.slice(0, pos).join('') + '.' + digits.slice(pos).join('');
}
// Trim leading zeros (keep one before decimal)
s = s.replace(/^0+(?=[1-9])/, '');
if (s.startsWith('.')) s = '0' + s;
// Trim trailing zeros after decimal
if (s.includes('.')) s = s.replace(/\.?0+$/, '');
return s || '0';
}
// ─── KEYBOARD ─────────────────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
if (['INPUT','TEXTAREA','SELECT'].includes(e.target.tagName)) return;
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); stepForward(); }
if (e.key === 'ArrowLeft') { e.preventDefault(); stepBack(); }
if (e.key.toLowerCase() === 'p') togglePlay();
if (e.key.toLowerCase() === 'r') resetAll();
});
id('ordinary-input').addEventListener('keydown', e => { if(e.key==='Enter') convertToSF(); });
id('coeff-input') .addEventListener('keydown', e => { if(e.key==='Enter') convertToOrd(); });
id('power-input') .addEventListener('keydown', e => { if(e.key==='Enter') convertToOrd(); });
</script>
</body>
</html>