Initial Commit
This commit is contained in:
836
standard-form-explorer.html
Normal file
836
standard-form-explorer.html
Normal file
@@ -0,0 +1,836 @@
|
||||
<!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 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> 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 ·
|
||||
<kbd>←</kbd> back · <kbd>P</kbd> play/pause · <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> · ${Math.abs(rem)} step${Math.abs(rem)!==1?'s':''} remaining`;
|
||||
el.className = 'dir-left';
|
||||
} else {
|
||||
el.innerHTML = `→ Moving <strong>right</strong> · ${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>
|
||||
Reference in New Issue
Block a user