From d4765b3788e7385a074cce5a03c10ef02fd0c014 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Sun, 14 Dec 2025 19:46:23 +0000 Subject: [PATCH] feat(v2-alpha): refactor binary simulator and introduce shared site layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite binary simulator with unified unsigned and two’s complement logic - Support dynamic bit widths from 4 to 64 with LSB-preserving resizing - Replace legacy unsigned-only scripts with a single maintainable implementation - Extract binary styles into dedicated CSS and add global site styling - Introduce shared header, footer, and base layout components - Migrate Binary page to BaseLayout and modular assets Signed-off-by: Alexander Lyall --- public/css/binary.css | 293 ++++++++++++ public/js/binary.js | 321 +++++++++++++ public/js/binary/unsigned-binary.js | 115 ----- public/js/tools/unsigned-binary.js | 72 --- public/scripts/binary.js | 301 ++++++++++++ public/styles/global.css | 75 +++ src/components/SiteFooter.astro | 6 + src/components/SiteHeader.astro | 15 + src/layouts/BaseLayout.astro | 33 +- src/pages/binary.astro | 690 +++------------------------- src/styles/base.css | 2 +- src/styles/md3-tokens.css | 2 +- 12 files changed, 1102 insertions(+), 823 deletions(-) create mode 100644 public/css/binary.css create mode 100644 public/js/binary.js delete mode 100644 public/js/binary/unsigned-binary.js delete mode 100644 public/js/tools/unsigned-binary.js create mode 100644 public/scripts/binary.js create mode 100644 public/styles/global.css create mode 100644 src/components/SiteFooter.astro create mode 100644 src/components/SiteHeader.astro diff --git a/public/css/binary.css b/public/css/binary.css new file mode 100644 index 0000000..645c5b7 --- /dev/null +++ b/public/css/binary.css @@ -0,0 +1,293 @@ +:root{ + --bg: #1f2027; + --panel2: rgba(255,255,255,.04); + --text: #e8e8ee; + --muted: #a9acb8; + --accent: #33ff7a; + --accent-dim: rgba(51,255,122,.15); + --line: rgba(255,255,255,.12); +} + +/* DSEG font (you already have these in public/fonts/) */ +@font-face{ + font-family: "DSEG7ClassicRegular"; + src: + url("/fonts/DSEG7Classic-Regular.woff") format("woff"), + url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +.wrap{ + max-width: 1200px; + margin: 0 auto; + padding: 32px 20px 60px; +} + +.topGrid{ + display:grid; + grid-template-columns: 1fr 340px; + gap: 28px; + align-items:start; +} + +.readout{ + background: transparent; + text-align:center; + padding: 10px 10px 0; +} + +.label{ + letter-spacing: .18em; + font-weight: 800; + color: var(--muted); + text-transform: uppercase; + font-size: 14px; + margin-top: 10px; +} + +.num{ + font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-weight: 400; + color: var(--accent); + text-shadow: 0 0 18px var(--accent-dim); +} + +.denary{ + font-size: 84px; + line-height: 1.0; + margin: 6px 0 16px; +} + +.binary{ + font-size: 62px; + letter-spacing: .12em; + line-height: 1.0; + margin: 6px 0 18px; +} + +.controls{ + margin-top: 10px; + display:flex; + gap: 12px; + justify-content:center; + flex-wrap:wrap; +} + +.btn{ + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.14); + color: #fff; + padding: 12px 14px; + border-radius: 12px; + font-weight: 800; + cursor: pointer; + min-width: 160px; +} +.btn:active{ transform: translateY(1px); } + +.divider{ + margin-top: 26px; + border-top: 1px solid var(--line); +} + +/* Right panels */ +.panelCol{ + display:flex; + flex-direction:column; + gap: 14px; +} + +.card{ + background: var(--panel2); + border: 1px solid rgba(255,255,255,.10); + border-radius: 14px; + padding: 14px; +} + +.cardTitle{ + letter-spacing: .18em; + font-weight: 900; + color: var(--muted); + text-transform: uppercase; + font-size: 12px; + margin: 0 0 10px; +} + +.hint{ + color: var(--muted); + font-size: 12px; + margin-top: 8px; + line-height: 1.35; +} + +.toggleRow{ + display:flex; + align-items:center; + justify-content:space-between; + gap: 10px; +} + +.toggleLabel{ + color: var(--text); + font-weight: 800; + font-size: 14px; +} + +/* Switch (re-used for mode + bits) */ +.switch{ + position: relative; + width: 56px; + height: 34px; + display:inline-block; + flex: 0 0 auto; +} +.switch input{ + opacity:0; + width:0; + height:0; +} +.slider{ + position:absolute; + inset:0; + background: rgba(255,255,255,.10); + border: 1px solid rgba(255,255,255,.14); + border-radius: 999px; + transition: .18s ease; +} +.slider::before{ + content:""; + position:absolute; + height: 28px; + width: 28px; + left: 3px; + top: 2px; + background: rgba(255,255,255,.92); + border-radius: 50%; + transition: .18s ease; +} +.switch input:checked + .slider{ + background: rgba(51,255,122,.20); + border-color: rgba(51,255,122,.55); +} +.switch input:checked + .slider::before{ + transform: translateX(22px); + background: var(--accent); +} + +/* Bit width controls */ +.bitWidthRow{ + display:grid; + grid-template-columns: 44px 1fr 44px; + gap: 10px; + align-items:center; +} + +.miniBtn{ + height: 44px; + width: 44px; + border-radius: 12px; + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.14); + color: #fff; + cursor:pointer; + font-weight: 900; + font-size: 18px; +} + +.bitInputWrap{ + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.14); + border-radius: 12px; + padding: 10px 12px; + display:flex; + align-items:center; + justify-content:space-between; + gap: 12px; +} + +.bitInputLabel{ + color: var(--muted); + font-size: 12px; + font-weight: 900; + letter-spacing: .18em; + text-transform: uppercase; +} + +.bitInput{ + width: 86px; + text-align:right; + background: transparent; + border: none; + outline: none; + color: var(--accent); + font-family: "DSEG7ClassicRegular", ui-monospace, monospace; + font-size: 28px; +} +.bitInput::-webkit-outer-spin-button, +.bitInput::-webkit-inner-spin-button{ + -webkit-appearance:none; + margin:0; +} + +/* ✅ Bits grid: wraps every 8 bits, NO horizontal scroll bar */ +.bits{ + margin-top: 26px; + padding-top: 22px; + display:grid; + grid-template-columns: repeat(8, minmax(90px, 1fr)); + gap: 18px; + align-items:end; + text-align:center; + + /* make absolutely sure we don't create a horizontal scrollbar */ + overflow-x: hidden; +} + +/* Bit tile */ +.bit{ + display:flex; + flex-direction:column; + align-items:center; + gap: 10px; + padding: 8px 4px; +} + +.bulb{ + width: 22px; + height: 22px; + border-radius: 50%; + background: rgba(255,255,255,.08); + border: 1px solid rgba(255,255,255,.12); + box-shadow: none; + margin-bottom: 4px; +} +.bulb.on{ + background: #ffd86b; + border-color: rgba(255,216,107,.7); + box-shadow: 0 0 18px rgba(255,216,107,.6); +} + +.bitVal{ + font-family: "DSEG7ClassicRegular", ui-monospace, monospace; + font-size: 30px; + color: var(--text); + opacity: .95; + line-height: 1; + min-height: 34px; +} + +/* Responsive */ +@media (max-width: 980px){ + .topGrid{ grid-template-columns: 1fr; } + .denary{ font-size: 72px; } + .binary{ font-size: 52px; } + .bits{ grid-template-columns: repeat(4, minmax(90px, 1fr)); } +} + +@media (max-width: 520px){ + .denary{ font-size: 62px; } + .binary{ font-size: 44px; } + .btn{ min-width: 140px; } +} diff --git a/public/js/binary.js b/public/js/binary.js new file mode 100644 index 0000000..0e65497 --- /dev/null +++ b/public/js/binary.js @@ -0,0 +1,321 @@ +// Binary simulator: unsigned + two's complement, 4–64 bits. +// Key fixes: +// - CSS moved to /public so dynamically-created switches & bulbs are styled. +// - Bits grid wraps into rows of 8 (CSS). +// - Binary readout wraps every 8 bits (JS -> adds \n). + +const bitsGrid = document.getElementById("bitsGrid"); +const denaryEl = document.getElementById("denaryNumber"); +const binaryEl = document.getElementById("binaryNumber"); + +const modeToggle = document.getElementById("modeToggle"); +const modeHint = document.getElementById("modeHint"); + +const bitsInput = document.getElementById("bitsInput"); +const btnUp = document.getElementById("btnBitsUp"); +const btnDown = document.getElementById("btnBitsDown"); + +const btnShiftL = document.getElementById("btnShiftLeft"); +const btnShiftR = document.getElementById("btnShiftRight"); +const btnCustBin = document.getElementById("btnCustomBinary"); +const btnCustDen = document.getElementById("btnCustomDenary"); + +let bitCount = clampInt(Number(bitsInput.value || 8), 4, 64); +bitsInput.value = String(bitCount); + +let isTwos = false; + +// bits[0] = MSB, bits[bitCount-1] = LSB +let bits = new Array(bitCount).fill(false); + +/* ----------------------------- + Helpers +----------------------------- */ +function clampInt(n, min, max){ + n = Number(n); + if (!Number.isFinite(n)) return min; + n = Math.floor(n); + return Math.max(min, Math.min(max, n)); +} + +function maxUnsigned(nBits){ + // nBits up to 64 -> use BigInt for correctness + return (1n << BigInt(nBits)) - 1n; +} + +function rangeTwos(nBits){ + const min = -(1n << BigInt(nBits - 1)); + const max = (1n << BigInt(nBits - 1)) - 1n; + return { min, max }; +} + +function bitsToBigIntUnsigned(){ + let v = 0n; + for (let i = 0; i < bitCount; i++){ + v = (v << 1n) + (bits[i] ? 1n : 0n); + } + return v; +} + +function bitsToBigIntTwos(){ + // Interpret current bit pattern as signed two's complement. + const unsigned = bitsToBigIntUnsigned(); + const signBit = bits[0] ? 1n : 0n; + + if (signBit === 0n) return unsigned; // positive + + // negative: unsigned - 2^n + const mod = 1n << BigInt(bitCount); + return unsigned - mod; +} + +function bigIntToBitsUnsigned(v){ + // v assumed 0..2^n-1 + const out = new Array(bitCount).fill(false); + let x = BigInt(v); + for (let i = bitCount - 1; i >= 0; i--){ + out[i] = (x & 1n) === 1n; + x >>= 1n; + } + return out; +} + +function bigIntToBitsTwos(v){ + // v assumed in signed range; convert to 0..2^n-1 representation + const mod = 1n << BigInt(bitCount); + let x = BigInt(v); + if (x < 0n) x = mod + x; + return bigIntToBitsUnsigned(x); +} + +function formatBinaryForReadout(){ + // Wrap every 8 bits into a new line; keep spaces between groups. + const raw = bits.map(b => (b ? "1" : "0")).join(""); + const groupsOf8 = raw.match(/.{1,8}/g) || [raw]; + return groupsOf8.join("\n"); +} + +/* ----------------------------- + UI build +----------------------------- */ +function buildBitsGrid(){ + bitsGrid.innerHTML = ""; + + for (let i = 0; i < bitCount; i++){ + const weightUnsigned = 1n << BigInt(bitCount - 1 - i); + const isMSB = i === 0; + + const bitEl = document.createElement("div"); + bitEl.className = "bit"; + + const bulb = document.createElement("div"); + bulb.className = "bulb"; + bulb.id = `bulb-${i}`; + bulb.setAttribute("aria-hidden", "true"); + + const val = document.createElement("div"); + val.className = "bitVal"; + // if in two's complement, show MSB as negative label visually + if (isTwos && isMSB) val.classList.add("msbNeg"); + val.textContent = weightUnsigned.toString(); // show magnitude only ( "-" is via CSS ) + + const label = document.createElement("label"); + label.className = "switch"; + label.setAttribute("aria-label", `Toggle bit ${i + 1}`); + + const input = document.createElement("input"); + input.type = "checkbox"; + input.dataset.index = String(i); + + const slider = document.createElement("span"); + slider.className = "slider"; + + label.appendChild(input); + label.appendChild(slider); + + bitEl.appendChild(bulb); + bitEl.appendChild(val); + bitEl.appendChild(label); + + bitsGrid.appendChild(bitEl); + } + + // Hook listeners after build + bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => { + input.addEventListener("change", () => { + const idx = Number(input.dataset.index); + bits[idx] = input.checked; + updateAll(); + }); + }); + + syncInputsToBits(); + updateAll(); +} + +function syncInputsToBits(){ + bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => { + const idx = Number(input.dataset.index); + input.checked = !!bits[idx]; + }); +} + +function syncBulbsToBits(){ + for (let i = 0; i < bitCount; i++){ + const bulb = document.getElementById(`bulb-${i}`); + if (bulb) bulb.classList.toggle("on", !!bits[i]); + } +} + +function updateModeHint(){ + modeHint.textContent = isTwos + ? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value." + : "Tip: In unsigned binary, all bits represent positive values."; +} + +function updateAll(){ + const denary = isTwos ? bitsToBigIntTwos() : bitsToBigIntUnsigned(); + denaryEl.textContent = denary.toString(); + binaryEl.textContent = formatBinaryForReadout(); + syncBulbsToBits(); +} + +/* ----------------------------- + Bit-count changes (preserve LSBs) +----------------------------- */ +function setBitCount(newCount){ + newCount = clampInt(newCount, 4, 64); + if (newCount === bitCount) return; + + // preserve LSB-aligned pattern: + // take from the right end of old bits, pad on the left with zeros. + const old = bits.slice(); + const newBits = new Array(newCount).fill(false); + + const take = Math.min(bitCount, newCount); + for (let i = 0; i < take; i++){ + // copy from LSB side + newBits[newCount - 1 - i] = old[bitCount - 1 - i]; + } + + bitCount = newCount; + bits = newBits; + bitsInput.value = String(bitCount); + + buildBitsGrid(); // rebuild with correct styling + rows +} + +/* ----------------------------- + Custom input +----------------------------- */ +function requestBinary(){ + const v = prompt(`Enter a ${bitCount}-bit binary number (0/1):`); + if (v === null) return; + + const clean = v.replace(/\s+/g, ""); + if (!/^[01]+$/.test(clean)){ + alert("Invalid input. Use only 0 and 1."); + return; + } + + const padded = clean.slice(-bitCount).padStart(bitCount, "0"); + bits = [...padded].map(ch => ch === "1"); + syncInputsToBits(); + updateAll(); +} + +function requestDenary(){ + const promptText = isTwos + ? `Enter a denary number (${rangeTwos(bitCount).min} to ${rangeTwos(bitCount).max}):` + : `Enter a denary number (0 to ${maxUnsigned(bitCount)}):`; + + const raw = prompt(promptText); + if (raw === null) return; + + // allow leading +/- and digits + if (!/^[+-]?\d+$/.test(raw.trim())){ + alert("Invalid input. Enter a whole number."); + return; + } + + const n = BigInt(raw.trim()); + + if (isTwos){ + const { min, max } = rangeTwos(bitCount); + if (n < min || n > max){ + alert(`Out of range. Enter between ${min} and ${max}.`); + return; + } + bits = bigIntToBitsTwos(n); + } else { + const maxU = maxUnsigned(bitCount); + if (n < 0n || n > maxU){ + alert(`Out of range. Enter between 0 and ${maxU}.`); + return; + } + bits = bigIntToBitsUnsigned(n); + } + + syncInputsToBits(); + updateAll(); +} + +/* ----------------------------- + Shifts +----------------------------- */ +function shiftLeft(){ + bits.shift(); + bits.push(false); + syncInputsToBits(); + updateAll(); +} + +function shiftRight(){ + if (isTwos){ + // arithmetic shift right (preserve sign bit) + const sign = bits[0]; + bits.pop(); + bits.unshift(sign); + } else { + // logical shift right + bits.pop(); + bits.unshift(false); + } + syncInputsToBits(); + updateAll(); +} + +/* ----------------------------- + Mode toggle +----------------------------- */ +function setModeTwos(on){ + isTwos = !!on; + updateModeHint(); + + // rebuild so MSB label shows "-" via CSS class + // (and keeps the same bit pattern) + buildBitsGrid(); +} + +/* ----------------------------- + Wire up UI controls +----------------------------- */ +modeToggle.addEventListener("change", () => setModeTwos(modeToggle.checked)); + +btnUp.addEventListener("click", () => setBitCount(bitCount + 1)); +btnDown.addEventListener("click", () => setBitCount(bitCount - 1)); + +bitsInput.addEventListener("change", () => setBitCount(Number(bitsInput.value))); + +btnShiftL.addEventListener("click", shiftLeft); +btnShiftR.addEventListener("click", shiftRight); + +btnCustBin.addEventListener("click", requestBinary); +btnCustDen.addEventListener("click", requestDenary); + +/* ----------------------------- + Init +----------------------------- */ +updateModeHint(); +buildBitsGrid(); +updateAll(); diff --git a/public/js/binary/unsigned-binary.js b/public/js/binary/unsigned-binary.js deleted file mode 100644 index e28ab9f..0000000 --- a/public/js/binary/unsigned-binary.js +++ /dev/null @@ -1,115 +0,0 @@ -// Browser-only script. Safe because it's loaded via - - + + + diff --git a/src/styles/base.css b/src/styles/base.css index 46400b8..f667474 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1,4 +1,4 @@ -/* src/styles/base.css */ +src/styles/base.css @import "./md3-tokens.css"; html, body{ height:100%; } body{ diff --git a/src/styles/md3-tokens.css b/src/styles/md3-tokens.css index beb9cf3..dff543c 100644 --- a/src/styles/md3-tokens.css +++ b/src/styles/md3-tokens.css @@ -1,4 +1,4 @@ -/* src/styles/md3-tokens.css */ +src/styles/md3-tokens.css /* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */ :root{ /* Typography */