From d4765b3788e7385a074cce5a03c10ef02fd0c014 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Sun, 14 Dec 2025 19:46:23 +0000 Subject: [PATCH 01/23] 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 */ From e6da9c8c98f33b3bc2ec0c50ca16a94aee16b811 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Sun, 14 Dec 2025 20:00:01 +0000 Subject: [PATCH 02/23] Tweaks to Binary UI Signed-off-by: Alexander Lyall --- public/css/tools/binary.css | 137 --------- public/favicon.svg | 9 - public/js/binary.js | 321 -------------------- public/scripts/binary.js | 388 +++++++++++-------------- public/styles.css | 96 ------ public/{css => styles}/binary.css | 113 ++++--- public/styles/{global.css => site.css} | 63 ++-- src/components/Footer.astro | 7 + src/components/Header.astro | 13 + src/components/SiteFooter.astro | 6 - src/components/SiteHeader.astro | 15 - src/layouts/BaseLayout.astro | 23 +- src/pages/binary.astro | 35 +-- src/scripts/unsignedBinary.js | 54 ---- src/styles/base.css | 52 ---- src/styles/binary.css | 68 ----- src/styles/fonts.css | 11 - src/styles/md3-tokens.css | 43 --- 18 files changed, 304 insertions(+), 1150 deletions(-) delete mode 100644 public/css/tools/binary.css delete mode 100644 public/favicon.svg delete mode 100644 public/js/binary.js delete mode 100644 public/styles.css rename public/{css => styles}/binary.css (73%) rename public/styles/{global.css => site.css} (53%) create mode 100644 src/components/Footer.astro create mode 100644 src/components/Header.astro delete mode 100644 src/components/SiteFooter.astro delete mode 100644 src/components/SiteHeader.astro delete mode 100644 src/scripts/unsignedBinary.js delete mode 100644 src/styles/base.css delete mode 100644 src/styles/binary.css delete mode 100644 src/styles/fonts.css delete mode 100644 src/styles/md3-tokens.css diff --git a/public/css/tools/binary.css b/public/css/tools/binary.css deleted file mode 100644 index f300e8f..0000000 --- a/public/css/tools/binary.css +++ /dev/null @@ -1,137 +0,0 @@ -/* ---------- DSEG7 font ---------- */ -/* Put your font file here: - public/fonts/DSEG7Classic-Regular.woff2 -*/ -@font-face { - font-family: "DSEG7ClassicRegular"; - src: url("/fonts/DSEG7Classic-Regular.woff2") format("woff2"); - font-display: swap; -} - -/* ---------- Layout ---------- */ -.tool-shell { - min-height: 100vh; - display: grid; - place-items: center; - padding: 1rem; - background: #0b0f14; - color: #e7eaf0; - font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; -} - -.tool-card { - width: min(1100px, 100%); - background: #111824; - border: 1px solid rgba(255,255,255,0.08); - border-radius: 18px; - padding: 1rem; -} - -.tool-header h1 { margin: 0 0 .25rem 0; font-size: 1.4rem; } -.tool-header p { margin: 0 0 1rem 0; opacity: .85; } - -.display-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: .75rem; - margin-bottom: .75rem; -} - -.display-box { - background: #0b0f14; - border: 1px solid rgba(255,255,255,0.08); - border-radius: 14px; - padding: .75rem; -} - -.display-label { font-size: .9rem; opacity: .8; margin-bottom: .25rem; } - -.sevenseg { - font-family: "DSEG7ClassicRegular", monospace; - font-size: clamp(2rem, 4vw, 3.2rem); - letter-spacing: 0.08em; - line-height: 1.1; -} - -/* Buttons under denary/binary (your request) */ -.actions { - display: flex; - flex-wrap: wrap; - gap: .5rem; - margin-bottom: 1rem; -} - -/* ---------- Simple MD3-ish buttons ---------- */ -.md3-btn { - border: 1px solid rgba(255,255,255,0.16); - background: rgba(255,255,255,0.06); - color: #e7eaf0; - padding: .6rem .9rem; - border-radius: 999px; - cursor: pointer; - font-weight: 600; -} -.md3-btn:hover { background: rgba(255,255,255,0.10); } -.md3-btn--tonal { background: rgba(255,255,255,0.10); } - -/* ---------- Switches row ---------- */ -.switch-row { - display: grid; - grid-template-columns: repeat(8, minmax(90px, 1fr)); - gap: .75rem; -} - -.switch-col { - display: grid; - justify-items: center; - gap: .35rem; -} - -.bit-label { opacity: .85; font-weight: 600; } - -/* ---------- “Light switch” rocker ---------- */ -.rocker { - position: relative; - width: 70px; - height: 46px; - display: inline-block; - user-select: none; -} - -.rocker input { - opacity: 0; - width: 0; - height: 0; -} - -.rocker-body { - position: absolute; - inset: 0; - border-radius: 12px; - background: #1a2331; - border: 1px solid rgba(255,255,255,0.14); - box-shadow: inset 0 0 0 2px rgba(0,0,0,0.35); -} - -/* the “toggle” */ -.rocker-body::after { - content: ""; - position: absolute; - left: 6px; - top: 6px; - width: 58px; - height: 18px; - border-radius: 10px; - background: rgba(255,255,255,0.20); - transition: transform 180ms ease, background 180ms ease; -} - -/* ON position */ -.rocker input:checked + .rocker-body::after { - transform: translateY(16px); - background: rgba(255,255,255,0.55); -} - -@media (max-width: 900px) { - .switch-row { grid-template-columns: repeat(4, minmax(90px, 1fr)); } -} diff --git a/public/favicon.svg b/public/favicon.svg deleted file mode 100644 index f157bd1..0000000 --- a/public/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/public/js/binary.js b/public/js/binary.js deleted file mode 100644 index 0e65497..0000000 --- a/public/js/binary.js +++ /dev/null @@ -1,321 +0,0 @@ -// 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/scripts/binary.js b/public/scripts/binary.js index 8effa3c..4733635 100644 --- a/public/scripts/binary.js +++ b/public/scripts/binary.js @@ -1,11 +1,4 @@ -/* Binary simulator for Computing:Box - - Wrap bits every 8 (CSS handles layout) - - Bit width 4..64 - - Unsigned + Two’s complement toggle (WORKING) - - Bulbs + toggle switches for each bit -*/ - -const bitsGrid = document.getElementById("bitsGrid"); +const bitsRows = document.getElementById("bitsRows"); const denaryEl = document.getElementById("denaryNumber"); const binaryEl = document.getElementById("binaryNumber"); @@ -21,281 +14,250 @@ const btnShiftRight = document.getElementById("btnShiftRight"); const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); -let bitCount = clampInt(Number(bitsInput?.value ?? 8), 4, 64); -let isTwos = false; +let bitCount = clampInt(Number(bitsInput.value || 8), 1, 64); +let bits = new Array(bitCount).fill(false); // index 0 = MSB -// bits[0] is MSB, bits[bitCount-1] is LSB -let bits = new Array(bitCount).fill(false); - -function clampInt(n, min, max) { +function clampInt(n, min, max){ n = Number(n); - if (!Number.isInteger(n)) n = min; + if (!Number.isFinite(n)) n = min; + n = Math.floor(n); return Math.max(min, Math.min(max, n)); } -function pow2(exp) { - // safe up to 2^63 in JS integer precision? (JS uses float) but our usage is display/control, ok. - return 2 ** exp; +function isTwos(){ + return !!modeToggle.checked; } -/* ----------------------------- - Build UI (bulbs + switches) ------------------------------ */ -function buildBits(count) { - bitCount = clampInt(count, 4, 64); - bits = resizeBits(bits, bitCount); +function msbValue(){ + return 2 ** (bitCount - 1); +} - bitsGrid.innerHTML = ""; +function unsignedValueAt(i){ + // i=0 is MSB + return 2 ** (bitCount - 1 - i); +} - for (let i = 0; i < bitCount; i++) { - const exp = bitCount - 1 - i; // MSB has highest exponent - const value = pow2(exp); +function computeUnsigned(){ + let sum = 0; + for (let i = 0; i < bitCount; i++){ + if (bits[i]) sum += unsignedValueAt(i); + } + return sum; +} - const bit = document.createElement("div"); - bit.className = "bit"; - bit.innerHTML = ` - -
${value}
- - `; +function computeDenary(){ + const u = computeUnsigned(); + if (!isTwos()) return u; - bitsGrid.appendChild(bit); + // Two's complement: + // if MSB is 1, value = unsigned - 2^n + if (bits[0]) return u - (2 ** bitCount); + return u; +} + +function bitsToString(){ + return bits.map(b => (b ? "1" : "0")).join(""); +} + +function updateModeHint(){ + if (isTwos()){ + modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value."; + } else { + modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; + } +} + +function buildBitsUI(){ + bitsRows.innerHTML = ""; + + // Build rows of 8 bits + const rowCount = Math.ceil(bitCount / 8); + + for (let r = 0; r < rowCount; r++){ + const row = document.createElement("div"); + row.className = "byteRow"; + + const start = r * 8; + const end = Math.min(start + 8, bitCount); + + for (let i = start; i < end; i++){ + const bitEl = document.createElement("div"); + bitEl.className = "bit"; + + // label: show -MSB in two's complement + const labelVal = (isTwos() && i === 0) ? -msbValue() : unsignedValueAt(i); + + bitEl.innerHTML = ` + +
${labelVal}
+ + `; + + row.appendChild(bitEl); + } + + bitsRows.appendChild(row); } - hookSwitches(); - syncUI(); -} - -function resizeBits(oldBits, newCount) { - // keep LSB end stable when changing bit width: - // align old bits to the right (LSB) - const out = new Array(newCount).fill(false); - const copy = Math.min(oldBits.length, newCount); - - for (let k = 0; k < copy; k++) { - // copy from end (LSB) - out[newCount - 1 - k] = oldBits[oldBits.length - 1 - k]; - } - return out; -} - -function hookSwitches() { - bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { + // Hook switches + bitsRows.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => { input.addEventListener("change", () => { const i = Number(input.dataset.index); bits[i] = input.checked; updateReadout(); - updateBulb(i); }); }); + + syncUI(); } -/* ----------------------------- - Mode toggle (Unsigned <-> Two’s) ------------------------------ */ -function setModeTwos(on) { - isTwos = !!on; - - 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."; - - // Just re-calc denary using current bit pattern - updateReadout(); -} - -modeToggle?.addEventListener("change", () => setModeTwos(modeToggle.checked)); - -/* ----------------------------- - Calculations ------------------------------ */ -function getUnsignedValue() { - let n = 0; - for (let i = 0; i < bitCount; i++) { - if (!bits[i]) continue; - const exp = bitCount - 1 - i; - n += pow2(exp); - } - return n; -} - -function getTwosValue() { - // MSB has negative weight - let n = 0; - for (let i = 0; i < bitCount; i++) { - if (!bits[i]) continue; - const exp = bitCount - 1 - i; - if (i === 0) n -= pow2(exp); // MSB - else n += pow2(exp); - } - return n; -} - -function getDenary() { - return isTwos ? getTwosValue() : getUnsignedValue(); -} - -function getBinaryString() { - return bits.map(b => (b ? "1" : "0")).join(""); -} - -/* ----------------------------- - UI updates ------------------------------ */ -function updateBulb(i) { - const bulb = document.getElementById(`bulb-${i}`); - if (bulb) bulb.classList.toggle("on", bits[i]); -} - -function updateReadout() { - denaryEl.textContent = String(getDenary()); - binaryEl.textContent = getBinaryString(); -} - -function syncUI() { - bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { +function syncUI(){ + // sync inputs + bitsRows.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => { const i = Number(input.dataset.index); input.checked = !!bits[i]; - updateBulb(i); }); + + // sync bulbs + for (let i = 0; i < bitCount; i++){ + const bulb = document.getElementById(`bulb-${i}`); + if (bulb) bulb.classList.toggle("on", !!bits[i]); + } + updateReadout(); } -/* ----------------------------- - Set from Binary / Denary ------------------------------ */ -function setFromBinary(bin) { +function updateReadout(){ + denaryEl.textContent = String(computeDenary()); + binaryEl.textContent = bitsToString(); +} + +function setFromBinary(bin){ const clean = String(bin).replace(/\s+/g, ""); if (!/^[01]+$/.test(clean)) return false; const padded = clean.slice(-bitCount).padStart(bitCount, "0"); - bits = [...padded].map(ch => ch === "1"); + for (let i = 0; i < bitCount; i++){ + bits[i] = padded[i] === "1"; + } + syncUI(); return true; } -function setFromDenary(n) { +function setFromDenary(n){ + n = Number(n); if (!Number.isInteger(n)) return false; - if (!isTwos) { - const min = 0; - const max = pow2(bitCount) - 1; - if (n < min || n > max) return false; + if (!isTwos()){ + // unsigned: 0 .. (2^n - 1) + const max = (2 ** bitCount) - 1; + if (n < 0 || n > max) return false; - // unsigned fill from MSB->LSB - let remaining = n; - bits = bits.map((_, i) => { - const exp = bitCount - 1 - i; - const value = pow2(exp); - if (remaining >= value) { - remaining -= value; - return true; + for (let i = 0; i < bitCount; i++){ + const v = unsignedValueAt(i); + if (n >= v){ + bits[i] = true; + n -= v; + } else { + bits[i] = false; } - return false; - }); - + } syncUI(); return true; } - // Two's complement bounds - const min = -pow2(bitCount - 1); - const max = pow2(bitCount - 1) - 1; + // two's complement: -(2^(n-1)) .. (2^(n-1)-1) + const min = -(2 ** (bitCount - 1)); + const max = (2 ** (bitCount - 1)) - 1; if (n < min || n > max) return false; - // Convert to raw unsigned representation: - // if negative, represent as 2^bitCount + n - let raw = n; - if (raw < 0) raw = pow2(bitCount) + raw; + // convert to unsigned representation + let u = n; + if (u < 0) u = (2 ** bitCount) + u; // wrap - let remaining = raw; - bits = bits.map((_, i) => { - const exp = bitCount - 1 - i; - const value = pow2(exp); - if (remaining >= value) { - remaining -= value; - return true; + for (let i = 0; i < bitCount; i++){ + const v = unsignedValueAt(i); + if (u >= v){ + bits[i] = true; + u -= v; + } else { + bits[i] = false; } - return false; - }); + } syncUI(); return true; } -/* ----------------------------- - Shifts ------------------------------ */ -function shiftLeft() { - // drop MSB, append 0 at LSB - bits.shift(); - bits.push(false); +function shiftLeft(){ + bits.shift(); // drop MSB + bits.push(false); // add LSB syncUI(); } -function shiftRight() { - // unsigned: logical shift right (prepend 0) - // twos: arithmetic shift right (prepend old MSB) - const msb = bits[0]; - bits.pop(); - bits.unshift(isTwos ? msb : false); +function shiftRight(){ + bits.pop(); // drop LSB + bits.unshift(false); // add MSB syncUI(); } -/* ----------------------------- - Bit width controls ------------------------------ */ -function applyBitCount(next) { - const v = clampInt(next, 4, 64); - bitsInput.value = String(v); - buildBits(v); +function setBitCount(newCount){ + newCount = clampInt(newCount, 4, 64); + if (newCount === bitCount) return; + + // preserve right-most bits (LSB side) when resizing + const old = bits.slice(); + const next = new Array(newCount).fill(false); + + const copy = Math.min(old.length, next.length); + for (let k = 0; k < copy; k++){ + // copy from end (LSB) + next[newCount - 1 - k] = old[old.length - 1 - k]; + } + + bitCount = newCount; + bits = next; + + bitsInput.value = String(bitCount); + buildBitsUI(); } -btnBitsUp?.addEventListener("click", () => applyBitCount(bitCount + 1)); -btnBitsDown?.addEventListener("click", () => applyBitCount(bitCount - 1)); +/* -------------------- events -------------------- */ -bitsInput?.addEventListener("change", () => { - applyBitCount(Number(bitsInput.value)); +modeToggle.addEventListener("change", () => { + updateModeHint(); + buildBitsUI(); // rebuild labels (MSB becomes negative/positive) }); -/* ----------------------------- - Buttons ------------------------------ */ -btnShiftLeft?.addEventListener("click", shiftLeft); -btnShiftRight?.addEventListener("click", shiftRight); +btnBitsUp.addEventListener("click", () => setBitCount(bitCount + 1)); +btnBitsDown.addEventListener("click", () => setBitCount(bitCount - 1)); -btnCustomBinary?.addEventListener("click", () => { - const v = prompt(`Enter a ${bitCount}-bit binary number:`); +bitsInput.addEventListener("change", () => setBitCount(Number(bitsInput.value))); + +btnShiftLeft.addEventListener("click", shiftLeft); +btnShiftRight.addEventListener("click", shiftRight); + +btnCustomBinary.addEventListener("click", () => { + const v = prompt(`Enter ${bitCount}-bit binary:`); if (v === null) return; - if (!setFromBinary(v)) alert("Invalid input. Use only 0 and 1."); + if (!setFromBinary(v)) alert("Invalid binary. Use only 0 and 1."); }); -btnCustomDenary?.addEventListener("click", () => { - const min = isTwos ? -pow2(bitCount - 1) : 0; - const max = isTwos ? (pow2(bitCount - 1) - 1) : (pow2(bitCount) - 1); +btnCustomDenary.addEventListener("click", () => { + const min = isTwos() ? -(2 ** (bitCount - 1)) : 0; + const max = isTwos() ? (2 ** (bitCount - 1)) - 1 : (2 ** bitCount) - 1; const v = prompt(`Enter a denary number (${min} to ${max}):`); if (v === null) return; - - const n = Number(v); - if (!Number.isInteger(n) || !setFromDenary(n)) { - alert(`Invalid input. Enter an integer from ${min} to ${max}.`); - } + if (!setFromDenary(Number(v))) alert("Invalid denary for current mode/bit width."); }); -/* ----------------------------- - INIT ------------------------------ */ -function init() { - // default mode: unsigned - setModeTwos(false); - modeToggle.checked = false; +/* -------------------- init -------------------- */ - // build initial bits - applyBitCount(bitCount); -} - -init(); +bitsInput.value = String(bitCount); +updateModeHint(); +buildBitsUI(); diff --git a/public/styles.css b/public/styles.css deleted file mode 100644 index 50ca124..0000000 --- a/public/styles.css +++ /dev/null @@ -1,96 +0,0 @@ -/* src/styles/md3-tokens.css */ -/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */ -:root{ - /* Typography */ - --font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; - /* Spacing + shape */ - --radius-1: 10px; - --radius-2: 16px; - --radius-3: 22px; - --shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06); - /* Color roles (keep simple) */ - --md-surface: #ffffff; - --md-surface-2: #f6f7fb; - --md-on-surface: #111318; - --md-primary: #2f6fed; /* calm blue */ - --md-on-primary: #ffffff; - --md-secondary: #5a5f72; /* muted */ - --md-on-secondary: #ffffff; - --md-tertiary: #0f766e; /* teal for "practical" tools */ - --md-on-tertiary: #ffffff; - --md-outline: #d7dbe7; - --md-success: #1a7f37; - --md-warning: #b54708; - --md-danger: #b42318; - /* Focus ring for accessibility */ - --md-focus: 0 0 0 3px rgba(47,111,237,.28); -} -@media (prefers-color-scheme: dark){ - :root{ - --md-surface: #0b0e14; - --md-surface-2: #121725; - --md-on-surface: #e8eaf2; - --md-primary: #9bb6ff; - --md-on-primary: #0b0e14; - --md-secondary: #b8bccd; - --md-on-secondary: #0b0e14; - --md-tertiary: #4fd1c5; - --md-on-tertiary: #0b0e14; - --md-outline: #2b3244; - --md-focus: 0 0 0 3px rgba(155,182,255,.25); - } -} - -/* src/styles/base.css */ -@import "./md3-tokens.css"; -html, body{ height:100%; } -body{ - margin:0; - font-family: var(--font-sans); - background: var(--md-surface-2); - color: var(--md-on-surface); -} -a{ color: var(--md-primary); text-decoration: none; } -a:hover{ text-decoration: underline; } -.container{ - max-width: 1100px; - margin: 0 auto; - padding: 16px; -} -.card{ - background: var(--md-surface); - border: 1px solid var(--md-outline); - border-radius: var(--radius-2); - box-shadow: var(--shadow-1); - padding: 16px; -} -.btn{ - display:inline-flex; - gap:8px; - align-items:center; - justify-content:center; - border-radius: 999px; - border: 1px solid var(--md-outline); - background: var(--md-surface); - color: var(--md-on-surface); - padding: 10px 14px; - font-weight: 600; - cursor: pointer; -} -.btn:hover{ filter: brightness(0.98); } -.btn:focus{ outline:none; box-shadow: var(--md-focus); } -.btn-primary{ - background: var(--md-primary); - color: var(--md-on-primary); - border-color: transparent; -} -.badge{ - display:inline-block; - padding: 2px 10px; - border-radius: 999px; - font-size: 12px; - border: 1px solid var(--md-outline); - background: var(--md-surface-2); -} -code, pre{ font-family: var(--font-mono); } diff --git a/public/css/binary.css b/public/styles/binary.css similarity index 73% rename from public/css/binary.css rename to public/styles/binary.css index 645c5b7..af70bde 100644 --- a/public/css/binary.css +++ b/public/styles/binary.css @@ -1,14 +1,4 @@ -: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 */ @font-face{ font-family: "DSEG7ClassicRegular"; src: @@ -19,10 +9,8 @@ font-display: swap; } -.wrap{ - max-width: 1200px; - margin: 0 auto; - padding: 32px 20px 60px; +.binaryWrap{ + padding-top: 8px; } .topGrid{ @@ -33,7 +21,6 @@ } .readout{ - background: transparent; text-align:center; padding: 10px 10px 0; } @@ -43,7 +30,7 @@ font-weight: 800; color: var(--muted); text-transform: uppercase; - font-size: 14px; + font-size: 13px; margin-top: 10px; } @@ -55,16 +42,16 @@ } .denary{ - font-size: 84px; - line-height: 1.0; - margin: 6px 0 16px; + font-size: 70px; /* smaller than before */ + line-height: 1; + margin: 6px 0 10px; } .binary{ - font-size: 62px; + font-size: 54px; /* smaller than before */ letter-spacing: .12em; - line-height: 1.0; - margin: 6px 0 18px; + line-height: 1; + margin: 6px 0 16px; } .controls{ @@ -81,7 +68,7 @@ color: #fff; padding: 12px 14px; border-radius: 12px; - font-weight: 800; + font-weight: 700; cursor: pointer; min-width: 160px; } @@ -92,7 +79,7 @@ border-top: 1px solid var(--line); } -/* Right panels */ +/* Right-side cards */ .panelCol{ display:flex; flex-direction:column; @@ -135,7 +122,7 @@ font-size: 14px; } -/* Switch (re-used for mode + bits) */ +/* Switch (reused for mode + bits) */ .switch{ position: relative; width: 56px; @@ -144,6 +131,7 @@ flex: 0 0 auto; } .switch input{ + position:absolute; opacity:0; width:0; height:0; @@ -176,7 +164,7 @@ background: var(--accent); } -/* Bit width controls */ +/* Bit-width control */ .bitWidthRow{ display:grid; grid-template-columns: 44px 1fr 44px; @@ -191,7 +179,7 @@ background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.14); color: #fff; - cursor:pointer; + cursor: pointer; font-weight: 900; font-size: 18px; } @@ -219,8 +207,8 @@ width: 86px; text-align:right; background: transparent; - border: none; - outline: none; + border:none; + outline:none; color: var(--accent); font-family: "DSEG7ClassicRegular", ui-monospace, monospace; font-size: 28px; @@ -231,63 +219,66 @@ 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)); +/* Bits: wrap every 8 (rows built in JS) */ +.bitsRows{ + margin-top: 22px; + display:flex; + flex-direction:column; gap: 18px; - align-items:end; - text-align:center; - - /* make absolutely sure we don't create a horizontal scrollbar */ - overflow-x: hidden; } -/* Bit tile */ +/* A row of up to 8 bits */ +.byteRow{ + display:flex; + justify-content:center; + gap: 18px; + flex-wrap:nowrap; +} + +/* A single bit */ .bit{ + width: 110px; display:flex; flex-direction:column; align-items:center; gap: 10px; - padding: 8px 4px; + padding: 6px 4px; } +/* Bulb emoji: bigger */ .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; + font-size: 30px; + line-height: 1; + opacity: .20; + filter: grayscale(1); + transform: translateY(2px); } .bulb.on{ - background: #ffd86b; - border-color: rgba(255,216,107,.7); - box-shadow: 0 0 18px rgba(255,216,107,.6); + opacity: 1; + filter: grayscale(0); + text-shadow: 0 0 16px rgba(255,216,107,.65); } .bitVal{ font-family: "DSEG7ClassicRegular", ui-monospace, monospace; font-size: 30px; color: var(--text); - opacity: .95; - line-height: 1; + opacity: .92; min-height: 34px; } -/* Responsive */ +/* Responsiveness */ @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)); } + .denary{ font-size: 62px; } + .binary{ font-size: 48px; } + .bit{ width: 96px; } } @media (max-width: 520px){ - .denary{ font-size: 62px; } - .binary{ font-size: 44px; } + .denary{ font-size: 56px; } + .binary{ font-size: 42px; } .btn{ min-width: 140px; } + .byteRow{ gap: 12px; } + .bit{ width: 86px; } } diff --git a/public/styles/global.css b/public/styles/site.css similarity index 53% rename from public/styles/global.css rename to public/styles/site.css index 79b8405..5fdd4a9 100644 --- a/public/styles/global.css +++ b/public/styles/site.css @@ -4,9 +4,11 @@ --text: #e8e8ee; --muted: #a9acb8; --line: rgba(255,255,255,.12); + --accent: #33ff7a; + --accent-dim: rgba(51,255,122,.15); } -*{ box-sizing:border-box; } +*{ box-sizing: border-box; } body{ margin:0; @@ -15,61 +17,62 @@ body{ color: var(--text); } -.site-header{ - border-bottom: 1px solid var(--line); - background: rgba(0,0,0,.12); - backdrop-filter: blur(6px); +.page{ + min-height: calc(100vh - 120px); } -.site-header__inner{ +.siteHeader{ + position: sticky; + top: 0; + z-index: 10; + background: rgba(0,0,0,.15); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(10px); +} + +.siteHeader__inner{ max-width: 1200px; margin: 0 auto; padding: 14px 20px; - display:flex; - align-items:center; - justify-content:space-between; - gap:16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; } .brand{ - text-decoration:none; color: var(--text); - font-weight: 900; + text-decoration: none; + font-weight: 800; letter-spacing: .02em; } .nav{ - display:flex; + display: flex; gap: 14px; - flex-wrap:wrap; + flex-wrap: wrap; + justify-content: flex-end; } -.nav a{ +.nav__link{ color: var(--muted); - text-decoration:none; + text-decoration: none; font-weight: 700; font-size: 14px; } -.nav a:hover{ color: var(--text); } +.nav__link:hover{ color: var(--text); } -.site-main{ - min-height: calc(100vh - 120px); -} - -.site-footer{ +.siteFooter{ border-top: 1px solid var(--line); - margin-top: 28px; background: rgba(0,0,0,.10); } -.site-footer__inner{ +.siteFooter__inner{ max-width: 1200px; margin: 0 auto; padding: 18px 20px; - display:flex; - justify-content:space-between; - gap:12px; - flex-wrap:wrap; + color: var(--muted); + font-size: 12px; + display: grid; + gap: 6px; } - -.muted{ color: var(--muted); font-size: 13px; } diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..443552a --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,7 @@ +
+ +
diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 0000000..7746fba --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,13 @@ + diff --git a/src/components/SiteFooter.astro b/src/components/SiteFooter.astro deleted file mode 100644 index 0948906..0000000 --- a/src/components/SiteFooter.astro +++ /dev/null @@ -1,6 +0,0 @@ -
- -
diff --git a/src/components/SiteHeader.astro b/src/components/SiteHeader.astro deleted file mode 100644 index eecd89e..0000000 --- a/src/components/SiteHeader.astro +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 7a7a8de..dfca60b 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,9 +1,10 @@ --- -import SiteHeader from "../components/SiteHeader.astro"; -import SiteFooter from "../components/SiteFooter.astro"; +import Header from "../components/Header.astro"; +import Footer from "../components/Footer.astro"; const { title = "Computing:Box" } = Astro.props; --- + @@ -11,16 +12,18 @@ const { title = "Computing:Box" } = Astro.props; {title} - - - - - + + -
+ + + + + +
+
- - +
diff --git a/src/pages/binary.astro b/src/pages/binary.astro index a1a0949..5f05b69 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -3,12 +3,11 @@ import BaseLayout from "../layouts/BaseLayout.astro"; --- - -
-
- +
+
+
Denary
@@ -27,11 +26,11 @@ import BaseLayout from "../layouts/BaseLayout.astro";
- -
+ +
- + -
-
+ + -
diff --git a/src/scripts/unsignedBinary.js b/src/scripts/unsignedBinary.js deleted file mode 100644 index 58abb25..0000000 --- a/src/scripts/unsignedBinary.js +++ /dev/null @@ -1,54 +0,0 @@ -let bits = [128,64,32,16,8,4,2,1]; -let state = Array(8).fill(0); - -const denaryEl = document.getElementById("denaryNumber"); -const binaryEl = document.getElementById("binaryNumber"); -const bitEls = document.querySelectorAll(".bit"); - -bitEls.forEach((el, i) => { - el.addEventListener("click", () => { - state[i] = state[i] ? 0 : 1; - el.classList.toggle("on"); - update(); - }); -}); - -function update() { - const denary = state.reduce((sum, bit, i) => sum + bit * bits[i], 0); - denaryEl.textContent = denary; - binaryEl.textContent = state.join(""); -} - -function requestBinary() { - const input = prompt("Enter 8-bit binary:"); - if (!/^[01]{8}$/.test(input)) return; - state = input.split("").map(Number); - bitEls.forEach((el,i)=>el.classList.toggle("on",state[i])); - update(); -} - -function requestDenary() { - const input = parseInt(prompt("Enter denary (0–255)"),10); - if (isNaN(input) || input < 0 || input > 255) return; - - let value = input; - state = bits.map(b => { - if (value >= b) { - value -= b; - return 1; - } - return 0; - }); - - bitEls.forEach((el,i)=>el.classList.toggle("on",state[i])); - update(); -} - -function shiftBinary(dir) { - if (dir === "left") state.shift(), state.push(0); - if (dir === "right") state.pop(), state.unshift(0); - bitEls.forEach((el,i)=>el.classList.toggle("on",state[i])); - update(); -} - -update(); diff --git a/src/styles/base.css b/src/styles/base.css deleted file mode 100644 index f667474..0000000 --- a/src/styles/base.css +++ /dev/null @@ -1,52 +0,0 @@ -src/styles/base.css -@import "./md3-tokens.css"; -html, body{ height:100%; } -body{ - margin:0; - font-family: var(--font-sans); - background: var(--md-surface-2); - color: var(--md-on-surface); -} -a{ color: var(--md-primary); text-decoration: none; } -a:hover{ text-decoration: underline; } -.container{ - max-width: 1100px; - margin: 0 auto; - padding: 16px; -} -.card{ - background: var(--md-surface); - border: 1px solid var(--md-outline); - border-radius: var(--radius-2); - box-shadow: var(--shadow-1); - padding: 16px; -} -.btn{ - display:inline-flex; - gap:8px; - align-items:center; - justify-content:center; - border-radius: 999px; - border: 1px solid var(--md-outline); - background: var(--md-surface); - color: var(--md-on-surface); - padding: 10px 14px; - font-weight: 600; - cursor: pointer; -} -.btn:hover{ filter: brightness(0.98); } -.btn:focus{ outline:none; box-shadow: var(--md-focus); } -.btn-primary{ - background: var(--md-primary); - color: var(--md-on-primary); - border-color: transparent; -} -.badge{ - display:inline-block; - padding: 2px 10px; - border-radius: 999px; - font-size: 12px; - border: 1px solid var(--md-outline); - background: var(--md-surface-2); -} -code, pre{ font-family: var(--font-mono); } diff --git a/src/styles/binary.css b/src/styles/binary.css deleted file mode 100644 index 6a03738..0000000 --- a/src/styles/binary.css +++ /dev/null @@ -1,68 +0,0 @@ -.binary-container { - max-width: 1100px; - margin: auto; - padding: 2rem; - color: #e0e0e0; -} - -.display { - text-align: center; -} - -.label { - font-size: 1.2rem; - opacity: 0.7; -} - -.number { - font-size: 3rem; - margin-bottom: 1rem; - color: #00ff66; -} - -.controls { - margin-top: 1rem; -} - -.controls button { - margin: 0.25rem; - padding: 0.6rem 1rem; - font-size: 1rem; -} - -.bits { - display: grid; - grid-template-columns: repeat(8, 1fr); - gap: 1rem; - margin-top: 3rem; -} - -.bit { - width: 40px; - height: 80px; - background: #333; - border-radius: 20px; - position: relative; - cursor: pointer; -} - -.bit::after { - content: ""; - width: 32px; - height: 32px; - background: #555; - border-radius: 50%; - position: absolute; - bottom: 6px; - left: 4px; - transition: all 0.2s ease; -} - -.bit.on { - background: #00c853; -} - -.bit.on::after { - bottom: 42px; - background: #eaffea; -} diff --git a/src/styles/fonts.css b/src/styles/fonts.css deleted file mode 100644 index 768cb39..0000000 --- a/src/styles/fonts.css +++ /dev/null @@ -1,11 +0,0 @@ -@font-face { - font-family: "DSEG7"; - src: url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); - font-weight: normal; - font-style: normal; -} - -.dseg { - font-family: "DSEG7", monospace; - letter-spacing: 0.15em; -} diff --git a/src/styles/md3-tokens.css b/src/styles/md3-tokens.css deleted file mode 100644 index dff543c..0000000 --- a/src/styles/md3-tokens.css +++ /dev/null @@ -1,43 +0,0 @@ -src/styles/md3-tokens.css -/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */ -:root{ - /* Typography */ - --font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; - /* Spacing + shape */ - --radius-1: 10px; - --radius-2: 16px; - --radius-3: 22px; - --shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06); - /* Color roles (keep simple) */ - --md-surface: #ffffff; - --md-surface-2: #f6f7fb; - --md-on-surface: #111318; - --md-primary: #2f6fed; /* calm blue */ - --md-on-primary: #ffffff; - --md-secondary: #5a5f72; /* muted */ - --md-on-secondary: #ffffff; - --md-tertiary: #0f766e; /* teal for "practical" tools */ - --md-on-tertiary: #ffffff; - --md-outline: #d7dbe7; - --md-success: #1a7f37; - --md-warning: #b54708; - --md-danger: #b42318; - /* Focus ring for accessibility */ - --md-focus: 0 0 0 3px rgba(47,111,237,.28); -} -@media (prefers-color-scheme: dark){ - :root{ - --md-surface: #0b0e14; - --md-surface-2: #121725; - --md-on-surface: #e8eaf2; - --md-primary: #9bb6ff; - --md-on-primary: #0b0e14; - --md-secondary: #b8bccd; - --md-on-secondary: #0b0e14; - --md-tertiary: #4fd1c5; - --md-on-tertiary: #0b0e14; - --md-outline: #2b3244; - --md-focus: 0 0 0 3px rgba(155,182,255,.25); - } -} From 002fbb8b6cfb2acdf0a797592b82248c0382a82f Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 11:19:51 +0000 Subject: [PATCH 03/23] Addition of increment/decrement buttons, addition of Auto Random button Signed-off-by: Alexander Lyall --- package.json | 4 +- public/scripts/binary.js | 263 ------------ src/components/Footer.astro | 30 +- src/components/Header.astro | 58 ++- src/components/tools/BinarySimulator.astro | 381 ----------------- src/layouts/BaseLayout.astro | 18 +- src/pages/binary.astro | 163 +++++--- src/scripts/binary.js | 390 ++++++++++++++++++ {public => src}/styles/binary.css | 165 +++++--- .../styles/site.css => src/styles/global.css | 65 +-- 10 files changed, 730 insertions(+), 807 deletions(-) delete mode 100644 public/scripts/binary.js delete mode 100644 src/components/tools/BinarySimulator.astro create mode 100644 src/scripts/binary.js rename {public => src}/styles/binary.css (66%) rename public/styles/site.css => src/styles/global.css (50%) diff --git a/package.json b/package.json index d51f503..27caf2c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "computing-box", + "name": "Computing:Box", "type": "module", - "version": "0.0.1", + "version": "2.0.0 Alpha", "scripts": { "dev": "astro dev", "build": "astro build", diff --git a/public/scripts/binary.js b/public/scripts/binary.js deleted file mode 100644 index 4733635..0000000 --- a/public/scripts/binary.js +++ /dev/null @@ -1,263 +0,0 @@ -const bitsRows = document.getElementById("bitsRows"); -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 btnBitsUp = document.getElementById("btnBitsUp"); -const btnBitsDown = document.getElementById("btnBitsDown"); - -const btnShiftLeft = document.getElementById("btnShiftLeft"); -const btnShiftRight = document.getElementById("btnShiftRight"); -const btnCustomBinary = document.getElementById("btnCustomBinary"); -const btnCustomDenary = document.getElementById("btnCustomDenary"); - -let bitCount = clampInt(Number(bitsInput.value || 8), 1, 64); -let bits = new Array(bitCount).fill(false); // index 0 = MSB - -function clampInt(n, min, max){ - n = Number(n); - if (!Number.isFinite(n)) n = min; - n = Math.floor(n); - return Math.max(min, Math.min(max, n)); -} - -function isTwos(){ - return !!modeToggle.checked; -} - -function msbValue(){ - return 2 ** (bitCount - 1); -} - -function unsignedValueAt(i){ - // i=0 is MSB - return 2 ** (bitCount - 1 - i); -} - -function computeUnsigned(){ - let sum = 0; - for (let i = 0; i < bitCount; i++){ - if (bits[i]) sum += unsignedValueAt(i); - } - return sum; -} - -function computeDenary(){ - const u = computeUnsigned(); - if (!isTwos()) return u; - - // Two's complement: - // if MSB is 1, value = unsigned - 2^n - if (bits[0]) return u - (2 ** bitCount); - return u; -} - -function bitsToString(){ - return bits.map(b => (b ? "1" : "0")).join(""); -} - -function updateModeHint(){ - if (isTwos()){ - modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value."; - } else { - modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; - } -} - -function buildBitsUI(){ - bitsRows.innerHTML = ""; - - // Build rows of 8 bits - const rowCount = Math.ceil(bitCount / 8); - - for (let r = 0; r < rowCount; r++){ - const row = document.createElement("div"); - row.className = "byteRow"; - - const start = r * 8; - const end = Math.min(start + 8, bitCount); - - for (let i = start; i < end; i++){ - const bitEl = document.createElement("div"); - bitEl.className = "bit"; - - // label: show -MSB in two's complement - const labelVal = (isTwos() && i === 0) ? -msbValue() : unsignedValueAt(i); - - bitEl.innerHTML = ` - -
${labelVal}
- - `; - - row.appendChild(bitEl); - } - - bitsRows.appendChild(row); - } - - // Hook switches - bitsRows.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => { - input.addEventListener("change", () => { - const i = Number(input.dataset.index); - bits[i] = input.checked; - updateReadout(); - }); - }); - - syncUI(); -} - -function syncUI(){ - // sync inputs - bitsRows.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => { - const i = Number(input.dataset.index); - input.checked = !!bits[i]; - }); - - // sync bulbs - for (let i = 0; i < bitCount; i++){ - const bulb = document.getElementById(`bulb-${i}`); - if (bulb) bulb.classList.toggle("on", !!bits[i]); - } - - updateReadout(); -} - -function updateReadout(){ - denaryEl.textContent = String(computeDenary()); - binaryEl.textContent = bitsToString(); -} - -function setFromBinary(bin){ - const clean = String(bin).replace(/\s+/g, ""); - if (!/^[01]+$/.test(clean)) return false; - - const padded = clean.slice(-bitCount).padStart(bitCount, "0"); - for (let i = 0; i < bitCount; i++){ - bits[i] = padded[i] === "1"; - } - - syncUI(); - return true; -} - -function setFromDenary(n){ - n = Number(n); - if (!Number.isInteger(n)) return false; - - if (!isTwos()){ - // unsigned: 0 .. (2^n - 1) - const max = (2 ** bitCount) - 1; - if (n < 0 || n > max) return false; - - for (let i = 0; i < bitCount; i++){ - const v = unsignedValueAt(i); - if (n >= v){ - bits[i] = true; - n -= v; - } else { - bits[i] = false; - } - } - syncUI(); - return true; - } - - // two's complement: -(2^(n-1)) .. (2^(n-1)-1) - const min = -(2 ** (bitCount - 1)); - const max = (2 ** (bitCount - 1)) - 1; - if (n < min || n > max) return false; - - // convert to unsigned representation - let u = n; - if (u < 0) u = (2 ** bitCount) + u; // wrap - - for (let i = 0; i < bitCount; i++){ - const v = unsignedValueAt(i); - if (u >= v){ - bits[i] = true; - u -= v; - } else { - bits[i] = false; - } - } - - syncUI(); - return true; -} - -function shiftLeft(){ - bits.shift(); // drop MSB - bits.push(false); // add LSB - syncUI(); -} - -function shiftRight(){ - bits.pop(); // drop LSB - bits.unshift(false); // add MSB - syncUI(); -} - -function setBitCount(newCount){ - newCount = clampInt(newCount, 4, 64); - if (newCount === bitCount) return; - - // preserve right-most bits (LSB side) when resizing - const old = bits.slice(); - const next = new Array(newCount).fill(false); - - const copy = Math.min(old.length, next.length); - for (let k = 0; k < copy; k++){ - // copy from end (LSB) - next[newCount - 1 - k] = old[old.length - 1 - k]; - } - - bitCount = newCount; - bits = next; - - bitsInput.value = String(bitCount); - buildBitsUI(); -} - -/* -------------------- events -------------------- */ - -modeToggle.addEventListener("change", () => { - updateModeHint(); - buildBitsUI(); // rebuild labels (MSB becomes negative/positive) -}); - -btnBitsUp.addEventListener("click", () => setBitCount(bitCount + 1)); -btnBitsDown.addEventListener("click", () => setBitCount(bitCount - 1)); - -bitsInput.addEventListener("change", () => setBitCount(Number(bitsInput.value))); - -btnShiftLeft.addEventListener("click", shiftLeft); -btnShiftRight.addEventListener("click", shiftRight); - -btnCustomBinary.addEventListener("click", () => { - const v = prompt(`Enter ${bitCount}-bit binary:`); - if (v === null) return; - if (!setFromBinary(v)) alert("Invalid binary. Use only 0 and 1."); -}); - -btnCustomDenary.addEventListener("click", () => { - const min = isTwos() ? -(2 ** (bitCount - 1)) : 0; - const max = isTwos() ? (2 ** (bitCount - 1)) - 1 : (2 ** bitCount) - 1; - - const v = prompt(`Enter a denary number (${min} to ${max}):`); - if (v === null) return; - if (!setFromDenary(Number(v))) alert("Invalid denary for current mode/bit width."); -}); - -/* -------------------- init -------------------- */ - -bitsInput.value = String(bitCount); -updateModeHint(); -buildBitsUI(); diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 443552a..2dbcdbd 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -1,7 +1,29 @@
-
+ + diff --git a/src/components/Header.astro b/src/components/Header.astro index 7746fba..6c42d5c 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -1,13 +1,57 @@ + + diff --git a/src/components/tools/BinarySimulator.astro b/src/components/tools/BinarySimulator.astro deleted file mode 100644 index 43d5ce1..0000000 --- a/src/components/tools/BinarySimulator.astro +++ /dev/null @@ -1,381 +0,0 @@ ---- -const { mode = "unsigned", defaultBits = 8 } = Astro.props; - -// For unsigned: min 1 bit, max 16 bits (tweak if you want) -const minBits = 4; -const maxBits = 16; -const initialBits = Math.min(Math.max(defaultBits, minBits), maxBits); ---- - -
-
-
- - -
- -
-
DENARY
-
0
- -
BINARY
-
00000000
-
- -
- - - - - -
-
Bits
-
- - {initialBits} - -
-
-
-
- -
-
- - - - diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index dfca60b..c813bc3 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -11,12 +11,6 @@ const { title = "Computing:Box" } = Astro.props; {title} - - - - - - @@ -27,3 +21,15 @@ const { title = "Computing:Box" } = Astro.props;
+ + diff --git a/src/pages/binary.astro b/src/pages/binary.astro index 5f05b69..7a9398c 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,75 +1,122 @@ --- -import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/binary.css"; + +// keeps JS in src/ and lets Vite/Astro bundle it properly +const scriptUrl = Astro.resolve("../scripts/binary.js"); --- - - + + + + + + Binary | Computing:Box -
-
- -
-
-
Denary
-
0
+ + -
Binary
-
00000000
+ + -
- - - - +
+
+ +
+
+
Denary
+
0
+ +
Binary
+
0
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
-
+ +
+
+
Mode
- - +
+
Unsigned
+ + + +
Two’s complement
+
+ +
+ Tip: In unsigned binary, all bits represent positive values. +
+
+ +
+
Bit width
+ +
+ + +
+
Bits
+ +
+ + +
+ +
Minimum 1 bit, maximum 64 bits.
+
+ +
+
+ + diff --git a/src/scripts/binary.js b/src/scripts/binary.js new file mode 100644 index 0000000..1b33d3b --- /dev/null +++ b/src/scripts/binary.js @@ -0,0 +1,390 @@ +/* Binary simulator (Unsigned + Two's complement) + - bits 1..64 + - wrap bits every 8 visually (CSS), no scrollbars + - bulbs always update in BOTH modes + - MSB label shows negative weight in two's complement (e.g. -128) +*/ + +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 btnBitsUp = document.getElementById("btnBitsUp"); +const btnBitsDown = document.getElementById("btnBitsDown"); + +const btnShiftLeft = document.getElementById("btnShiftLeft"); +const btnShiftRight = document.getElementById("btnShiftRight"); +const btnCustomBinary = document.getElementById("btnCustomBinary"); +const btnCustomDenary = document.getElementById("btnCustomDenary"); + +const btnClear = document.getElementById("btnClear"); +const btnDec1 = document.getElementById("btnDec1"); +const btnInc1 = document.getElementById("btnInc1"); +const btnAutoRandom = document.getElementById("btnAutoRandom"); + +let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); + +// state is MSB -> LSB +let bits = new Array(bitCount).fill(false); + +let autoTimer = null; + +function clampInt(n, min, max){ + n = Number(n); + if (!Number.isFinite(n)) return min; + n = Math.floor(n); + if (n < min) return min; + if (n > max) return max; + return n; +} + +function pow2Big(n){ + // n is number (0..63) + return 1n << BigInt(n); +} + +function isTwos(){ + return !!modeToggle?.checked; +} + +function getRange(){ + // returns { min: BigInt, max: BigInt, mod: BigInt } + const n = bitCount; + const mod = pow2Big(n); + if (!isTwos()){ + return { min: 0n, max: mod - 1n, mod }; + } + // two's: [-2^(n-1), 2^(n-1)-1] + if (n === 1){ + return { min: -1n, max: 0n, mod }; + } + const half = pow2Big(n - 1); + return { min: -half, max: half - 1n, mod }; +} + +function currentValueBig(){ + // interpret current bits as unsigned or two's complement signed + let unsigned = 0n; + for (let i = 0; i < bitCount; i++){ + if (!bits[i]) continue; + const shift = BigInt(bitCount - 1 - i); + unsigned += 1n << shift; + } + + if (!isTwos()){ + return unsigned; + } + + // signed decode + const signBit = bits[0]; + if (!signBit) return unsigned; + + const { mod } = getRange(); + return unsigned - mod; // two's complement negative +} + +function setFromUnsignedBig(u){ + // u in [0, 2^n-1] + const n = bitCount; + for (let i = 0; i < n; i++){ + const shift = BigInt(n - 1 - i); + bits[i] = ((u >> shift) & 1n) === 1n; + } +} + +function setFromValueBig(v){ + // v is signed depending on mode. We convert to bit pattern. + const { min, max, mod } = getRange(); + + if (v < min) v = min; + if (v > max) v = max; + + if (!isTwos()){ + setFromUnsignedBig(v); + return; + } + + // two's: if negative, add 2^n + let u = v; + if (u < 0n) u = u + mod; + setFromUnsignedBig(u); +} + +function buildBits(){ + bitsGrid.innerHTML = ""; + + for (let i = 0; i < bitCount; i++){ + const placePow = bitCount - 1 - i; + const unsignedWeight = pow2Big(placePow); + + // label weight depends on mode (MSB negative in two's) + let label = unsignedWeight.toString(); + if (isTwos() && i === 0){ + label = "-" + unsignedWeight.toString(); + } + + const bit = document.createElement("div"); + bit.className = "bit"; + bit.innerHTML = ` + +
${label}
+ + `; + + bitsGrid.appendChild(bit); + } + + // hook switches + bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { + input.addEventListener("change", () => { + const idx = Number(input.dataset.index); + bits[idx] = input.checked; + updateReadout(); + }); + }); + + syncUI(); +} + +function binaryStringGrouped(){ + const raw = bits.map(b => (b ? "1" : "0")).join(""); + // group every 8 from the RIGHT (LSB side) so long widths look sane + // Example: 11 bits -> 00000000 000 (as in your screenshot) + const groups = []; + for (let end = raw.length; end > 0; end -= 8){ + const start = Math.max(0, end - 8); + groups.unshift(raw.slice(start, end)); + } + return groups.join(" "); +} + +function updateModeHint(){ + if (!modeHint) return; + if (isTwos()){ + modeHint.textContent = "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; + } else { + modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; + } +} + +function updateReadout(){ + const v = currentValueBig(); + + // display + denaryEl.textContent = v.toString(); + binaryEl.textContent = binaryStringGrouped(); + + // bulbs update ALWAYS (mode should not affect bulb on/off) + for (let i = 0; i < bitCount; i++){ + const bulb = document.getElementById(`bulb-${i}`); + if (bulb) bulb.classList.toggle("on", bits[i]); + } +} + +function syncUI(){ + // sync switch positions + bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { + const idx = Number(input.dataset.index); + input.checked = !!bits[idx]; + }); + + updateReadout(); +} + +function clearBits(){ + bits = new Array(bitCount).fill(false); + syncUI(); +} + +function shiftLeft(){ + // logical left shift: drop MSB, add 0 at LSB + bits.shift(); + bits.push(false); + syncUI(); +} + +function shiftRight(){ + // logical right shift: drop LSB, add 0 at MSB + bits.pop(); + bits.unshift(false); + syncUI(); +} + +function setFromBinaryPrompt(){ + const v = prompt(`Enter binary (${bitCount} bits). Spaces allowed:`); + if (v === null) return; + + const clean = String(v).replace(/\s+/g, ""); + if (!/^[01]+$/.test(clean)){ + alert("Invalid input. Use only 0 and 1 (spaces allowed)."); + return; + } + + const padded = clean.slice(-bitCount).padStart(bitCount, "0"); + bits = [...padded].map(ch => ch === "1"); + syncUI(); +} + +function setFromDenaryPrompt(){ + const v = prompt(`Enter denary (${isTwos() ? "signed" : "unsigned"}).`); + if (v === null) return; + + // BigInt parse (handles negatives) + let n; + try { + n = BigInt(String(v).trim()); + } catch { + alert("Invalid number."); + return; + } + + setFromValueBig(n); + syncUI(); +} + +function stepBy(delta){ + const v = currentValueBig(); + const next = v + BigInt(delta); + + // wrap within valid range + const { min, max, mod } = getRange(); + + let wrapped = next; + if (!isTwos()){ + // unsigned wrap: modulo 2^n + wrapped = ((next % mod) + mod) % mod; + } else { + // signed wrap across [min..max] + const span = max - min + 1n; // equals 2^n + wrapped = next; + while (wrapped > max) wrapped -= span; + while (wrapped < min) wrapped += span; + } + + setFromValueBig(wrapped); + syncUI(); +} + +function autoRandomOnce(){ + // runs briefly then stops automatically + if (autoTimer){ + clearInterval(autoTimer); + autoTimer = null; + btnAutoRandom.textContent = "Auto Random"; + return; + } + + btnAutoRandom.textContent = "Auto Random (Running…)"; + const { min, max, mod } = getRange(); + + const start = Date.now(); + const durationMs = 1800; // short burst + const tickMs = 90; + + autoTimer = setInterval(() => { + const now = Date.now(); + if (now - start >= durationMs){ + clearInterval(autoTimer); + autoTimer = null; + btnAutoRandom.textContent = "Auto Random"; + return; + } + + // pick a random unsigned pattern 0..2^n-1 then interpret via mode + // (this keeps distribution consistent even for signed mode) + const r = randomBigIntBelow(mod); + setFromUnsignedBig(r); + syncUI(); + }, tickMs); +} + +function randomBigIntBelow(maxExclusive){ + // maxExclusive up to 2^64 + // Use crypto if available, otherwise fallback (still fine for teaching tool) + const n = bitCount; + + if (globalThis.crypto && crypto.getRandomValues){ + const bytes = Math.ceil(n / 8); + const buf = new Uint8Array(bytes); + + while (true){ + crypto.getRandomValues(buf); + let val = 0n; + for (const b of buf){ + val = (val << 8n) + BigInt(b); + } + + // mask extra bits + const extra = BigInt(bytes * 8 - n); + if (extra > 0n) val = val & ((1n << BigInt(n)) - 1n); + + if (val < maxExclusive) return val; + } + } + + // fallback + const maxNum = Number.MAX_SAFE_INTEGER; + let val = BigInt(Math.floor(Math.random() * maxNum)); + return val % maxExclusive; +} + +function setBitCount(nextCount){ + nextCount = clampInt(nextCount, 1, 64); + bitCount = nextCount; + bitsInput.value = String(bitCount); + + // preserve current value if possible by re-encoding it into new width + const v = currentValueBig(); + bits = new Array(bitCount).fill(false); + setFromValueBig(v); + + buildBits(); + updateModeHint(); +} + +function onModeChange(){ + updateModeHint(); + + // rebuild labels so MSB shows negative weight in two's mode + // preserve current *bit pattern* (not numeric), because students are toggling interpretation + const currentPattern = bits.slice(); + + buildBits(); + bits = currentPattern.slice(0, bitCount); + + // if length changed (shouldn't), pad + if (bits.length < bitCount){ + bits = bits.concat(new Array(bitCount - bits.length).fill(false)); + } + + // rebuild labels again (already done), then resync + syncUI(); +} + +/* ----------------- Hooks ----------------- */ +btnShiftLeft?.addEventListener("click", shiftLeft); +btnShiftRight?.addEventListener("click", shiftRight); +btnCustomBinary?.addEventListener("click", setFromBinaryPrompt); +btnCustomDenary?.addEventListener("click", setFromDenaryPrompt); + +btnClear?.addEventListener("click", clearBits); +btnDec1?.addEventListener("click", () => stepBy(-1)); +btnInc1?.addEventListener("click", () => stepBy(+1)); +btnAutoRandom?.addEventListener("click", autoRandomOnce); + +btnBitsUp?.addEventListener("click", () => setBitCount(bitCount + 1)); +btnBitsDown?.addEventListener("click", () => setBitCount(bitCount - 1)); +bitsInput?.addEventListener("change", () => setBitCount(Number(bitsInput.value))); + +modeToggle?.addEventListener("change", onModeChange); + +/* ----------------- Init ----------------- */ +updateModeHint(); +buildBits(); diff --git a/public/styles/binary.css b/src/styles/binary.css similarity index 66% rename from public/styles/binary.css rename to src/styles/binary.css index af70bde..dd8efc6 100644 --- a/public/styles/binary.css +++ b/src/styles/binary.css @@ -1,7 +1,20 @@ -/* Font */ +:root{ + --bg: #1f2027; + --panel: #22242d; + --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); +} + @font-face{ font-family: "DSEG7ClassicRegular"; src: + url("/fonts/DSEG7Classic-Regular.woff2") format("woff2"), url("/fonts/DSEG7Classic-Regular.woff") format("woff"), url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); font-weight: 400; @@ -9,8 +22,17 @@ font-display: swap; } -.binaryWrap{ - padding-top: 8px; +body{ + margin:0; + font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + background: var(--bg); + color: var(--text); +} + +.wrap{ + max-width: 1200px; + margin: 0 auto; + padding: 32px 20px 60px; } .topGrid{ @@ -21,6 +43,7 @@ } .readout{ + background: transparent; text-align:center; padding: 10px 10px 0; } @@ -41,25 +64,32 @@ text-shadow: 0 0 18px var(--accent-dim); } -.denary{ - font-size: 70px; /* smaller than before */ - line-height: 1; +.denaryValue{ + font-size: 72px; + line-height: 1.0; margin: 6px 0 10px; } -.binary{ - font-size: 54px; /* smaller than before */ - letter-spacing: .12em; - line-height: 1; - margin: 6px 0 16px; +.binaryValue{ + font-size: 52px; + letter-spacing: .10em; + line-height: 1.0; + margin: 6px 0 14px; } .controls{ margin-top: 10px; + display:flex; + flex-direction:column; + gap: 10px; + align-items:center; +} + +.controlRow{ display:flex; gap: 12px; justify-content:center; - flex-wrap:wrap; + flex-wrap:nowrap; } .btn{ @@ -68,22 +98,33 @@ color: #fff; padding: 12px 14px; border-radius: 12px; - font-weight: 700; + font-weight: 800; cursor: pointer; min-width: 160px; } .btn:active{ transform: translateY(1px); } +.btnPrimary{ + background: rgba(51,255,122,.18); + border-color: rgba(51,255,122,.45); + box-shadow: 0 0 0 1px rgba(51,255,122,.10) inset; +} + +.btnSpin{ + min-width: 120px; + font-size: 18px; + padding: 12px 10px; +} + .divider{ margin-top: 26px; border-top: 1px solid var(--line); } -/* Right-side cards */ .panelCol{ display:flex; flex-direction:column; - gap: 14px; + gap:14px; } .card{ @@ -113,7 +154,7 @@ display:flex; align-items:center; justify-content:space-between; - gap: 10px; + gap:10px; } .toggleLabel{ @@ -122,7 +163,6 @@ font-size: 14px; } -/* Switch (reused for mode + bits) */ .switch{ position: relative; width: 56px; @@ -131,7 +171,6 @@ flex: 0 0 auto; } .switch input{ - position:absolute; opacity:0; width:0; height:0; @@ -164,7 +203,6 @@ background: var(--accent); } -/* Bit-width control */ .bitWidthRow{ display:grid; grid-template-columns: 44px 1fr 44px; @@ -173,13 +211,13 @@ } .miniBtn{ - height: 44px; - width: 44px; - border-radius: 12px; + 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; + color:#fff; + cursor:pointer; font-weight: 900; font-size: 18px; } @@ -215,70 +253,83 @@ } .bitInput::-webkit-outer-spin-button, .bitInput::-webkit-inner-spin-button{ - -webkit-appearance:none; - margin:0; + -webkit-appearance: none; + margin: 0; } -/* Bits: wrap every 8 (rows built in JS) */ -.bitsRows{ +.actionGrid{ + display:grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} +.actionGrid .btn{ + min-width: 0; + width: 100%; +} + +/* -------- Bits grid: wrap every 8, centered when <8, no scrollbar -------- */ +.bits{ margin-top: 22px; - display:flex; - flex-direction:column; + padding-top: 18px; + + display: grid; + grid-template-columns: repeat(8, 92px); gap: 18px; + justify-content: center; + + width: fit-content; + max-width: 100%; + margin-left: auto; + margin-right: auto; } -/* A row of up to 8 bits */ -.byteRow{ - display:flex; - justify-content:center; - gap: 18px; - flex-wrap:nowrap; -} - -/* A single bit */ .bit{ - width: 110px; display:flex; flex-direction:column; align-items:center; gap: 10px; - padding: 6px 4px; + padding: 8px 4px; } -/* Bulb emoji: bigger */ +/* Bulb: emoji 💡 larger, grey when off, glowing when on */ .bulb{ - font-size: 30px; + font-size: 28px; /* bigger bulb */ line-height: 1; - opacity: .20; - filter: grayscale(1); + filter: grayscale(100%) brightness(.85); + opacity: .65; transform: translateY(2px); + user-select: none; } .bulb.on{ + filter: none; opacity: 1; - filter: grayscale(0); - text-shadow: 0 0 16px rgba(255,216,107,.65); + text-shadow: 0 0 18px rgba(255, 216, 107, .55); } +/* Bit place value */ .bitVal{ font-family: "DSEG7ClassicRegular", ui-monospace, monospace; font-size: 30px; color: var(--text); - opacity: .92; + opacity: .95; + line-height: 1; min-height: 34px; } -/* Responsiveness */ +/* Per-bit switch */ +.bit .switch{ + transform: scale(1.05); +} + @media (max-width: 980px){ .topGrid{ grid-template-columns: 1fr; } - .denary{ font-size: 62px; } - .binary{ font-size: 48px; } - .bit{ width: 96px; } + .denaryValue{ font-size: 64px; } + .binaryValue{ font-size: 46px; } + .btn{ min-width: 140px; } } @media (max-width: 520px){ - .denary{ font-size: 56px; } - .binary{ font-size: 42px; } - .btn{ min-width: 140px; } - .byteRow{ gap: 12px; } - .bit{ width: 86px; } + .bits{ + grid-template-columns: repeat(4, 92px); + } } diff --git a/public/styles/site.css b/src/styles/global.css similarity index 50% rename from public/styles/site.css rename to src/styles/global.css index 5fdd4a9..a71bbdf 100644 --- a/public/styles/site.css +++ b/src/styles/global.css @@ -1,14 +1,15 @@ :root{ --bg: #1f2027; - --panel: rgba(255,255,255,.04); + --panel: #22242d; + --panel2: rgba(255,255,255,.04); --text: #e8e8ee; --muted: #a9acb8; - --line: rgba(255,255,255,.12); --accent: #33ff7a; --accent-dim: rgba(51,255,122,.15); + --line: rgba(255,255,255,.12); } -*{ box-sizing: border-box; } +*{ box-sizing:border-box; } body{ margin:0; @@ -17,62 +18,68 @@ body{ color: var(--text); } -.page{ - min-height: calc(100vh - 120px); -} - .siteHeader{ position: sticky; top: 0; z-index: 10; background: rgba(0,0,0,.15); - border-bottom: 1px solid var(--line); - backdrop-filter: blur(10px); + backdrop-filter: blur(8px); + border-bottom: 1px solid rgba(255,255,255,.06); } -.siteHeader__inner{ +.siteHeaderInner{ max-width: 1200px; margin: 0 auto; padding: 14px 20px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 18px; + display:flex; + align-items:center; + justify-content:space-between; + gap: 16px; } .brand{ color: var(--text); - text-decoration: none; - font-weight: 800; - letter-spacing: .02em; + text-decoration:none; + font-weight: 900; + letter-spacing:.02em; } .nav{ - display: flex; + display:flex; gap: 14px; - flex-wrap: wrap; - justify-content: flex-end; + flex-wrap:wrap; + justify-content:flex-end; } - -.nav__link{ +.nav a{ color: var(--muted); - text-decoration: none; + text-decoration:none; font-weight: 700; font-size: 14px; } -.nav__link:hover{ color: var(--text); } +.nav a:hover{ color: var(--text); } + +.siteMain{ + min-height: calc(100vh - 140px); +} .siteFooter{ - border-top: 1px solid var(--line); + border-top: 1px solid rgba(255,255,255,.08); + margin-top: 32px; background: rgba(0,0,0,.10); } -.siteFooter__inner{ +.siteFooterInner{ max-width: 1200px; margin: 0 auto; - padding: 18px 20px; + padding: 18px 20px 26px; color: var(--muted); font-size: 12px; - display: grid; - gap: 6px; + line-height: 1.6; +} + +.footerTitle{ + color: var(--text); + opacity:.9; + font-weight: 800; + margin-bottom: 6px; } From ac585701a3e553f623e938fa678bc396facffb0b Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 11:30:59 +0000 Subject: [PATCH 04/23] Fix broken code Signed-off-by: Alexander Lyall --- src/layouts/BaseLayout.astro | 42 +-- src/pages/binary.astro | 178 ++++++------ src/scripts/binary.js | 527 ++++++++++++++++------------------- src/styles/binary.css | 212 ++++++-------- src/styles/site.css | 75 +++++ 5 files changed, 509 insertions(+), 525 deletions(-) create mode 100644 src/styles/site.css diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index c813bc3..9d59a34 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,7 +1,4 @@ --- -import Header from "../components/Header.astro"; -import Footer from "../components/Footer.astro"; - const { title = "Computing:Box" } = Astro.props; --- @@ -11,25 +8,34 @@ const { title = "Computing:Box" } = Astro.props; {title} + -
-
+ + +
-
+ +
+ +
- - diff --git a/src/pages/binary.astro b/src/pages/binary.astro index 7a9398c..384bca0 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,122 +1,106 @@ --- +import BaseLayout from "../layouts/BaseLayout.astro"; import "../styles/binary.css"; -// keeps JS in src/ and lets Vite/Astro bundle it properly -const scriptUrl = Astro.resolve("../scripts/binary.js"); +// ✅ Correct Astro v5 way: bundle script from src/ and get its final URL +import binaryScriptUrl from "../scripts/binary.js?url"; --- - - - - - - Binary | Computing:Box + +
+
+ +
+
+
Denary
+
0
- - +
Binary
+
0
- - - -
-
- -
-
-
Denary
-
0
- -
Binary
-
0
- - -
-
- - -
-
- - -
+ +
+
+ + +
+
+ +
- -
- - -
- -
- -
-
- - + +
+
diff --git a/src/scripts/binary.js b/src/scripts/binary.js index 1b33d3b..f89037f 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,10 +1,3 @@ -/* Binary simulator (Unsigned + Two's complement) - - bits 1..64 - - wrap bits every 8 visually (CSS), no scrollbars - - bulbs always update in BOTH modes - - MSB label shows negative weight in two's complement (e.g. -128) -*/ - const bitsGrid = document.getElementById("bitsGrid"); const denaryEl = document.getElementById("denaryNumber"); const binaryEl = document.getElementById("binaryNumber"); @@ -22,122 +15,57 @@ const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); const btnClear = document.getElementById("btnClear"); -const btnDec1 = document.getElementById("btnDec1"); -const btnInc1 = document.getElementById("btnInc1"); +const btnMinus1 = document.getElementById("btnMinus1"); +const btnPlus1 = document.getElementById("btnPlus1"); const btnAutoRandom = document.getElementById("btnAutoRandom"); let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); +let isTwos = Boolean(modeToggle?.checked); -// state is MSB -> LSB -let bits = new Array(bitCount).fill(false); - +let bits = new Array(bitCount).fill(false); // MSB at index 0 let autoTimer = null; function clampInt(n, min, max){ n = Number(n); if (!Number.isFinite(n)) return min; - n = Math.floor(n); - if (n < min) return min; - if (n > max) return max; - return n; + n = Math.trunc(n); + return Math.max(min, Math.min(max, n)); } -function pow2Big(n){ - // n is number (0..63) - return 1n << BigInt(n); -} - -function isTwos(){ - return !!modeToggle?.checked; -} - -function getRange(){ - // returns { min: BigInt, max: BigInt, mod: BigInt } - const n = bitCount; - const mod = pow2Big(n); - if (!isTwos()){ - return { min: 0n, max: mod - 1n, mod }; - } - // two's: [-2^(n-1), 2^(n-1)-1] - if (n === 1){ - return { min: -1n, max: 0n, mod }; - } - const half = pow2Big(n - 1); - return { min: -half, max: half - 1n, mod }; -} - -function currentValueBig(){ - // interpret current bits as unsigned or two's complement signed - let unsigned = 0n; +/* ---------------------------- + Label values (MSB..LSB) + Unsigned: [2^(n-1) ... 1] + Two's: [-2^(n-1), 2^(n-2) ... 1] +----------------------------- */ +function getLabelValues(){ + const vals = []; for (let i = 0; i < bitCount; i++){ - if (!bits[i]) continue; - const shift = BigInt(bitCount - 1 - i); - unsigned += 1n << shift; + const pow = bitCount - 1 - i; + let v = 2 ** pow; + if (isTwos && i === 0) v = -v; // ✅ MSB label becomes negative + vals.push(v); } - - if (!isTwos()){ - return unsigned; - } - - // signed decode - const signBit = bits[0]; - if (!signBit) return unsigned; - - const { mod } = getRange(); - return unsigned - mod; // two's complement negative -} - -function setFromUnsignedBig(u){ - // u in [0, 2^n-1] - const n = bitCount; - for (let i = 0; i < n; i++){ - const shift = BigInt(n - 1 - i); - bits[i] = ((u >> shift) & 1n) === 1n; - } -} - -function setFromValueBig(v){ - // v is signed depending on mode. We convert to bit pattern. - const { min, max, mod } = getRange(); - - if (v < min) v = min; - if (v > max) v = max; - - if (!isTwos()){ - setFromUnsignedBig(v); - return; - } - - // two's: if negative, add 2^n - let u = v; - if (u < 0n) u = u + mod; - setFromUnsignedBig(u); + return vals; } function buildBits(){ + // wrap every 8 bits + bitsGrid.style.setProperty("--cols", String(Math.min(8, bitCount))); + bitsGrid.innerHTML = ""; + const labelValues = getLabelValues(); for (let i = 0; i < bitCount; i++){ - const placePow = bitCount - 1 - i; - const unsignedWeight = pow2Big(placePow); - - // label weight depends on mode (MSB negative in two's) - let label = unsignedWeight.toString(); - if (isTwos() && i === 0){ - label = "-" + unsignedWeight.toString(); - } - const bit = document.createElement("div"); bit.className = "bit"; bit.innerHTML = ` -
${label}
-
+ + + + + diff --git a/src/scripts/binary.js b/src/scripts/binary.js index f89037f..d3b25bd 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,347 +1,353 @@ -const bitsGrid = document.getElementById("bitsGrid"); -const denaryEl = document.getElementById("denaryNumber"); -const binaryEl = document.getElementById("binaryNumber"); +// src/scripts/binary.js -const modeToggle = document.getElementById("modeToggle"); -const modeHint = document.getElementById("modeHint"); +document.addEventListener("DOMContentLoaded", () => { + const bitsGrid = document.getElementById("bitsGrid"); + const denaryEl = document.getElementById("denaryNumber"); + const binaryEl = document.getElementById("binaryNumber"); -const bitsInput = document.getElementById("bitsInput"); -const btnBitsUp = document.getElementById("btnBitsUp"); -const btnBitsDown = document.getElementById("btnBitsDown"); + const modeToggle = document.getElementById("modeToggle"); + const modeHint = document.getElementById("modeHint"); -const btnShiftLeft = document.getElementById("btnShiftLeft"); -const btnShiftRight = document.getElementById("btnShiftRight"); -const btnCustomBinary = document.getElementById("btnCustomBinary"); -const btnCustomDenary = document.getElementById("btnCustomDenary"); + const bitsInput = document.getElementById("bitsInput"); + const btnBitsUp = document.getElementById("btnBitsUp"); + const btnBitsDown = document.getElementById("btnBitsDown"); -const btnClear = document.getElementById("btnClear"); -const btnMinus1 = document.getElementById("btnMinus1"); -const btnPlus1 = document.getElementById("btnPlus1"); -const btnAutoRandom = document.getElementById("btnAutoRandom"); + const btnCustomBinary = document.getElementById("btnCustomBinary"); + const btnCustomDenary = document.getElementById("btnCustomDenary"); + const btnShiftLeft = document.getElementById("btnShiftLeft"); + const btnShiftRight = document.getElementById("btnShiftRight"); -let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); -let isTwos = Boolean(modeToggle?.checked); + const btnClear = document.getElementById("btnClear"); + const btnRandom = document.getElementById("btnRandom"); + const btnInc = document.getElementById("btnInc"); + const btnDec = document.getElementById("btnDec"); -let bits = new Array(bitCount).fill(false); // MSB at index 0 -let autoTimer = null; + let bitCount = clampInt(Number(bitsInput.value || 8), 1, 64); + let isTwos = false; -function clampInt(n, min, max){ - n = Number(n); - if (!Number.isFinite(n)) return min; - n = Math.trunc(n); - return Math.max(min, Math.min(max, n)); -} + // Bits stored MSB -> LSB (index 0 is MSB) + let bits = new Array(bitCount).fill(false); -/* ---------------------------- - Label values (MSB..LSB) - Unsigned: [2^(n-1) ... 1] - Two's: [-2^(n-1), 2^(n-2) ... 1] ------------------------------ */ -function getLabelValues(){ - const vals = []; - for (let i = 0; i < bitCount; i++){ - const pow = bitCount - 1 - i; - let v = 2 ** pow; - if (isTwos && i === 0) v = -v; // ✅ MSB label becomes negative - vals.push(v); - } - return vals; -} + // Random timer + let randomTimer = null; -function buildBits(){ - // wrap every 8 bits - bitsGrid.style.setProperty("--cols", String(Math.min(8, bitCount))); - - bitsGrid.innerHTML = ""; - const labelValues = getLabelValues(); - - for (let i = 0; i < bitCount; i++){ - const bit = document.createElement("div"); - bit.className = "bit"; - bit.innerHTML = ` - -
${labelValues[i]}
- - `; - bitsGrid.appendChild(bit); + 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)); } - // hook switches - bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { - input.addEventListener("change", () => { - const idx = Number(input.dataset.index); - bits[idx] = input.checked; - updateUI(); + function pow2(exp) { + // exp can be up to 63; JS Number is fine for display and basic use here + return 2 ** exp; + } + + function buildBits(count) { + bitsGrid.innerHTML = ""; + bits = new Array(count).fill(false); + bitCount = count; + + // Grid wrap at 8 bits per row; also center for small counts + if (count < 8) { + bitsGrid.classList.add("bitsFew"); + bitsGrid.style.setProperty("--cols", String(count)); + } else { + bitsGrid.classList.remove("bitsFew"); + bitsGrid.style.removeProperty("--cols"); + } + + for (let i = 0; i < count; i++) { + const isMSB = i === 0; + const valueUnsigned = pow2(count - 1 - i); // MSB is 2^(n-1) + + const bit = document.createElement("div"); + bit.className = "bit"; + + bit.innerHTML = ` + +
${valueUnsigned}
+ + `; + + bitsGrid.appendChild(bit); + } + + hookSwitches(); + updateModeLabels(); + updateReadout(); + } + + function hookSwitches() { + bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { + input.addEventListener("change", () => { + const i = Number(input.dataset.index); + bits[i] = input.checked; + updateReadout(); + }); }); - }); - - updateUI(); -} - -function setLabels(){ - const labelValues = getLabelValues(); - for (let i = 0; i < bitCount; i++){ - const el = document.getElementById(`label-${i}`); - if (el) el.textContent = String(labelValues[i]); } -} -function bitsToUnsigned(){ - let n = 0; - for (let i = 0; i < bitCount; i++){ - if (!bits[i]) continue; - const pow = bitCount - 1 - i; - n += 2 ** pow; - } - return n; -} + function updateModeLabels() { + isTwos = Boolean(modeToggle.checked); -function bitsToTwos(){ - // Two's complement interpretation - // value = -MSB*2^(n-1) + sum(other set bits) - let n = 0; - for (let i = 0; i < bitCount; i++){ - if (!bits[i]) continue; - const pow = bitCount - 1 - i; - const v = 2 ** pow; - if (i === 0) n -= v; - else n += v; - } - return n; -} + 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 getCurrentValue(){ - return isTwos ? bitsToTwos() : bitsToUnsigned(); -} + // Update the labels so the MSB shows negative weight in two's complement + for (let i = 0; i < bitCount; i++) { + const label = document.getElementById(`label-${i}`); + if (!label) continue; -function setFromUnsignedValue(n){ - // clamp to range of bitCount - const max = (2 ** bitCount) - 1; - n = clampInt(n, 0, max); + const unsignedWeight = pow2(bitCount - 1 - i); - for (let i = 0; i < bitCount; i++){ - const pow = bitCount - 1 - i; - const v = 2 ** pow; - if (n >= v){ - bits[i] = true; - n -= v; - } else { - bits[i] = false; + if (isTwos && i === 0) { + // MSB weight is negative + label.textContent = `-${unsignedWeight}`; + } else { + label.textContent = `${unsignedWeight}`; + } } } - syncSwitchesAndBulbs(); - updateUI(false); -} -function setFromTwosValue(n){ - // represent in two's complement with bitCount bits: - // allowed range: [-2^(n-1), 2^(n-1)-1] - const min = -(2 ** (bitCount - 1)); - const max = (2 ** (bitCount - 1)) - 1; - n = clampInt(n, min, max); + function formatBinaryString(raw) { + // group every 4 for readability (keeps your "0000 0000" look) + return raw.replace(/(.{4})/g, "$1 ").trim(); + } - // Convert to unsigned representation modulo 2^bitCount - const mod = 2 ** bitCount; - let u = ((n % mod) + mod) % mod; + function computeUnsignedValue() { + let value = 0; + for (let i = 0; i < bitCount; i++) { + if (!bits[i]) continue; + value += pow2(bitCount - 1 - i); + } + return value; + } - // then set bits from unsigned u - for (let i = 0; i < bitCount; i++){ - const pow = bitCount - 1 - i; - const v = 2 ** pow; - if (u >= v){ - bits[i] = true; - u -= v; - } else { - bits[i] = false; + function computeTwosValue() { + // If MSB is 0 -> same as unsigned + const msb = bits[0] ? 1 : 0; + let value = computeUnsignedValue(); + if (msb === 1) { + // subtract 2^n to get signed negative value + value -= pow2(bitCount); + } + return value; + } + + function updateReadout() { + // Binary string (MSB->LSB) + const rawBinary = bits.map((b) => (b ? "1" : "0")).join(""); + binaryEl.textContent = formatBinaryString(rawBinary); + + // Denary value based on mode + const denary = isTwos ? computeTwosValue() : computeUnsignedValue(); + denaryEl.textContent = String(denary); + + // Bulbs MUST update in BOTH modes (this was your reported bug) + for (let i = 0; i < bitCount; i++) { + const bulb = document.getElementById(`bulb-${i}`); + if (!bulb) continue; + bulb.classList.toggle("on", bits[i]); } } - syncSwitchesAndBulbs(); - updateUI(false); -} -function formatBinary(groupsOf = 4){ - const raw = bits.map(b => (b ? "1" : "0")).join(""); - // group for readability (keeps your “wrap every 8 bits” layout for switches; - // this just formats the readout) - let out = ""; - for (let i = 0; i < raw.length; i++){ - out += raw[i]; - const isLast = i === raw.length - 1; - if (!isLast && (i + 1) % groupsOf === 0) out += " "; - } - return out.trim(); -} - -function syncSwitchesAndBulbs(){ - // ✅ Bulbs always update (unsigned OR two's) - bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { - const idx = Number(input.dataset.index); - input.checked = Boolean(bits[idx]); - }); - - for (let i = 0; i < bitCount; i++){ - const bulb = document.getElementById(`bulb-${i}`); - if (bulb) bulb.classList.toggle("on", Boolean(bits[i])); - } -} - -function updateUI(sync = true){ - if (sync) syncSwitchesAndBulbs(); - - // labels update when mode changes - setLabels(); - - // readouts - const value = getCurrentValue(); - denaryEl.textContent = String(value); - binaryEl.textContent = formatBinary(4); - - // hint - if (isTwos){ - modeHint.textContent = "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; - } else { - modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; - } -} - -/* ---------------------------- - Controls ------------------------------ */ -btnShiftLeft?.addEventListener("click", () => { - // shift left: drop MSB, append 0 to LSB - bits.shift(); - bits.push(false); - updateUI(); -}); - -btnShiftRight?.addEventListener("click", () => { - // shift right: drop LSB, insert 0 at MSB - bits.pop(); - bits.unshift(false); - updateUI(); -}); - -btnClear?.addEventListener("click", () => { - bits = new Array(bitCount).fill(false); - updateUI(); -}); - -btnMinus1?.addEventListener("click", () => { - const v = getCurrentValue(); - if (isTwos) setFromTwosValue(v - 1); - else setFromUnsignedValue(v - 1); -}); - -btnPlus1?.addEventListener("click", () => { - const v = getCurrentValue(); - if (isTwos) setFromTwosValue(v + 1); - else setFromUnsignedValue(v + 1); -}); - -btnAutoRandom?.addEventListener("click", () => { - // stop if already running - if (autoTimer){ - clearInterval(autoTimer); - autoTimer = null; - btnAutoRandom.textContent = "Auto Random"; - return; + function syncInputs() { + bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { + const i = Number(input.dataset.index); + input.checked = Boolean(bits[i]); + }); + updateReadout(); } - btnAutoRandom.textContent = "Stop Random"; + function setAllBits(off = true) { + bits = bits.map(() => !off); + syncInputs(); + } - // run briefly then stop automatically - const start = Date.now(); - const durationMs = 2200; // auto stop + function shiftLeft() { + // left shift: drop MSB, append 0 at LSB + bits.shift(); + bits.push(false); + syncInputs(); + } - autoTimer = setInterval(() => { - const now = Date.now(); - if (now - start > durationMs){ - clearInterval(autoTimer); - autoTimer = null; - btnAutoRandom.textContent = "Auto Random"; + function shiftRight() { + // right shift: drop LSB, prepend 0 at MSB + bits.pop(); + bits.unshift(false); + syncInputs(); + } + + function setFromBinary(input) { + const clean = String(input).replace(/\s+/g, ""); + if (!/^[01]+$/.test(clean)) return false; + + const padded = clean.slice(-bitCount).padStart(bitCount, "0"); + bits = [...padded].map((ch) => ch === "1"); + syncInputs(); + return true; + } + + function setFromDenary(input) { + let n = Number(input); + if (!Number.isInteger(n)) return false; + + // For unsigned mode: allow 0..(2^n - 1) + // For two's mode: allow -(2^(n-1))..(2^(n-1)-1) + const maxUnsigned = pow2(bitCount) - 1; + const minTwos = -pow2(bitCount - 1); + const maxTwos = pow2(bitCount - 1) - 1; + + if (!isTwos) { + if (n < 0 || n > maxUnsigned) return false; + // build bits from unsigned n + bits = new Array(bitCount).fill(false); + for (let i = 0; i < bitCount; i++) { + const weight = pow2(bitCount - 1 - i); + if (n >= weight) { + bits[i] = true; + n -= weight; + } + } + syncInputs(); + return true; + } + + // Two's complement: convert signed integer to n-bit representation + if (n < minTwos || n > maxTwos) return false; + + let u = n; + if (u < 0) u = pow2(bitCount) + u; // wrap into unsigned range + const bin = u.toString(2).padStart(bitCount, "0"); + bits = [...bin].map((ch) => ch === "1"); + syncInputs(); + return true; + } + + function increment() { + // increment the underlying value in current mode, wrap appropriately + if (!isTwos) { + const max = pow2(bitCount) - 1; + let v = computeUnsignedValue(); + v = (v + 1) % (max + 1); + setFromDenary(v); return; } - // random within correct range for current mode - if (isTwos){ - const min = -(2 ** (bitCount - 1)); - const max = (2 ** (bitCount - 1)) - 1; - const n = Math.floor(Math.random() * (max - min + 1)) + min; - setFromTwosValue(n); - } else { - const max = (2 ** bitCount) - 1; - const n = Math.floor(Math.random() * (max + 1)); - setFromUnsignedValue(n); + const min = -pow2(bitCount - 1); + const max = pow2(bitCount - 1) - 1; + let v = computeTwosValue(); + v = v + 1; + if (v > max) v = min; // wrap + setFromDenary(v); + } + + function decrement() { + if (!isTwos) { + const max = pow2(bitCount) - 1; + let v = computeUnsignedValue(); + v = v - 1; + if (v < 0) v = max; + setFromDenary(v); + return; } - }, 90); -}); -btnCustomBinary?.addEventListener("click", () => { - 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 binary. Use only 0 and 1."); - return; + const min = -pow2(bitCount - 1); + const max = pow2(bitCount - 1) - 1; + let v = computeTwosValue(); + v = v - 1; + if (v < min) v = max; + setFromDenary(v); } - const padded = clean.slice(-bitCount).padStart(bitCount, "0"); - bits = [...padded].map(ch => ch === "1"); - updateUI(); -}); + function startAutoRandom() { + stopAutoRandom(); -btnCustomDenary?.addEventListener("click", () => { - const v = prompt(isTwos - ? `Enter a denary number (${-(2 ** (bitCount - 1))} to ${(2 ** (bitCount - 1)) - 1}):` - : `Enter a denary number (0 to ${(2 ** bitCount) - 1}):` - ); - if (v === null) return; + const durationMs = 1200; // runs briefly then stops + const tickMs = 90; - const n = Number(v); - if (!Number.isFinite(n) || !Number.isInteger(n)){ - alert("Invalid denary. Enter a whole number."); - return; + const start = Date.now(); + randomTimer = window.setInterval(() => { + // pick a random representable number depending on mode + let target; + if (!isTwos) { + target = Math.floor(Math.random() * (pow2(bitCount))); + } else { + const min = -pow2(bitCount - 1); + const max = pow2(bitCount - 1) - 1; + target = min + Math.floor(Math.random() * (max - min + 1)); + } + setFromDenary(target); + + if (Date.now() - start >= durationMs) stopAutoRandom(); + }, tickMs); } - if (isTwos) setFromTwosValue(n); - else setFromUnsignedValue(n); -}); + function stopAutoRandom() { + if (randomTimer !== null) { + window.clearInterval(randomTimer); + randomTimer = null; + } + } -/* ---------------------------- - Mode + Bit width ------------------------------ */ -modeToggle?.addEventListener("change", () => { - isTwos = Boolean(modeToggle.checked); - // keep the same underlying bit pattern; just reinterpret and relabel - updateUI(false); -}); + // MODE toggle + modeToggle.addEventListener("change", () => { + updateModeLabels(); + updateReadout(); + }); -btnBitsUp?.addEventListener("click", () => { - bitCount = clampInt(bitCount + 1, 1, 64); - bitsInput.value = String(bitCount); - bits = new Array(bitCount).fill(false); - buildBits(); -}); + // Bit width + btnBitsUp.addEventListener("click", () => { + const next = clampInt(bitCount + 1, 1, 64); + bitsInput.value = String(next); + buildBits(next); + }); -btnBitsDown?.addEventListener("click", () => { - bitCount = clampInt(bitCount - 1, 1, 64); - bitsInput.value = String(bitCount); - bits = new Array(bitCount).fill(false); - buildBits(); -}); + btnBitsDown.addEventListener("click", () => { + const next = clampInt(bitCount - 1, 1, 64); + bitsInput.value = String(next); + buildBits(next); + }); -bitsInput?.addEventListener("change", () => { - bitCount = clampInt(Number(bitsInput.value), 1, 64); - bitsInput.value = String(bitCount); - bits = new Array(bitCount).fill(false); - buildBits(); -}); + bitsInput.addEventListener("change", () => { + const next = clampInt(bitsInput.value, 1, 64); + bitsInput.value = String(next); + buildBits(next); + }); -/* ---------------------------- - Init ------------------------------ */ -buildBits(); + // Buttons + btnShiftLeft.addEventListener("click", shiftLeft); + btnShiftRight.addEventListener("click", shiftRight); + + btnCustomBinary.addEventListener("click", () => { + const val = prompt(`Enter a ${bitCount}-bit binary number:`); + if (val === null) return; + if (!setFromBinary(val)) alert("Invalid binary input (use only 0 and 1)."); + }); + + btnCustomDenary.addEventListener("click", () => { + const modeRange = isTwos + ? `(${ -pow2(bitCount - 1) } to ${ pow2(bitCount - 1) - 1 })` + : `(0 to ${ pow2(bitCount) - 1 })`; + + const val = prompt(`Enter a denary number ${modeRange}:`); + if (val === null) return; + if (!setFromDenary(val)) alert("Invalid denary input for the current mode/bit width."); + }); + + btnClear.addEventListener("click", () => setAllBits(true)); + btnRandom.addEventListener("click", startAutoRandom); + + btnInc.addEventListener("click", increment); + btnDec.addEventListener("click", decrement); + + // INIT + modeToggle.checked = false; + updateModeLabels(); + buildBits(bitCount); +}); diff --git a/src/styles/binary.css b/src/styles/binary.css index 0a17546..5c88b94 100644 --- a/src/styles/binary.css +++ b/src/styles/binary.css @@ -1,4 +1,14 @@ -/* DSEG7ClassicRegular font */ +:root{ + --bg: #1f2027; + --panel: #22242d; + --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); +} + @font-face{ font-family: "DSEG7ClassicRegular"; src: @@ -9,8 +19,17 @@ font-display: swap; } -.binary-wrap{ - padding-top: 6px; +body{ + margin:0; + font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + background: var(--bg); + color: var(--text); +} + +.wrap{ + max-width: 1200px; + margin: 0 auto; + padding: 32px 20px 60px; } .topGrid{ @@ -28,10 +47,10 @@ .label{ letter-spacing: .18em; - font-weight: 800; + font-weight: 700; color: var(--muted); text-transform: uppercase; - font-size: 13px; + font-size: 14px; margin-top: 10px; } @@ -42,41 +61,33 @@ text-shadow: 0 0 18px var(--accent-dim); } -.denary{ - font-size: 72px; /* smaller */ +.denaryValue{ + font-size: 70px; /* smaller than before */ line-height: 1.0; margin: 6px 0 10px; } -.binary{ - font-size: 46px; /* smaller */ +.binaryValue{ + font-size: 52px; /* smaller than before */ letter-spacing: .12em; - line-height: 1.15; + line-height: 1.0; margin: 6px 0 14px; - white-space: pre-wrap; - word-break: break-word; - display:inline-block; - text-align:center; + white-space: pre; /* keep spaces */ } -.controls{ - margin-top: 8px; +.controlsStack{ + margin-top: 10px; display:flex; - justify-content:center; - gap: 12px; - flex-wrap: wrap; -} - -.controls--twoRows{ - flex-direction: column; + flex-direction:column; gap: 10px; + align-items:center; } .controlsRow{ display:flex; gap: 12px; justify-content:center; - flex-wrap: wrap; + flex-wrap:wrap; } .btn{ @@ -85,29 +96,19 @@ color: #fff; padding: 12px 14px; border-radius: 12px; - font-weight: 800; + font-weight: 700; cursor: pointer; - min-width: 160px; + min-width: 170px; } - .btn:active{ transform: translateY(1px); } -.btn--green{ - background: rgba(51,255,122,.16); +.btnAccent{ + background: rgba(51,255,122,.18); border-color: rgba(51,255,122,.45); } -.btn--green:hover{ - background: rgba(51,255,122,.22); -} - -.btn--spin{ - min-width: 120px; - font-size: 18px; /* bigger */ -} - .divider{ - margin-top: 22px; + margin-top: 26px; border-top: 1px solid var(--line); } @@ -118,15 +119,15 @@ } .card{ - background: var(--panel); - border: 1px solid var(--panel-border); + background: var(--panel2); + border: 1px solid rgba(255,255,255,.10); border-radius: 14px; padding: 14px; } .cardTitle{ letter-spacing: .18em; - font-weight: 900; + font-weight: 800; color: var(--muted); text-transform: uppercase; font-size: 12px; @@ -149,16 +150,17 @@ .toggleLabel{ color: var(--text); - font-weight: 800; + font-weight: 700; font-size: 14px; } +/* Shared toggle switch (mode + bit switches) */ .switch{ - position:relative; - width:56px; - height:34px; + position: relative; + width: 56px; + height: 34px; display:inline-block; - flex:0 0 auto; + flex: 0 0 auto; } .switch input{ opacity:0; @@ -170,19 +172,19 @@ inset:0; background: rgba(255,255,255,.10); border: 1px solid rgba(255,255,255,.14); - border-radius:999px; - transition:.18s ease; + border-radius: 999px; + transition: .18s ease; } .slider::before{ content:""; position:absolute; - height:28px; - width:28px; - left:3px; - top:2px; + height: 28px; + width: 28px; + left: 3px; + top: 2px; background: rgba(255,255,255,.92); - border-radius:50%; - transition:.18s ease; + border-radius: 50%; + transition: .18s ease; } .switch input:checked + .slider{ background: rgba(51,255,122,.20); @@ -193,19 +195,47 @@ background: var(--accent); } -.bitWidthRow{ +/* Tools card layout */ +.toolRow{ display:grid; - grid-template-columns:44px 1fr 44px; - gap:10px; - align-items:center; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 10px; } +.toolRow2{ + display:grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.toolBtn{ + height: 48px; + border-radius: 12px; + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.14); + color: #fff; + cursor: pointer; + font-weight: 800; +} + +.toolSpin{ + font-size: 22px; /* bigger spin feature */ +} + +/* Bit width control */ +.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); + background:rgba(255,255,255,.06); + border:1px solid rgba(255,255,255,.14); color:#fff; cursor:pointer; font-weight:900; @@ -213,8 +243,8 @@ } .bitInputWrap{ - background: rgba(255,255,255,.06); - border: 1px solid rgba(255,255,255,.14); + background:rgba(255,255,255,.06); + border:1px solid rgba(255,255,255,.14); border-radius:12px; padding:10px 12px; display:flex; @@ -222,22 +252,20 @@ justify-content:space-between; gap:12px; } - .bitInputLabel{ - color: var(--muted); + color:var(--muted); font-size:12px; - font-weight:900; + font-weight:800; letter-spacing:.18em; text-transform:uppercase; } - .bitInput{ width:86px; text-align:right; - background: transparent; - border: none; - outline: none; - color: var(--accent); + background:transparent; + border:none; + outline:none; + color:var(--accent); font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size:28px; } @@ -247,17 +275,17 @@ margin:0; } -/* Bits: wrap every 8 */ -.bits{ - --cols: 8; - margin-top: 26px; - padding-top: 18px; +/* Bits area (wrap every 8 bits, centered) */ +.bitsWrap{ + margin-top: 22px; +} + +.bitsGrid{ display:grid; - grid-template-columns: repeat(var(--cols), 90px); - justify-content: center; /* centres when < 8 too */ gap: 18px; - align-items:end; - text-align:center; + justify-content:center; + grid-template-columns: repeat(8, minmax(90px, 1fr)); /* wraps at 8 */ + padding-top: 18px; } .bit{ @@ -266,32 +294,60 @@ align-items:center; gap: 10px; padding: 8px 4px; + text-align:center; } -/* Bulb emoji bigger */ +/* “Bulb like 💡” but consistent + bigger */ .bulb{ - font-size: 26px; /* bigger */ + width: 34px; /* bigger */ + height: 34px; /* bigger */ + border-radius: 50%; + background: rgba(255,255,255,.08); + border: 1px solid rgba(255,255,255,.12); + box-shadow: none; + display:flex; + align-items:center; + justify-content:center; + font-size: 20px; line-height: 1; + opacity: .55; +} +.bulb::before{ + content: "💡"; filter: grayscale(1); - opacity: .35; - transform: translateY(2px); } .bulb.on{ - filter: grayscale(0); opacity: 1; - text-shadow: 0 0 16px rgba(255,216,107,.55); + background: rgba(255,216,107,.18); + border-color: rgba(255,216,107,.55); + box-shadow: 0 0 18px rgba(255,216,107,.45); +} +.bulb.on::before{ + filter: grayscale(0); } +/* Bit value (MSB becomes negative in two’s mode via JS label text) */ .bitVal{ + font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size: 28px; color: var(--text); opacity: .95; line-height: 1; - min-height: 34px; + min-height: 32px; +} + +/* Make sure small bit counts still look centered/nice */ +.bitsGrid.bitsFew{ + grid-template-columns: repeat(var(--cols, 4), minmax(90px, 1fr)); } @media (max-width: 980px){ .topGrid{ grid-template-columns: 1fr; } - .denary{ font-size: 62px; } - .binary{ font-size: 40px; } + .denaryValue{ font-size: 62px; } + .binaryValue{ font-size: 46px; } +} + +@media (max-width: 520px){ + .btn{ min-width: 150px; } + .bitsGrid{ grid-template-columns: repeat(4, minmax(90px, 1fr)); } } From 1519032f5b6a2c88e3551cf46e1a70e59c370fc8 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 14:27:51 +0000 Subject: [PATCH 06/23] Fixed the dual bulb issue, added new font to typeface package Signed-off-by: Alexander Lyall --- public/fonts/Seven-Segment.woff | Bin 0 -> 8828 bytes public/fonts/Seven-Segment.woff2 | Bin 0 -> 6760 bytes src/pages/binary.astro | 31 +- src/scripts/binary.js | 671 +++++++++++++++++++------------ src/styles/binary.css | 115 ++++-- 5 files changed, 508 insertions(+), 309 deletions(-) create mode 100644 public/fonts/Seven-Segment.woff create mode 100644 public/fonts/Seven-Segment.woff2 diff --git a/public/fonts/Seven-Segment.woff b/public/fonts/Seven-Segment.woff new file mode 100644 index 0000000000000000000000000000000000000000..5a388a972668a42795a218737d3686d85071f186 GIT binary patch literal 8828 zcmbVyWmH`~6Yjy?-K`YY;_mJgcXy|_yStTxyUW3?xVu|{;!s?Q6}hMH{eIqGH)}na zoyjwk$VH|G$cq)K^6?&lucC{0}J;5cAR!l2Txu z#Q$`p02)AovN{Vp008j|tWyHR*keah%FMyU832HM1?z{wGNBNM|JcmKog4swT?2D4 z{{g81cGuF`$^ig?9s=_k!8{A~W|nd*6F0Cer2v=%|39Ex*?U_80C4930A45nV3PEG zi-OlI$;}HWJ8v{&7!?65sUb&124tViJennWX zu)yaE1_$r%a2NWuYKbtzL1D>Y1GxB+tO5VC@%`h(1r<7H2OIJ65q?V$L`ho!1;~zp zO#(oGy+;9b0ccK5vXWn7G}-`kmsBf0rlQ< z#U~LYS;xC9)x7ICt?nbHt0$2RWrgcpBL((pvP1QjDy3(QRJp2hlrOqnzw;s}Wf(GJ zIYzt&d9I#m33-3qr0yuN6{{>aaUctp()`g9EbKj*^-<_uV!3w{l!JJ|kaTwBC7?3Sjg59az7v z-OOOoT&S0KbqTt8N!p0znCES7?(rdRB%G(acn!;(2_jg9r^|1d7MK;-Od3z`+bRG`OXDfK54S7kt-_;^!^E} zKYZL>L%n6{X`|l&)O2DTNGHhjZBJ)_8EJj?b8G!3dVzJ^$Q>Cu6!aJ7!|VJe{g+ zD;DUc{~FHx7=_9sMH&<}>j%#MeU}S<5n!z2uttRz_u=;WC~El~Ro#sz1fp5&ZPfj5 z%{Kd=dcQ%Lv4lRtCIm&r$Hn<46K$^ei3$ZB~+nNaV7xk(2gAxwWsv)bf>=5n9fcQYPO&AZF2Ss8_ zVk@3uo=0aGmlo%XU7rD_LBUv>*kiOS#%*OkNk8SMttTf02?Pbqr_b7-y+ml0PEwrH zu1S(1Rbl6G=lM+`O!bCBhmK{&WJ_evlf1}nJ`vF9&`^`NC}k^lD!xQ2q)qX!3#?nX z>bn}aY8}KLV^+xpplv;Fql=QoMSlSr}D zqKsQ`M%oIl1NVh8A5154C(`}Xy~pX>Tz(J~^TJM1pu$JQI|CFctG>B2Bp}yWUG%?q zexV;|Pqgaa>p4nAr&2~k&G?-7RLl^E}E0ICA%1w-RBdwFEsg$tCxvC45|gP zmFQ`h0~fdybD6&XRZf8=YG^(Md90Dpt#1aQj4gapB*Z)G?A*$RsKqi?nKSvzU~MnI z{rGpAe>+(qMQ*QITNKE*O+x1VD70gg|M>R9H-wfL8~TRSTPG+2hl_~qf@qNAgq)e88w}Pu2fG}N6 zpND#YT2gp}bB{_Yj7gqD2;~mCn{z+*4Dh|^FqL~MxCkeOy9WL{UQIzwMFu`|rssvB zuKofhgsjd&T3<~62T8?ORQvE;LW=XUdeG-4r~0|xQNzNBc|^xleSOQ zS5Q>53H9x8(Wzd8sk-yP)n>!MJf(hsWq-B>o*q5ZVOgf!-zZ#TBBrEbpb2}=5EqO! z$TY{$Z=7}e<+|}y{Bi$i6=h%Em(c2x57A2Ix4N&8n+}w9qnR6*w29M<8y{)XuiMVU3(1tm+$WR zcEz_Z=cEx$k;N+*Ydu&BmsG9eC{=vfKf^$QbBvhI`$3f zoSx~^cIg|J3zVc+8ns09*8xqg4gw)08I7SsNE^fRsU-b0Xe!qeIjFt zU9!u_MJ(mF>BQ#*W-@| zScAFx*xPb@WxW~xo$)yv+y(|=c{+g#+_i{R0#8il+PBq0vbj;&4y1c8Kj zuwj@+{!&*FNYm_fj)t6VYdyVP96!oC9+MQbdDx&nY&aT__hCWb_`ZK@Un~*1Vi0wS z<8&Uv?@43~DjpaEm7t_@ASjqkA=I3dE1xYdM!0lttlhmSetXu|FbY^<%`;f39?iJ$ zkC{f;@$?Ja>9b@xq^3%v&1$6V>Hg^EmHD2?J7tXU41pURlT40+NPh^6IJ6uXFCXuR z;rLk!vUIUHqMR*k@>%JNelquT3I! z7O+*91gv^l5&k-E=2uS7{Cj!Y>1-jzi|M?(2r1wNh}Jd~2It^=zy1&#j#QH(^*L!jY@QXIU3^Bu>l_a6FWk3}C65Ii=h0(-LHC*g?s?aql|x-u zxXwSO9vGPqR?}p7VC^+TDHPh*>H8Ab^iE-UB@gK237d6NLIWLm`ekjhyp<#?kx~-Z znj@l19KVn_mlns*Sd&n$%Vl~=lp4#EBegflEMODyc)}5v0iB;5A_R1Gm_m3&j#mdD zcp-Vhcu{|QtF!1*0fx1c#aN3Vp`ixV)*l!?yHnLHEo+CU4F}l{6O>l-yC-+h! zwz=t-C{gUe2@lOc%g~zh`b{10##@v178Yx_3Hs82DQw{GmdlFadDHj?@U50c4S>~x zFnWo`=5InIRgF&65-vQ%6WC|%Fi!sNgDwS=ra*U;L=&%VqhXB;VM!SCv%NZ~{M9nt z=|1IGPC4|nA4a&f5?<|d_2*H(F_#P!p&2z%+%l^y*4kfRy@7jatt?;nbKzGG@Ax*v zJnNI3ZV*YK_j{fqvIVjkzG%<$^EBw1mi=6mmf5_+9`Wc72YNYgQ*4Vi(z`_9-%~XD zN%KlXa`W*HX`vC0h=v-3<{baDRIPZPoAbN)35#%1;6z$KRJRveoy1yvqRYt2)DOhg zrX%hG)FX`|;w41)w(76>y=#lgMRDV1aQEsM`%`kuImvnZ^r!-Pfx9roJRLlCckqg@ zbl1i0{X64iV(!$l1fssI%&h5aUBJ3jCdHzgm7R{4fJZqMOo6QWg#sK)btcNi9 zKr#=bmckzxl2U)-IYM-a4!c0OALbx0uKYV@c-wN_H5vAI0W3 zAf$&Wr(rvGE2Y!2P^O_R6|BHm21_nBFeBu3Y8mG{+)oHg5)dJO!H z0}0E03Z#t##DISXxY||qK}%?|10u#+^Q1oKmq)^SYW~ssQ&dhnF;7fc#+9#QtN~xy zxVT#Rp47Dlc5fLDsChKF*r&{D9I46J<06r7qXhf#h<9EjQvH5RH7_cpcha6{5RRyF zQmeVfRoWOJCoVwblr0o2U0279dMWv~?emJ~ghu&qCwaI^POnwPc2piE5;ndf81Fz0 zB%x!ai0RV82U0JIRrHE9*bPRhAfwBD78?vw!Ib`^j!p5M0({1bhZOr>#0f#;DrvWo zAYQo3Gm@fe5lAb*gFPG^kvHV1T)` zsz6R_WegoBJy>;^Z!hwh4jxTRk}ssS)7D%QqiXug9-LtCWK`RN`w-pDb!gDEr9Dpb z1;Q(2`zq7c%7~W3xu1g>$VeigP8Ol_=;SpI^lpYGi}huZd7CH7v)TI8nNp(}5aQ*=m#hJo*0eNYnqmXBDD>^qJx_s_|O_hPiU`AMsmedA;a0U zQ=*GCNFWuP`|g=(XHVq5KPV8WczHs4H;2MW9p2r(N=LfYoRXUKMH;+oQRz1z!KnB5 z0aGP8zOid_Y5XDp;CpyUa7N~i;mUJR>g6QNgxGG8EsWZ{lo-%;4zndb4STY8CoT9r zY|SvO1+Xu4kS}XE>N)njhE_YrnWCpt4 z%jSd|z7{NqJE=MLM7#AsSOxnOKXs~kp6BV=xE~_dif}*K{EWp~9?+(j8m@d6=P~}X z04$YMlSB3bs^;hm{5Yfu6p`dYqWl|7aq&wQX)b4ChlD0G!D&3R{u<3jsdQYL)YTg| zbFYFa2FvZ`$m^{@4URW_^M0#~aQ!oi?betE9on6N&19v(@L|fnYrjDKcg8}-shc7z zImuy07UFpKiMnQ;X3iS*Hf>K=#v^>Z<#$%pCQ}3^^{l)@N~DqUiEXo>2UmwC)lw8t zz1q4@!YjSF1zFz>@w4{ef&KG3&)esxo?47Y4Cl2Bsk|3I&@kG@^G)^M(+uS&7enLG z`D1Htj@FAy9%YQHyRzRszf}}-?)nZ>p}2HR+-ypTcJtPiyiBq)6M=iUxlvWDGy@1I(t|u*oB>1WTl_V|H>Ekys zkCoHCt5ffH|3pSYg8_m@F|3Q?=F=bF^dQBvrMt&;9oN(1QeG5b=%o_=P!Ae&h3pZ-PG~q4n5GT9b_L(vwUT)E_!~ng{PY3YMAQcCj~)4L`dp z({k>+r?nmW;cLioU1gHxr3zHnq;&}6ZiS>K7!!?Xb}2@>yy^25)k|z#%0dKc6H^D3 z-pxClch?Air{wOS#eN~LCL;ts-o|w_$L61o+^JX|c}34qD&Xn_mYu(M*9)K>>ZWr3 zP7Yz^IV#E>Q<(8cuUk=F*z(@ivM+N}%7K7@Qt|AZQSxa)*XR2+)xR~aW7vF<#v=l! z^6CzYS8Ka8tP+-3q#uqlZBXKZ!}}w4^DtkfuoYL}y|X7^F*uatwQTG{*?hmE;pCLR zCq8NVNF%+e^>1@2&zy4(;kTTJpD2t!hv~>LT(k!wcN_DX88+usdKTA2qA%@T`;TMXr4e`9bD>pf5{b{Qvt7s@bX3_dH z>G>t|$?3qU<@ZW1-;rVol(|8@<5pGIkrJQx%%-4Q&dm{VdAf5sht9(SHY;s?(-O73 z?!edQ+0l|zgZr=OCYePZSXYfE<_xJlzFH86+_FgLIq0yi9O(l<-&Stl@{uk8TMiDQ%(&2WvUEd*aB z?E8)F{`FA5iC;p@bf-<}6Nj_-)6XLTxfMn{>)ndIy*fa9|jM(jomame*Za=jrcC;ZUFme3!le8&oe@|ioCyvvD$R( zz|&HD*_fda9-aG`Z>@7eHI4UDfg4GiX(E11Klvm zM@|qH?Xo{B-@CYHXXO53U52}o;vWC`OIt&RrSAl!eDZWl*2xCEUNHi(BdL!d)b&hT zmaQOq!kT53a$luD!@!&h2Yjfckae~TQ3LfX`x`{|V7{3oaE9(%pX@M}4K!lBK4rL# zZwn+|t)pe+lT5~Fh`fC+(Stco;7+DedGYD$!b9v&l=wW1^8T^+tuggYX**#h;e@h9 z0#|PP)_}}5=U11B!$hY;Hr@^5)LiVl4ORo@p_wogDEDtR#^}Bq@IA?8bMkK?KX=FF z+1D@Fre<5Z?5=z>coUsXW$e2ir=X_hWb}ee3~SzOCVjgEygR(*&x*7B{Eh>gBqKk% zsaXzmhJQY|t&>t^vH3P1D{YiE#k;fkA_5bQ&dVEIUfGu&UO^J8nb8hZOxFq*UKhPS znjA(vsME~bn|TZfu0MY)v~I5oyBt_{QM6Tyr}Xvqui6#EE2&8v&*04CW%0+XICAH$ zw4)Z#u`!8djst_B{DQF1EN|tIKtEeC)+=G~KyG*Z9vfqZizqq|*KWa%ZJ{i|Y_K=>v-5pSJdMnr4q(yiMmWx!h}8i6J?()~ni5w4XCUs+DlAb-z4^Y{a-Ta{Nd zHwQ)@s1105u^ublEAE}c?>?F+dYEvvNILqrj+>5%38yn23(aIO%ius6G$sXZ zCy88&tc?&Ne8omLEO(n`Gyi^X>+guPS|e zSF>Rz);hu*R$;YqoN_g9Fd3bWC!k7m1d@dK^3IazqW&y3pk^P=z>n9rSZh$RVV@MJ z8`)m?&nm>^TdL_Jakwi~&9$_iVB=L8Xu3zS^#6vWISyK+jt3bGpBAw?X1#W{)$!PH zY__!>=99Wj*PFGl8{K7_xlP+8cw6OumcstFg%<{ahO&?m7-98xwqB8kUS>u_dpco$arH@$FzYBbt@6naKOmG|%jFe~x~)J0#`<5-?L_J=;V zF+Z)eiX@koHp)nyEU8GHS!pmFL@KMn=yURjltVg&ZjI`%*BPD%th8IN)f+;8+~)rp zuqb!&K`k6#pn+hJU7KE6upm-MT0mj*uN!v0;B>K|589H`>}N3&fY^B!_B96~g72zI zQzqpGMX)12qFZ!rVvC|;VnzGI$2eAU>$>L@dxDs@tjo27HWpZWaZCIeusV#nSl4ad|B7gR9O@`9Xf$Czn6R`tTPLmcp)s_wkz zu>AU2Uqp2a39qpvFWgG?R><#|_0VBn@(^*VhG&%fL1m=u8oG)H9c-rLg(>ZYIb$YX zi==EOY5(~PR!G`#|DDAyW!w#8p*J?8uv3#^J4R*rp!7Qbh8-r6~?=+K|2Dz%DS z#^QV2LWw6?wONbKP>kpit=XZeLfLb-9!wBIc;TAoAK})B>srb`Thc+`5t(4)Oc>8p z_$*3uak?MmCsjG6_)VrG>gw}@%Y;}C^~;=8XUFVG3?g0KbEu}5)FnRieJLrQLnxP4 zq8Ei?y(dHZytVnu3^OvbqA&XHhtVqz!e$&Ccr6^JM&+Pfu!nbdma+BX1RG7R94x2S zHmxyzuX@lGA&W4_#hmw*Ze-mn$$x}6!t6+?uz!?V-MP$!%M(_<4)N~y{tPXV?c^Df z<2oec|Wf2Gpfx{4it7Z#=5&!`<0we>3AOs)gWm<4VJA>Dj>R}|%v^X%;Z>jZ8L5o>6_z(iQ6gO-{pvn%GIF~MyVm-5nF#i889 zmYnORhre^*pEDlQHfM35W8oTrX+U{^OE~bNKx){wHB7pUep|;Kf^=GOmc3yq zcR~+niw}?ro46V4Lr&neP-i(gcnlc;j#!J+OfGC-U=?W@K1@0)MWy47ML;0{|5k4fSaW$_4<6I%TRN zX4Z7pARmEzz@{W^|INM3`JTAD_^;xk&TzydWHE!%IsUdBD5fu&;%g`g;=S8806^~? zK7P410RX%?^Zq?AeW&i-r$NBZWNIv^s02sQ!8r-F*-XHKc{D)Q8fRk_w~&H3Mc`=C}q;HRyCN2zKJ02;Jo@rI0`Caogvg{?-7$un+(eXwDY=b5KWD>tit z!<^=rSL@n0Zvl6mMSTF(A!Tb(kq)5*9C){MU9M27)ZlvY38#?shsnMs0MalAAi{() zE~L`NIv--nWtx|D+mG|QAEplKcN?3U_qMcpeQoU>on74kJ*+>#@xk7{Q2)RnC}U<* zQ#1AcTB3ouW*xk10lKEGNbgK? zh{%rL7Bx19D6xCH`9@1H(;-Uj6G>PkoqVQ9f)0_g$9t4aOQyiTmpm8Trd0Us72Qbh zZO1RG^2urJ*d@wsAs*vqOH+6f^LS(^K4RQBp_cY=%m*>g5?h2NDc5pjPb9{QvUbwx zM*3E|h!^sxHLoXSW~SoTW8QAQJk^3l@XIAKqf8{&EdeLtqZZ72d#XneQIL;`R8ET} z+RUphF-sWiA^KMBCT0QAx!TkdT`HX$n`JDzWuF$gtg9&O{i703#OtJUmJ_ZyCZOSn5(Wph^&nYC?iQxp6*VF0dQkPwpo6M{rW2s$Lx zXXM^IeC*Tb)K&v}oupE<9I=@z3Q?2R^yp5>Lt?z3eVX!U1@#O~S*-@PUFM#etv4f$F9b6+KThTBm zNMk}QD%OMOFxzw;=K=xFuMlt^>mS{Kst(cMNUY+t8gXh6!)yvu{x};A0tt?8>=T@% zLh#yh+Q>S!<@U)GH0tn0C+K*tsk>pU&PW@f7Xk;DC@kA4mv&G)$oc!(fS|J>aU#Lb z#`gw-+Y$o9!P z!O0B})|`@TgKGi_VqyD8;c8n^G?Yb8st~wO#r@hAX585)k}dWh{S}RMXLzcMqwMtuB@;#5EBKQRbn-VI%lM6G3+O5ppYev zMmR*YO6lu;KF|6?8f{*}nWHTFCulK12ZRP>8hez$D1T-yp)$S1&DgE89H;#=vBbJ0 z22VC!1rOGH%gv3eABa9lWuZ26F9w5jv+RK0@96^J;5X}jeo z!f=*bR1H`gN<{XFfnbs3jSV&d&_W_ai-Ny&hiVD3)|TyE5k;dL4M6)beLU~fpZnao zPJz@DhA{PnkgBvLq*bfrx5aN{3DX0VtaS?9r*V4m$xjf5klzE}_vGYsH$A+%s2wJR zFlQ9`C?P+P9-sT68!E$Tn`>3wT9y2e<4LB)C49qhA0bx=WUFumdB*Xg&+Z|lnttZW zQwxjVizq)-<;e0liga0jWm!nvdvyV78N05s@n$pM$*RIv@MJkG8y`BdV8(6x(^K@P ziCNE|$~|!xc4y)8bc~W`mx4(V7w>e}K0_G7EL%A8dI7@_rht%)Lsn@$>|FE~5Q^6k zLJHxpDq#9B1jxp|{8H#xJLV%9Out*SOQfQ%dKYEn3FA)95qGEjwy;c5uQNM!@S1U# zyY(hBe;RvQkM(%7dA#cgA>=x=mw7iuldHO9;S@q*x>aLWU%fTSZtcb6Vgra}y=Od7 zyHyzt9eZqO94`wPn|B6@FcR7{Q=y~OPlo@ro09;ry!XJvb=A5~j>^hCVH zn44r?ypO76O^I+MA>XFXG2fD_mL}gJMd)GPLE>-bYbkMRtU}zM5d2bg=EWfGRLe*9gAyh8}lnP=<1+WP9h&yTx{kUz-3??ZtU5DR5W?@TJ1AP2Wm zr`IFxxIoD89@~uhHn%Rb8Q*ShMxL`?bzW=yc*_yy$uGZ`8q6< z_juEdy0ZpBK;UJl14Qr5h~#?%ErhtyCGxu6dZFFZD-JjJWh|fqCyUnS1w%%)>`yT7+S4JSIPh{?U{3xsG(mIsL=Qv{ zoEVl(dUPSwzj;(Bl&vX4<1}9lJvk`!Y*e2X3jO`pSe6Vsqlj}|TgYN8Yb;JX7t``f zlpD`{y@0s_OPm?QldSGLa$H@m+AO-pqB`#i@=h(B|6w)j0?X=p_pa#?;fnl2ewt@J z)TiG>NG(Ik3};LrLJC4U^+)dRLI*qc;&tJ9tTkS56hH5jA97h;wj0-L(3fe9IN5B} z@bMTV2K%2k^CmO@=S{r1rjsFt>pFv`sQ#Z@TMwq~8#LRRDeB&qntk4meb30Xw`q!v zC0;3fZAgoYCV^$9x;q88Kz3dlF&ZwP&a1G*Kef5#tM3$IR<`OReJa%-omOg0Td}gN z_2#C2{RhC&uHw+)>i>o&pv|$ zy&jJ~YRIpt)$7?OZ+!4E$<8noqttD~oZGg|hlIWgJzde8V=2aw0IM3ng zGe zu_Wo=rlhA4Qhz$2&ktVOzqvy2-`jS;RlN|566*6)!hh1>%zUDp!qFavrO? zT1Rp>Y#;NFV!E^G-rq_2d&~Ll4dP@)sMYCn}V~)b)(Zhd`K>!v4CQazTza0qiL&so7L7Sj~J{0{39Ha?)Iv z?zgr0J)vw|B|OIb)b8J}^?@Pa4KyS34q!+6_yG>&HTLBwzfo|K($V{AP#dL<^=W>w zeHk-5r=BH~JoI5>zLM8vqSy7~bpP=5+<`Ckm(yX(ORbG699xjnSM$+(*oOUk)tA@z z57gcpa$F~=tTLdRLfp(zZK5PSnDI{r+hhu<#}bW0@Tg38{bNYH-+2^z{+{nw)3W{Y zop`@GUZO*I+fwtwdjI9O&C;)aUU3tT2k{`Ww`8JKAicVsCHu+<*@8`X(%>YedZi31 z$zI(@gO9XB_n3XI$?w*}3@#GtM)(p>%y= zQOU7N-F9`|yxd;x9W{FhQ{T|k94F&@(i>|({_XB|txjYsU^`B$)e$9~o>kziYHROi zSIt_Yv(Nj<$h0|1%PK1Ows?IVU3-_&>u#5*vAw*tbFH3^>rhl;G$T1Uj#e^s(Ef&B@2YW)a&yarZ zDI!(ySzo6xuc!YVoL1gWi40F99^0n7VU+eBQy*P(U{(>!QiB9csZqytYv9`D@^G{S z4N5^a&)N`y4^1fwX&4+{v!5V>Qcl`a*n1e_q}v4Zx>vtTs54^ZpWyrJJ>FzhX!cxy-F!OXn$K9U=IC*^H&RG8f~8(!N^#bLV%sa>{gQ<1Ubrb;v8@gkVo8w zDv~+gn&_=4vW#|Ylcq@($?0OpsK7~KKP**L#xhZXR(yCRf*C{-Zm1!hRwU3TPy|Eb zHs#g&2G>J2^>!?Xs%UKai@E}H6G5#J=^|B##J|(9DnLJg2xgVhChCjrLxKM6)-(r# zz1$tEm`A<%KC=e65s7?8v6Zl>Z2-Jv^n$V*CP~-nqHk)KJk?)%AiCS%6h)rSyl)p9 zB>kB!(Ov?}ID3vpOP+Sfcr|I5t#|v-35T+A~1p?7!=ooO!^>^bvkzLjGY*snSNjj8YOVr;avS37ie-0 zgsu#cKxiLVk{RQC3cDZ! zuxk-B#DS97Xuw}4f(#J#+uZ?xOo!Gjv7x=5LY<(Z$$&C8SkSl;Yp?*@rAiFYl^SvDWO4k!=#h7Qi?1l@gZw_Gqc$eZf4522RBJhxuhWE!wsr|JkCx3$ zm+i6iZ5W^cT;OyWwjnrlOhuc#CQ;t~+jJ8l1C_2VldLK{aet6E9tJ#x7A)?25PuKP z{cn_3yIoEFv&9-#tAj->X*NJxGgPa_Ng7QjB67i=?F+oB!@Dbomb`)|4gm0QjUnd; zysUgH?pfS57hA{+4jG$Eb7gdiAIz%VMesGt@chT;-Pilar>@@Qn#x$0bnUmT*L*ip z4LocM+wDp_R3EM9SL_f<+Pbbk+>f8)%k!`aF^N88k^PQrrdVjD^~WZD1*`0h+dYUtM$&+NAj& zENyU;7@hp3sB8Z0GBIBKWz%6DyZk*kV#O~?c=D9o-j)+&6Bg??`~!jp7q)rv1We;r z(1BD^`x<}(Lx4k@=kLVd1wi)$ zu)b)!qp9uGZ2v}=tF0HyCG`hBx%RM4hxOQGHZ)FReMAV-Bj z73YdBf&y%6(v=&XccgF#3Lc4l;}w8;nk;`aC@{rQ$;v$ao$>dQ|HO(nXzXsyJ`AX` z!+APDkm%X11S+|5qucIEUXOvfF4-%fw=uq|!+Sb9B7pgo20g!#ueSqPh_`h+ejWf+ zLQd$FpkF}8t5pD@J209tCB96ZLcCp=lu*7)O0`y#6z|U@?GQ-H#CVemCM!uLm-CYv zP2Nf-iYxJtrX??SkfK%P4K@xWnbbAOt?IHOteu(!Ke#(79b-vqMFx|JCPyco*Ibj# zrR3)ijN(fg^oUp$@zt|1_+w^<{xbN@Lg&wk`T*UG$6qlmY|;5kpx-(`AtyICk18U0 z(3I~Yg==oNr;6I?aiT?%1p-!eV#$O#q$GoA+RiQq_C-t^J<%p?3hiS>DB{e)j~6j% z@IR}--!8%bMdBf{h>M(Vrw0^LtMc{gwJ2=VvI&?^*bwkuhewwuurE9W&|%h!MOeLv z85oPv4m!?P7IA%K2D3~%z_?}0=2dcWS^IZKrzv7FJ-DXRusJi}H)-%ED8y7!nQ4W; z>~MXH3AtU8@L%J96GpxJ;{hs2iYP<_|4Ekr2Wbh*@lu(bmX4l*k%?K7WGRq0TAjfN zfDnwJ7*3EB&9EFVh?1&Bg6&f)H~83j@z6;dM&ibL@z0VN_W(jh%E zAS3KJijE3*@B @@ -11,26 +19,31 @@ import "../styles/binary.css"; - +
- +
Denary
0
Binary
-
0000 0000
+
0
+
+
@@ -40,13 +53,12 @@ import "../styles/binary.css";
-
- +
+
Tools
-
-
@@ -115,7 +126,7 @@ import "../styles/binary.css";
- - + + diff --git a/src/scripts/binary.js b/src/scripts/binary.js index d3b25bd..6fb1103 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,353 +1,508 @@ // src/scripts/binary.js +// Computing:Box — Binary page logic (Unsigned + Two's Complement) +// NOTE: This file is written to match the IDs/classes in your current binary.astro HTML. -document.addEventListener("DOMContentLoaded", () => { +(() => { + /* ----------------------------- + DOM + ----------------------------- */ const bitsGrid = document.getElementById("bitsGrid"); const denaryEl = document.getElementById("denaryNumber"); const binaryEl = document.getElementById("binaryNumber"); + const bitsInput = document.getElementById("bitsInput"); const modeToggle = document.getElementById("modeToggle"); const modeHint = document.getElementById("modeHint"); - - const bitsInput = document.getElementById("bitsInput"); - const btnBitsUp = document.getElementById("btnBitsUp"); - const btnBitsDown = document.getElementById("btnBitsDown"); + const lblUnsigned = document.getElementById("lblUnsigned"); + const lblTwos = document.getElementById("lblTwos"); const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); const btnShiftLeft = document.getElementById("btnShiftLeft"); const btnShiftRight = document.getElementById("btnShiftRight"); + const btnDec = document.getElementById("btnDec"); + const btnInc = document.getElementById("btnInc"); const btnClear = document.getElementById("btnClear"); const btnRandom = document.getElementById("btnRandom"); - const btnInc = document.getElementById("btnInc"); - const btnDec = document.getElementById("btnDec"); - let bitCount = clampInt(Number(bitsInput.value || 8), 1, 64); - let isTwos = false; + const btnBitsUp = document.getElementById("btnBitsUp"); + const btnBitsDown = document.getElementById("btnBitsDown"); - // Bits stored MSB -> LSB (index 0 is MSB) + /* ----------------------------- + STATE + ----------------------------- */ + let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); + + // bits[i] is bit value 2^i (LSB at i=0) let bits = new Array(bitCount).fill(false); - // Random timer + // Random run timer (brief) let randomTimer = null; + /* ----------------------------- + 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)); + return Math.max(min, Math.min(max, Math.trunc(n))); } - function pow2(exp) { - // exp can be up to 63; JS Number is fine for display and basic use here - return 2 ** exp; + function isTwosMode() { + return !!modeToggle?.checked; } - function buildBits(count) { - bitsGrid.innerHTML = ""; - bits = new Array(count).fill(false); - bitCount = count; + function pow2Big(n) { + return 1n << BigInt(n); + } - // Grid wrap at 8 bits per row; also center for small counts - if (count < 8) { - bitsGrid.classList.add("bitsFew"); - bitsGrid.style.setProperty("--cols", String(count)); + function unsignedMaxExclusive(nBits) { + return pow2Big(nBits); // 2^n + } + + function unsignedMaxValue(nBits) { + return pow2Big(nBits) - 1n; + } + + function twosMin(nBits) { + return -pow2Big(nBits - 1); + } + + function twosMax(nBits) { + return pow2Big(nBits - 1) - 1n; + } + + function bitsToUnsignedBigInt() { + let v = 0n; + for (let i = 0; i < bitCount; i++) { + if (bits[i]) v += pow2Big(i); + } + return v; + } + + function unsignedBigIntToBits(vUnsigned) { + const v = ((vUnsigned % unsignedMaxExclusive(bitCount)) + unsignedMaxExclusive(bitCount)) % unsignedMaxExclusive(bitCount); + for (let i = 0; i < bitCount; i++) { + bits[i] = ((v >> BigInt(i)) & 1n) === 1n; + } + } + + function bitsToSignedBigIntTwos() { + const u = bitsToUnsignedBigInt(); + const signBit = bits[bitCount - 1] === true; + if (!signBit) return u; + + // negative: u - 2^n + return u - pow2Big(bitCount); + } + + function signedBigIntToBitsTwos(vSigned) { + // wrap into range [-2^(n-1), 2^(n-1)-1] + const min = twosMin(bitCount); + const max = twosMax(bitCount); + const span = pow2Big(bitCount); // 2^n + + let v = vSigned; + + // wrap using modular arithmetic on signed domain + // Convert to unsigned representative: v mod 2^n + v = ((v % span) + span) % span; + + unsignedBigIntToBits(v); + // labels/denary will show signed later + // (No further action needed here) + } + + function formatBinaryGrouped() { + // MSB..LSB with a space every 4 bits (matches your screenshot 0000 0000) + let s = ""; + for (let i = bitCount - 1; i >= 0; i--) { + s += bits[i] ? "1" : "0"; + const posFromRight = (bitCount - i); + if (i !== 0 && posFromRight % 4 === 0) s += " "; + } + return s; + } + + function updateModeHint() { + if (!modeHint) return; + if (isTwosMode()) { + modeHint.textContent = "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; + } else { + modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; + } + } + + /* ----------------------------- + BUILD UI (BITS) + ----------------------------- */ + function buildBits(count) { + bitCount = clampInt(count, 1, 64); + if (bitsInput) bitsInput.value = String(bitCount); + + // reset bits array size, preserve existing LSBs where possible + const oldBits = bits.slice(); + bits = new Array(bitCount).fill(false); + for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; + + bitsGrid.innerHTML = ""; + + // If less than 8 bits, centre nicely using your CSS helper + bitsGrid.classList.toggle("bitsFew", bitCount < 8); + if (bitCount < 8) { + bitsGrid.style.setProperty("--cols", String(bitCount)); } else { - bitsGrid.classList.remove("bitsFew"); bitsGrid.style.removeProperty("--cols"); } - for (let i = 0; i < count; i++) { - const isMSB = i === 0; - const valueUnsigned = pow2(count - 1 - i); // MSB is 2^(n-1) + // Render MSB..LSB left-to-right + for (let i = bitCount - 1; i >= 0; i--) { + const bitEl = document.createElement("div"); + bitEl.className = "bit"; - const bit = document.createElement("div"); - bit.className = "bit"; - - bit.innerHTML = ` - -
${valueUnsigned}
+ // IMPORTANT: We render the bulb as an emoji with NO circle/ring. + // We do not rely on the .bulb CSS ring/background at all. + bitEl.innerHTML = ` + +
`; - bitsGrid.appendChild(bit); + bitsGrid.appendChild(bitEl); } - hookSwitches(); - updateModeLabels(); - updateReadout(); - } - - function hookSwitches() { - bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { + // Hook switches + bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => { input.addEventListener("change", () => { const i = Number(input.dataset.index); bits[i] = input.checked; - updateReadout(); + updateUI(); }); }); - } - function updateModeLabels() { - isTwos = Boolean(modeToggle.checked); - - 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."; - - // Update the labels so the MSB shows negative weight in two's complement - for (let i = 0; i < bitCount; i++) { - const label = document.getElementById(`label-${i}`); - if (!label) continue; - - const unsignedWeight = pow2(bitCount - 1 - i); - - if (isTwos && i === 0) { - // MSB weight is negative - label.textContent = `-${unsignedWeight}`; - } else { - label.textContent = `${unsignedWeight}`; - } - } - } - - function formatBinaryString(raw) { - // group every 4 for readability (keeps your "0000 0000" look) - return raw.replace(/(.{4})/g, "$1 ").trim(); - } - - function computeUnsignedValue() { - let value = 0; - for (let i = 0; i < bitCount; i++) { - if (!bits[i]) continue; - value += pow2(bitCount - 1 - i); - } - return value; - } - - function computeTwosValue() { - // If MSB is 0 -> same as unsigned - const msb = bits[0] ? 1 : 0; - let value = computeUnsignedValue(); - if (msb === 1) { - // subtract 2^n to get signed negative value - value -= pow2(bitCount); - } - return value; - } - - function updateReadout() { - // Binary string (MSB->LSB) - const rawBinary = bits.map((b) => (b ? "1" : "0")).join(""); - binaryEl.textContent = formatBinaryString(rawBinary); - - // Denary value based on mode - const denary = isTwos ? computeTwosValue() : computeUnsignedValue(); - denaryEl.textContent = String(denary); - - // Bulbs MUST update in BOTH modes (this was your reported bug) + // Force the bulb to be "just the emoji" (removes the circle even if CSS adds it) for (let i = 0; i < bitCount; i++) { const bulb = document.getElementById(`bulb-${i}`); if (!bulb) continue; - bulb.classList.toggle("on", bits[i]); + + // Strip the ring/circle coming from CSS + bulb.style.width = "auto"; + bulb.style.height = "auto"; + bulb.style.border = "none"; + bulb.style.background = "transparent"; + bulb.style.borderRadius = "0"; + bulb.style.boxShadow = "none"; + bulb.style.opacity = "0.45"; + bulb.style.fontSize = "26px"; + bulb.style.lineHeight = "1"; + bulb.style.display = "flex"; + bulb.style.alignItems = "center"; + bulb.style.justifyContent = "center"; + bulb.style.filter = "grayscale(1)"; + bulb.textContent = "💡"; + } + + updateUI(); + } + + /* ----------------------------- + UI UPDATE (READOUT + LABELS + BULBS + SWITCHES) + ----------------------------- */ + function updateBitLabels() { + // Show weights under each bit. + // Unsigned: 2^i + // Two's: MSB is -2^(n-1), others are 2^i + for (let i = 0; i < bitCount; i++) { + const label = document.getElementById(`bitLabel-${i}`); + if (!label) continue; + + if (isTwosMode() && i === bitCount - 1) { + label.textContent = `-${pow2Big(bitCount - 1).toString()}`; + } else { + label.textContent = pow2Big(i).toString(); + } } } - function syncInputs() { - bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => { + function syncSwitchesToBits() { + bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => { const i = Number(input.dataset.index); - input.checked = Boolean(bits[i]); + input.checked = !!bits[i]; }); + } + + function updateBulbs() { + // Bulbs should ALWAYS reflect bits, regardless of mode. + for (let i = 0; i < bitCount; i++) { + const bulb = document.getElementById(`bulb-${i}`); + if (!bulb) continue; + + const on = bits[i] === true; + + // Make it look "lit" when on (no circle, just glow) + if (on) { + bulb.style.opacity = "1"; + bulb.style.filter = "grayscale(0)"; + bulb.style.textShadow = "0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45)"; + } else { + bulb.style.opacity = "0.45"; + bulb.style.filter = "grayscale(1)"; + bulb.style.textShadow = "none"; + } + } + } + + function updateReadout() { + if (!denaryEl || !binaryEl) return; + + if (isTwosMode()) { + const signed = bitsToSignedBigIntTwos(); + denaryEl.textContent = signed.toString(); + } else { + const unsigned = bitsToUnsignedBigInt(); + denaryEl.textContent = unsigned.toString(); + } + + binaryEl.textContent = formatBinaryGrouped(); + } + + function updateUI() { + updateModeHint(); + updateBitLabels(); + syncSwitchesToBits(); + updateBulbs(); updateReadout(); } - function setAllBits(off = true) { - bits = bits.map(() => !off); - syncInputs(); + /* ----------------------------- + SET FROM BINARY STRING + ----------------------------- */ + function setFromBinaryString(binStr) { + const clean = String(binStr ?? "").replace(/\s+/g, ""); + if (!/^[01]+$/.test(clean)) return false; + + // Use rightmost bitCount bits; left pad with 0 + const padded = clean.slice(-bitCount).padStart(bitCount, "0"); + + for (let i = 0; i < bitCount; i++) { + // padded is MSB..LSB, bits[] is LSB..MSB + const charFromRight = padded[padded.length - 1 - i]; + bits[i] = charFromRight === "1"; + } + + updateUI(); + return true; } + /* ----------------------------- + SET FROM DENARY INPUT + ----------------------------- */ + function setFromDenaryInput(vStr) { + const raw = String(vStr ?? "").trim(); + if (!raw) return false; + + // BigInt parse (supports negatives) + let v; + try { + // Allow normal integers only + if (!/^-?\d+$/.test(raw)) return false; + v = BigInt(raw); + } catch { + return false; + } + + if (isTwosMode()) { + // Clamp to representable range + const min = twosMin(bitCount); + const max = twosMax(bitCount); + if (v < min || v > max) return false; + + signedBigIntToBitsTwos(v); + } else { + // Unsigned only + if (v < 0n) return false; + if (v > unsignedMaxValue(bitCount)) return false; + + unsignedBigIntToBits(v); + } + + updateUI(); + return true; + } + + /* ----------------------------- + SHIFTS + ----------------------------- */ function shiftLeft() { - // left shift: drop MSB, append 0 at LSB - bits.shift(); - bits.push(false); - syncInputs(); + // logical left shift: bits move to higher index; LSB becomes 0 + for (let i = bitCount - 1; i >= 1; i--) { + bits[i] = bits[i - 1]; + } + bits[0] = false; + updateUI(); } function shiftRight() { - // right shift: drop LSB, prepend 0 at MSB - bits.pop(); - bits.unshift(false); - syncInputs(); - } - - function setFromBinary(input) { - const clean = String(input).replace(/\s+/g, ""); - if (!/^[01]+$/.test(clean)) return false; - - const padded = clean.slice(-bitCount).padStart(bitCount, "0"); - bits = [...padded].map((ch) => ch === "1"); - syncInputs(); - return true; - } - - function setFromDenary(input) { - let n = Number(input); - if (!Number.isInteger(n)) return false; - - // For unsigned mode: allow 0..(2^n - 1) - // For two's mode: allow -(2^(n-1))..(2^(n-1)-1) - const maxUnsigned = pow2(bitCount) - 1; - const minTwos = -pow2(bitCount - 1); - const maxTwos = pow2(bitCount - 1) - 1; - - if (!isTwos) { - if (n < 0 || n > maxUnsigned) return false; - // build bits from unsigned n - bits = new Array(bitCount).fill(false); - for (let i = 0; i < bitCount; i++) { - const weight = pow2(bitCount - 1 - i); - if (n >= weight) { - bits[i] = true; - n -= weight; - } - } - syncInputs(); - return true; + // logical right shift: bits move to lower index; MSB becomes 0 + for (let i = 0; i < bitCount - 1; i++) { + bits[i] = bits[i + 1]; } + bits[bitCount - 1] = false; + updateUI(); + } - // Two's complement: convert signed integer to n-bit representation - if (n < minTwos || n > maxTwos) return false; - - let u = n; - if (u < 0) u = pow2(bitCount) + u; // wrap into unsigned range - const bin = u.toString(2).padStart(bitCount, "0"); - bits = [...bin].map((ch) => ch === "1"); - syncInputs(); - return true; + /* ----------------------------- + CLEAR / INC / DEC + ----------------------------- */ + function clearAll() { + bits.fill(false); + updateUI(); } function increment() { - // increment the underlying value in current mode, wrap appropriately - if (!isTwos) { - const max = pow2(bitCount) - 1; - let v = computeUnsignedValue(); - v = (v + 1) % (max + 1); - setFromDenary(v); - return; + if (isTwosMode()) { + const min = twosMin(bitCount); + const max = twosMax(bitCount); + let v = bitsToSignedBigIntTwos() + 1n; + if (v > max) v = min; // wrap + signedBigIntToBitsTwos(v); + } else { + const span = unsignedMaxExclusive(bitCount); + const v = (bitsToUnsignedBigInt() + 1n) % span; + unsignedBigIntToBits(v); } - - const min = -pow2(bitCount - 1); - const max = pow2(bitCount - 1) - 1; - let v = computeTwosValue(); - v = v + 1; - if (v > max) v = min; // wrap - setFromDenary(v); + updateUI(); } function decrement() { - if (!isTwos) { - const max = pow2(bitCount) - 1; - let v = computeUnsignedValue(); - v = v - 1; - if (v < 0) v = max; - setFromDenary(v); - return; + if (isTwosMode()) { + const min = twosMin(bitCount); + const max = twosMax(bitCount); + let v = bitsToSignedBigIntTwos() - 1n; + if (v < min) v = max; // wrap + signedBigIntToBitsTwos(v); + } else { + const span = unsignedMaxExclusive(bitCount); + const v = (bitsToUnsignedBigInt() - 1n + span) % span; + unsignedBigIntToBits(v); } - - const min = -pow2(bitCount - 1); - const max = pow2(bitCount - 1) - 1; - let v = computeTwosValue(); - v = v - 1; - if (v < min) v = max; - setFromDenary(v); + updateUI(); } - function startAutoRandom() { - stopAutoRandom(); + /* ----------------------------- + RANDOM (FIXED: NO BigInt->Number Math.min) + ----------------------------- */ + function cryptoRandomBigInt(maxExclusive) { + // returns 0 <= x < maxExclusive + if (maxExclusive <= 0n) return 0n; - const durationMs = 1200; // runs briefly then stops - const tickMs = 90; + const bitLen = maxExclusive.toString(2).length; + const byteLen = Math.ceil(bitLen / 8); + + // Rejection sampling + while (true) { + const bytes = new Uint8Array(byteLen); + crypto.getRandomValues(bytes); + + let x = 0n; + for (const b of bytes) { + x = (x << 8n) | BigInt(b); + } + + // mask down to bitLen to reduce rejections slightly + const extraBits = BigInt(byteLen * 8 - bitLen); + if (extraBits > 0n) x = x >> extraBits; + + if (x < maxExclusive) return x; + } + } + + function setRandomOnce() { + if (isTwosMode()) { + const span = unsignedMaxExclusive(bitCount); // 2^n + const u = cryptoRandomBigInt(span); // 0..2^n-1 + unsignedBigIntToBits(u); + } else { + const span = unsignedMaxExclusive(bitCount); + const u = cryptoRandomBigInt(span); + unsignedBigIntToBits(u); + } + updateUI(); + } + + function runRandomBriefly() { + // stop any existing run + if (randomTimer) { + clearInterval(randomTimer); + randomTimer = null; + } const start = Date.now(); - randomTimer = window.setInterval(() => { - // pick a random representable number depending on mode - let target; - if (!isTwos) { - target = Math.floor(Math.random() * (pow2(bitCount))); - } else { - const min = -pow2(bitCount - 1); - const max = pow2(bitCount - 1) - 1; - target = min + Math.floor(Math.random() * (max - min + 1)); - } - setFromDenary(target); + const durationMs = 900; // brief run then stop + const tickMs = 80; - if (Date.now() - start >= durationMs) stopAutoRandom(); + randomTimer = setInterval(() => { + setRandomOnce(); + if (Date.now() - start >= durationMs) { + clearInterval(randomTimer); + randomTimer = null; + } }, tickMs); } - function stopAutoRandom() { - if (randomTimer !== null) { - window.clearInterval(randomTimer); - randomTimer = null; - } + /* ----------------------------- + BIT WIDTH CONTROLS + ----------------------------- */ + function setBitWidth(n) { + const v = clampInt(n, 1, 64); + buildBits(v); } - // MODE toggle - modeToggle.addEventListener("change", () => { - updateModeLabels(); - updateReadout(); + /* ----------------------------- + EVENTS + ----------------------------- */ + modeToggle?.addEventListener("change", () => { + updateUI(); }); - // Bit width - btnBitsUp.addEventListener("click", () => { - const next = clampInt(bitCount + 1, 1, 64); - bitsInput.value = String(next); - buildBits(next); + btnCustomBinary?.addEventListener("click", () => { + const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`); + if (v === null) return; + if (!setFromBinaryString(v)) alert("Invalid binary"); }); - btnBitsDown.addEventListener("click", () => { - const next = clampInt(bitCount - 1, 1, 64); - bitsInput.value = String(next); - buildBits(next); + btnCustomDenary?.addEventListener("click", () => { + const v = prompt( + isTwosMode() + ? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):` + : `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):` + ); + if (v === null) return; + if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width"); }); - bitsInput.addEventListener("change", () => { - const next = clampInt(bitsInput.value, 1, 64); - bitsInput.value = String(next); - buildBits(next); + btnShiftLeft?.addEventListener("click", shiftLeft); + btnShiftRight?.addEventListener("click", shiftRight); + + btnInc?.addEventListener("click", increment); + btnDec?.addEventListener("click", decrement); + + btnClear?.addEventListener("click", clearAll); + btnRandom?.addEventListener("click", runRandomBriefly); + + btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1)); + btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1)); + + bitsInput?.addEventListener("change", () => { + setBitWidth(Number(bitsInput.value)); }); - // Buttons - btnShiftLeft.addEventListener("click", shiftLeft); - btnShiftRight.addEventListener("click", shiftRight); - - btnCustomBinary.addEventListener("click", () => { - const val = prompt(`Enter a ${bitCount}-bit binary number:`); - if (val === null) return; - if (!setFromBinary(val)) alert("Invalid binary input (use only 0 and 1)."); - }); - - btnCustomDenary.addEventListener("click", () => { - const modeRange = isTwos - ? `(${ -pow2(bitCount - 1) } to ${ pow2(bitCount - 1) - 1 })` - : `(0 to ${ pow2(bitCount) - 1 })`; - - const val = prompt(`Enter a denary number ${modeRange}:`); - if (val === null) return; - if (!setFromDenary(val)) alert("Invalid denary input for the current mode/bit width."); - }); - - btnClear.addEventListener("click", () => setAllBits(true)); - btnRandom.addEventListener("click", startAutoRandom); - - btnInc.addEventListener("click", increment); - btnDec.addEventListener("click", decrement); - - // INIT - modeToggle.checked = false; - updateModeLabels(); + /* ----------------------------- + INIT + ----------------------------- */ + updateModeHint(); buildBits(bitCount); -}); +})(); diff --git a/src/styles/binary.css b/src/styles/binary.css index 5c88b94..a62409a 100644 --- a/src/styles/binary.css +++ b/src/styles/binary.css @@ -1,19 +1,34 @@ :root{ --bg: #1f2027; - --panel: #22242d; --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); + + --danger: #e24444; + --danger-dim: rgba(226,68,68,.22); + --success: #2fd66b; + --success-dim: rgba(47,214,107,.22); } +/* -------- Fonts -------- */ @font-face{ font-family: "DSEG7ClassicRegular"; src: - url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"), - url("/fonts/DSEG7Classic-Regular.woff") format("woff"); + url("/fonts/DSEG7Classic-Regular.woff") format("woff"), + url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face{ + font-family: "SevenSegment"; + src: + url("/fonts/Seven-Segment.woff2") format("woff2"), + url("/fonts/Seven-Segment.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; @@ -21,7 +36,7 @@ body{ margin:0; - font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + font-family: "SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--text); } @@ -40,7 +55,6 @@ body{ } .readout{ - background: transparent; text-align:center; padding: 10px 10px 0; } @@ -54,6 +68,7 @@ body{ margin-top: 10px; } +/* Anything that is a number uses DSEG7 */ .num{ font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: 400; @@ -62,17 +77,17 @@ body{ } .denaryValue{ - font-size: 70px; /* smaller than before */ + font-size: 70px; line-height: 1.0; margin: 6px 0 10px; } .binaryValue{ - font-size: 52px; /* smaller than before */ + font-size: 52px; letter-spacing: .12em; line-height: 1.0; margin: 6px 0 14px; - white-space: pre; /* keep spaces */ + white-space: pre; } .controlsStack{ @@ -99,6 +114,7 @@ body{ font-weight: 700; cursor: pointer; min-width: 170px; + font-family: "SevenSegment", system-ui, sans-serif; } .btn:active{ transform: translateY(1px); } @@ -152,9 +168,10 @@ body{ color: var(--text); font-weight: 700; font-size: 14px; + font-family: "SevenSegment", system-ui, sans-serif; } -/* Shared toggle switch (mode + bit switches) */ +/* Switch */ .switch{ position: relative; width: 56px; @@ -195,14 +212,13 @@ body{ background: var(--accent); } -/* Tools card layout */ +/* Tools layout */ .toolRow{ display:grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; } - .toolRow2{ display:grid; grid-template-columns: 1fr 1fr; @@ -217,10 +233,26 @@ body{ color: #fff; cursor: pointer; font-weight: 800; + font-family: "SevenSegment", system-ui, sans-serif; } +/* Narrower arrow buttons (only the arrow pair) */ .toolSpin{ - font-size: 22px; /* bigger spin feature */ + font-size: 22px; + height: 48px; + max-width: 120px; /* narrower */ + justify-self: start; + padding: 0; +} + +/* Down = red, Up = green */ +#btnDec{ + background: var(--danger-dim); + border-color: rgba(226,68,68,.45); +} +#btnInc{ + background: var(--success-dim); + border-color: rgba(47,214,107,.45); } /* Bit width control */ @@ -240,6 +272,7 @@ body{ cursor:pointer; font-weight:900; font-size:18px; + font-family: "SevenSegment", system-ui, sans-serif; } .bitInputWrap{ @@ -258,6 +291,7 @@ body{ font-weight:800; letter-spacing:.18em; text-transform:uppercase; + font-family: "SevenSegment", system-ui, sans-serif; } .bitInput{ width:86px; @@ -275,18 +309,20 @@ body{ margin:0; } -/* Bits area (wrap every 8 bits, centered) */ +/* Bits: wrap every 8, centred */ .bitsWrap{ margin-top: 22px; } - .bitsGrid{ display:grid; gap: 18px; justify-content:center; - grid-template-columns: repeat(8, minmax(90px, 1fr)); /* wraps at 8 */ + grid-template-columns: repeat(8, minmax(90px, 1fr)); padding-top: 18px; } +.bitsGrid.bitsFew{ + grid-template-columns: repeat(var(--cols, 4), minmax(90px, 1fr)); +} .bit{ display:flex; @@ -297,36 +333,38 @@ body{ text-align:center; } -/* “Bulb like 💡” but consistent + bigger */ +/* Bulb (emoji only — no circle, no ::before so it won't duplicate) */ .bulb{ - width: 34px; /* bigger */ - height: 34px; /* bigger */ - border-radius: 50%; - background: rgba(255,255,255,.08); - border: 1px solid rgba(255,255,255,.12); + width: auto; + height: auto; + border: none; + background: transparent; + border-radius: 0; box-shadow: none; + display:flex; align-items:center; justify-content:center; - font-size: 20px; + + font-size: 26px; line-height: 1; - opacity: .55; -} -.bulb::before{ - content: "💡"; + opacity: .45; filter: grayscale(1); -} -.bulb.on{ - opacity: 1; - background: rgba(255,216,107,.18); - border-color: rgba(255,216,107,.55); - box-shadow: 0 0 18px rgba(255,216,107,.45); -} -.bulb.on::before{ - filter: grayscale(0); + text-shadow: none; } -/* Bit value (MSB becomes negative in two’s mode via JS label text) */ +/* IMPORTANT: remove the pseudo-element that was causing the 2nd bulb */ +.bulb::before{ + content: none; +} + +.bulb.on{ + opacity: 1; + filter: grayscale(0); + text-shadow: 0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45); +} + +/* Bit value numbers use DSEG7 */ .bitVal{ font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size: 28px; @@ -336,11 +374,6 @@ body{ min-height: 32px; } -/* Make sure small bit counts still look centered/nice */ -.bitsGrid.bitsFew{ - grid-template-columns: repeat(var(--cols, 4), minmax(90px, 1fr)); -} - @media (max-width: 980px){ .topGrid{ grid-template-columns: 1fr; } .denaryValue{ font-size: 62px; } From 91a07e49ae92b954488b0d9aecd1216a9e9c4eb7 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 15:13:18 +0000 Subject: [PATCH 07/23] feat(v2-alpha): add collapsible toolbox and refine binary simulator layout - Introduce collapsible right-hand toolbox for the binary simulator - Pin toolbox to the right edge with responsive fallback on smaller screens - Rework tools and bit-width controls into a shared side layout - Adjust control sizing, spacing, and visual hierarchy for clarity Notes: - Toolbox collapse is functional but requires refinement - Tools and Bit Width should be merged into a single module/card - Collapsed state does not yet fully hide the panel or re-centre content Signed-off-by: Alexander Lyall --- src/pages/binary.astro | 62 +++++++---------- src/scripts/binary.js | 12 ++++ src/styles/binary.css | 155 +++++++++++++++++++++++++---------------- 3 files changed, 132 insertions(+), 97 deletions(-) diff --git a/src/pages/binary.astro b/src/pages/binary.astro index d4e91a5..8524e1e 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -59,38 +59,35 @@ import binaryScriptUrl from "../scripts/binary.js?url"; - + diff --git a/src/scripts/binary.js b/src/scripts/binary.js index 6fb1103..5d64896 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -464,6 +464,17 @@ /* ----------------------------- EVENTS ----------------------------- */ + + // Collapsible right panel + const sidePanel = document.getElementById("sidePanel"); + const btnPanelToggle = document.getElementById("btnPanelToggle"); + + btnPanelToggle?.addEventListener("click", () => { + sidePanel?.classList.toggle("isCollapsed"); + // flip the chevron + btnPanelToggle.textContent = sidePanel?.classList.contains("isCollapsed") ? "❮" : "❯"; + }); + modeToggle?.addEventListener("change", () => { updateUI(); }); @@ -506,3 +517,4 @@ updateModeHint(); buildBits(bitCount); })(); + diff --git a/src/styles/binary.css b/src/styles/binary.css index a62409a..288f2ef 100644 --- a/src/styles/binary.css +++ b/src/styles/binary.css @@ -7,13 +7,14 @@ --accent-dim: rgba(51,255,122,.15); --line: rgba(255,255,255,.12); - --danger: #e24444; - --danger-dim: rgba(226,68,68,.22); - --success: #2fd66b; - --success-dim: rgba(47,214,107,.22); + /* right column sizing */ + --sideW: 720px; + --sidePad: 24px; + + /* header offset (tweak if your header is taller/shorter) */ + --sideTop: 92px; } -/* -------- Fonts -------- */ @font-face{ font-family: "DSEG7ClassicRegular"; src: @@ -41,15 +42,17 @@ body{ color: var(--text); } +/* Leave enough room for the fixed right panel */ .wrap{ max-width: 1200px; margin: 0 auto; padding: 32px 20px 60px; + padding-right: calc(var(--sideW) + (var(--sidePad) * 2)); } .topGrid{ display:grid; - grid-template-columns: 1fr 340px; + grid-template-columns: 1fr; gap: 28px; align-items:start; } @@ -68,7 +71,6 @@ body{ margin-top: 10px; } -/* Anything that is a number uses DSEG7 */ .num{ font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: 400; @@ -76,18 +78,20 @@ body{ text-shadow: 0 0 18px var(--accent-dim); } +/* 25% smaller */ .denaryValue{ - font-size: 70px; + font-size: 53px; line-height: 1.0; margin: 6px 0 10px; } +/* 25% smaller + slightly wider spacing */ .binaryValue{ - font-size: 52px; - letter-spacing: .12em; + font-size: 39px; + letter-spacing: calc(.12em + 2px); line-height: 1.0; margin: 6px 0 14px; - white-space: pre; + white-space: pre; /* preserve spaces */ } .controlsStack{ @@ -128,12 +132,45 @@ body{ border-top: 1px solid var(--line); } +/* ------------------------- + RIGHT COLUMN: pinned + collapsible +------------------------- */ .panelCol{ + position: fixed; + right: var(--sidePad); + top: var(--sideTop); + width: var(--sideW); display:flex; flex-direction:column; gap: 14px; + z-index: 50; + transition: transform .2s ease; } +/* collapse leaves a small handle visible */ +.panelCol.isCollapsed{ + transform: translateX(calc(100% - 52px)); +} + +.panelToggle{ + position: absolute; + left: -52px; + top: 0; + width: 44px; + height: 44px; + border-radius: 12px; + border: 1px solid rgba(255,255,255,.14); + background: rgba(255,255,255,.06); + color: #fff; + cursor: pointer; + font-weight: 900; + display:flex; + align-items:center; + justify-content:center; + font-family: "SevenSegment", system-ui, sans-serif; +} + +/* cards */ .card{ background: var(--panel2); border: 1px solid rgba(255,255,255,.10); @@ -168,10 +205,9 @@ body{ color: var(--text); font-weight: 700; font-size: 14px; - font-family: "SevenSegment", system-ui, sans-serif; } -/* Switch */ +/* Shared toggle switch */ .switch{ position: relative; width: 56px; @@ -212,19 +248,23 @@ body{ background: var(--accent); } -/* Tools layout */ -.toolRow{ - display:grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-bottom: 10px; +/* Tools + Bit Width side-by-side */ +.sideRow{ + display:flex; + gap: 14px; } -.toolRow2{ - display:grid; - grid-template-columns: 1fr 1fr; - gap: 10px; +.sideRow .card{ + flex: 1 1 0; } +/* Tools layout: arrows row + reset/random row */ +.toolRow, .toolRow2{ + display:flex; + gap: 10px; + align-items:center; +} + +/* Reset/Random = 50% current width (explicit) */ .toolBtn{ height: 48px; border-radius: 12px; @@ -236,23 +276,26 @@ body{ font-family: "SevenSegment", system-ui, sans-serif; } -/* Narrower arrow buttons (only the arrow pair) */ -.toolSpin{ +/* Reset/Random smaller */ +.toolBtn.toolHalf{ + width: 120px; +} + +/* Arrows half of that */ +.toolBtn.toolQuarter{ + width: 60px; font-size: 22px; - height: 48px; - max-width: 120px; /* narrower */ - justify-self: start; padding: 0; } -/* Down = red, Up = green */ -#btnDec{ - background: var(--danger-dim); - border-color: rgba(226,68,68,.45); +/* colour arrows */ +.toolBtn.toolUp{ + background: rgba(51,255,122,.18); + border-color: rgba(51,255,122,.45); } -#btnInc{ - background: var(--success-dim); - border-color: rgba(47,214,107,.45); +.toolBtn.toolDown{ + background: rgba(255,80,80,.18); + border-color: rgba(255,80,80,.45); } /* Bit width control */ @@ -291,7 +334,6 @@ body{ font-weight:800; letter-spacing:.18em; text-transform:uppercase; - font-family: "SevenSegment", system-ui, sans-serif; } .bitInput{ width:86px; @@ -333,38 +375,22 @@ body{ text-align:center; } -/* Bulb (emoji only — no circle, no ::before so it won't duplicate) */ +/* Bulb: no circle here (JS controls the emoji), but keep sizing if used */ .bulb{ - width: auto; - height: auto; border: none; background: transparent; + width: auto; + height: auto; border-radius: 0; box-shadow: none; - display:flex; align-items:center; justify-content:center; - - font-size: 26px; line-height: 1; - opacity: .45; - filter: grayscale(1); - text-shadow: none; + opacity: .55; + font-size: 33px; /* 25% larger from 26px */ } -/* IMPORTANT: remove the pseudo-element that was causing the 2nd bulb */ -.bulb::before{ - content: none; -} - -.bulb.on{ - opacity: 1; - filter: grayscale(0); - text-shadow: 0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45); -} - -/* Bit value numbers use DSEG7 */ .bitVal{ font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size: 28px; @@ -374,13 +400,22 @@ body{ min-height: 32px; } +/* responsive: stop pinning on small screens */ @media (max-width: 980px){ - .topGrid{ grid-template-columns: 1fr; } - .denaryValue{ font-size: 62px; } - .binaryValue{ font-size: 46px; } + .panelCol{ + position: static; + width: auto; + transform: none !important; + } + .panelToggle{ display:none; } + .wrap{ + padding-right: 20px; + } } @media (max-width: 520px){ .btn{ min-width: 150px; } .bitsGrid{ grid-template-columns: repeat(4, minmax(90px, 1fr)); } + .toolBtn.toolHalf{ width: 100px; } + .toolBtn.toolQuarter{ width: 52px; } } From f68aea4e33db1589408a1848c7a469ad77d432d6 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 17:18:03 +0000 Subject: [PATCH 08/23] Binary Page Beta 1 Signed-off-by: Alexander Lyall --- src/pages/binary.astro | 153 +++++------ src/scripts/binary.js | 226 ++++++++-------- src/styles/binary.css | 581 ++++++++++++++++++++++------------------- 3 files changed, 493 insertions(+), 467 deletions(-) diff --git a/src/pages/binary.astro b/src/pages/binary.astro index 8524e1e..8e27469 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,31 +1,92 @@ --- import "../styles/binary.css"; - -// ✅ Vite-bundled JS URL (works in dev + build + preview) -import binaryScriptUrl from "../scripts/binary.js?url"; - -// If you already have a site-wide Layout that adds header/footer, -// wrap the page with it here. -// Example (uncomment and adjust if you have it): -// import Layout from "../layouts/Layout.astro"; --- - + Binary | Computing:Box - + + -
+ + + + +
@@ -34,16 +95,14 @@ import binaryScriptUrl from "../scripts/binary.js?url";
0
Binary
-
0
+
0000 0000
-
-
@@ -57,64 +116,10 @@ import binaryScriptUrl from "../scripts/binary.js?url";
- - - -
- - + + diff --git a/src/scripts/binary.js b/src/scripts/binary.js index 5d64896..73d63d2 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,6 +1,6 @@ // src/scripts/binary.js // Computing:Box — Binary page logic (Unsigned + Two's Complement) -// NOTE: This file is written to match the IDs/classes in your current binary.astro HTML. +// Matches IDs/classes in src/pages/binary.astro (() => { /* ----------------------------- @@ -13,8 +13,6 @@ const modeToggle = document.getElementById("modeToggle"); const modeHint = document.getElementById("modeHint"); - const lblUnsigned = document.getElementById("lblUnsigned"); - const lblTwos = document.getElementById("lblTwos"); const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); @@ -29,6 +27,9 @@ const btnBitsUp = document.getElementById("btnBitsUp"); const btnBitsDown = document.getElementById("btnBitsDown"); + const toolboxToggle = document.getElementById("toolboxToggle"); + const toolboxPanel = document.getElementById("toolboxPanel"); + /* ----------------------------- STATE ----------------------------- */ @@ -40,6 +41,10 @@ // Random run timer (brief) let randomTimer = null; + // Dynamic wrapping for the big binary display + // "nibbles per row" recalculated on resize + let nibblesPerRow = 2; // default for small widths + /* ----------------------------- HELPERS ----------------------------- */ @@ -81,7 +86,8 @@ } function unsignedBigIntToBits(vUnsigned) { - const v = ((vUnsigned % unsignedMaxExclusive(bitCount)) + unsignedMaxExclusive(bitCount)) % unsignedMaxExclusive(bitCount); + const span = unsignedMaxExclusive(bitCount); + const v = ((vUnsigned % span) + span) % span; for (let i = 0; i < bitCount; i++) { bits[i] = ((v >> BigInt(i)) & 1n) === 1n; } @@ -91,48 +97,77 @@ const u = bitsToUnsignedBigInt(); const signBit = bits[bitCount - 1] === true; if (!signBit) return u; - - // negative: u - 2^n return u - pow2Big(bitCount); } function signedBigIntToBitsTwos(vSigned) { - // wrap into range [-2^(n-1), 2^(n-1)-1] - const min = twosMin(bitCount); - const max = twosMax(bitCount); const span = pow2Big(bitCount); // 2^n - let v = vSigned; - - // wrap using modular arithmetic on signed domain - // Convert to unsigned representative: v mod 2^n v = ((v % span) + span) % span; - unsignedBigIntToBits(v); - // labels/denary will show signed later - // (No further action needed here) - } - - function formatBinaryGrouped() { - // MSB..LSB with a space every 4 bits (matches your screenshot 0000 0000) - let s = ""; - for (let i = bitCount - 1; i >= 0; i--) { - s += bits[i] ? "1" : "0"; - const posFromRight = (bitCount - i); - if (i !== 0 && posFromRight % 4 === 0) s += " "; - } - return s; } function updateModeHint() { if (!modeHint) return; if (isTwosMode()) { - modeHint.textContent = "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; + modeHint.textContent = + "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; } else { - modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; + modeHint.textContent = + "Tip: In unsigned binary, all bits represent positive values."; } } + /* ----------------------------- + BIG BINARY DISPLAY WRAP + - Determines how many nibbles (4 bits) fit per row + - Recalculates on window resize + ----------------------------- */ + function computeNibblesPerRow() { + if (!binaryEl) return; + + // available width for the binary number = element width + const w = binaryEl.getBoundingClientRect().width; + + // Approximate "nibble width" in pixels: + // 4 digits + a space; use font size and letter-spacing to estimate. + // This doesn't need to be perfect, just stable and responsive. + const style = window.getComputedStyle(binaryEl); + const fontSize = parseFloat(style.fontSize || "40"); // px + const letterSpacing = parseFloat(style.letterSpacing || "0"); + const digitW = fontSize * 0.62 + letterSpacing; // rough digit width + const nibbleW = digitW * 4 + digitW * 1.2; // include gap between nibbles + + // Always allow at least 2 nibbles per row + const fit = Math.max(2, Math.floor(w / nibbleW)); + nibblesPerRow = fit; + } + + function formatBinaryGroupedWrapped() { + // Build MSB..LSB and group into nibbles + const nibbles = []; + let current = ""; + + for (let i = bitCount - 1; i >= 0; i--) { + current += bits[i] ? "1" : "0"; + const posFromRight = (bitCount - i); + if (posFromRight % 4 === 0 || i === 0) { + // if last group is partial, left-pad inside the group + if (current.length < 4) current = current.padStart(4, "0"); + nibbles.push(current); + current = ""; + } + } + + // Now wrap nibbles into lines + const lines = []; + for (let i = 0; i < nibbles.length; i += nibblesPerRow) { + lines.push(nibbles.slice(i, i + nibblesPerRow).join(" ")); + } + + return lines.join("\n"); + } + /* ----------------------------- BUILD UI (BITS) ----------------------------- */ @@ -140,28 +175,18 @@ bitCount = clampInt(count, 1, 64); if (bitsInput) bitsInput.value = String(bitCount); - // reset bits array size, preserve existing LSBs where possible + // preserve existing LSBs where possible const oldBits = bits.slice(); bits = new Array(bitCount).fill(false); for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; bitsGrid.innerHTML = ""; - // If less than 8 bits, centre nicely using your CSS helper - bitsGrid.classList.toggle("bitsFew", bitCount < 8); - if (bitCount < 8) { - bitsGrid.style.setProperty("--cols", String(bitCount)); - } else { - bitsGrid.style.removeProperty("--cols"); - } - // Render MSB..LSB left-to-right for (let i = bitCount - 1; i >= 0; i--) { const bitEl = document.createElement("div"); bitEl.className = "bit"; - // IMPORTANT: We render the bulb as an emoji with NO circle/ring. - // We do not rely on the .bulb CSS ring/background at all. bitEl.innerHTML = `
@@ -183,36 +208,13 @@ }); }); - // Force the bulb to be "just the emoji" (removes the circle even if CSS adds it) - for (let i = 0; i < bitCount; i++) { - const bulb = document.getElementById(`bulb-${i}`); - if (!bulb) continue; - - // Strip the ring/circle coming from CSS - bulb.style.width = "auto"; - bulb.style.height = "auto"; - bulb.style.border = "none"; - bulb.style.background = "transparent"; - bulb.style.borderRadius = "0"; - bulb.style.boxShadow = "none"; - bulb.style.opacity = "0.45"; - bulb.style.fontSize = "26px"; - bulb.style.lineHeight = "1"; - bulb.style.display = "flex"; - bulb.style.alignItems = "center"; - bulb.style.justifyContent = "center"; - bulb.style.filter = "grayscale(1)"; - bulb.textContent = "💡"; - } - updateUI(); } /* ----------------------------- - UI UPDATE (READOUT + LABELS + BULBS + SWITCHES) + UI UPDATE ----------------------------- */ function updateBitLabels() { - // Show weights under each bit. // Unsigned: 2^i // Two's: MSB is -2^(n-1), others are 2^i for (let i = 0; i < bitCount; i++) { @@ -235,14 +237,11 @@ } function updateBulbs() { - // Bulbs should ALWAYS reflect bits, regardless of mode. for (let i = 0; i < bitCount; i++) { const bulb = document.getElementById(`bulb-${i}`); if (!bulb) continue; const on = bits[i] === true; - - // Make it look "lit" when on (no circle, just glow) if (on) { bulb.style.opacity = "1"; bulb.style.filter = "grayscale(0)"; @@ -266,7 +265,9 @@ denaryEl.textContent = unsigned.toString(); } - binaryEl.textContent = formatBinaryGrouped(); + // Ensure nibble wrapping is up-to-date + computeNibblesPerRow(); + binaryEl.textContent = formatBinaryGroupedWrapped(); } function updateUI() { @@ -284,11 +285,8 @@ const clean = String(binStr ?? "").replace(/\s+/g, ""); if (!/^[01]+$/.test(clean)) return false; - // Use rightmost bitCount bits; left pad with 0 const padded = clean.slice(-bitCount).padStart(bitCount, "0"); - for (let i = 0; i < bitCount; i++) { - // padded is MSB..LSB, bits[] is LSB..MSB const charFromRight = padded[padded.length - 1 - i]; bits[i] = charFromRight === "1"; } @@ -304,10 +302,8 @@ const raw = String(vStr ?? "").trim(); if (!raw) return false; - // BigInt parse (supports negatives) let v; try { - // Allow normal integers only if (!/^-?\d+$/.test(raw)) return false; v = BigInt(raw); } catch { @@ -315,17 +311,13 @@ } if (isTwosMode()) { - // Clamp to representable range const min = twosMin(bitCount); const max = twosMax(bitCount); if (v < min || v > max) return false; - signedBigIntToBitsTwos(v); } else { - // Unsigned only if (v < 0n) return false; if (v > unsignedMaxValue(bitCount)) return false; - unsignedBigIntToBits(v); } @@ -337,19 +329,13 @@ SHIFTS ----------------------------- */ function shiftLeft() { - // logical left shift: bits move to higher index; LSB becomes 0 - for (let i = bitCount - 1; i >= 1; i--) { - bits[i] = bits[i - 1]; - } + for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1]; bits[0] = false; updateUI(); } function shiftRight() { - // logical right shift: bits move to lower index; MSB becomes 0 - for (let i = 0; i < bitCount - 1; i++) { - bits[i] = bits[i + 1]; - } + for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1]; bits[bitCount - 1] = false; updateUI(); } @@ -367,7 +353,7 @@ const min = twosMin(bitCount); const max = twosMax(bitCount); let v = bitsToSignedBigIntTwos() + 1n; - if (v > max) v = min; // wrap + if (v > max) v = min; signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); @@ -382,7 +368,7 @@ const min = twosMin(bitCount); const max = twosMax(bitCount); let v = bitsToSignedBigIntTwos() - 1n; - if (v < min) v = max; // wrap + if (v < min) v = max; signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); @@ -393,26 +379,21 @@ } /* ----------------------------- - RANDOM (FIXED: NO BigInt->Number Math.min) + RANDOM (BigInt-safe) ----------------------------- */ function cryptoRandomBigInt(maxExclusive) { - // returns 0 <= x < maxExclusive if (maxExclusive <= 0n) return 0n; const bitLen = maxExclusive.toString(2).length; const byteLen = Math.ceil(bitLen / 8); - // Rejection sampling while (true) { const bytes = new Uint8Array(byteLen); crypto.getRandomValues(bytes); let x = 0n; - for (const b of bytes) { - x = (x << 8n) | BigInt(b); - } + for (const b of bytes) x = (x << 8n) | BigInt(b); - // mask down to bitLen to reduce rejections slightly const extraBits = BigInt(byteLen * 8 - bitLen); if (extraBits > 0n) x = x >> extraBits; @@ -421,27 +402,20 @@ } function setRandomOnce() { - if (isTwosMode()) { - const span = unsignedMaxExclusive(bitCount); // 2^n - const u = cryptoRandomBigInt(span); // 0..2^n-1 - unsignedBigIntToBits(u); - } else { - const span = unsignedMaxExclusive(bitCount); - const u = cryptoRandomBigInt(span); - unsignedBigIntToBits(u); - } + const span = unsignedMaxExclusive(bitCount); + const u = cryptoRandomBigInt(span); + unsignedBigIntToBits(u); updateUI(); } function runRandomBriefly() { - // stop any existing run if (randomTimer) { clearInterval(randomTimer); randomTimer = null; } const start = Date.now(); - const durationMs = 900; // brief run then stop + const durationMs = 900; const tickMs = 80; randomTimer = setInterval(() => { @@ -454,30 +428,25 @@ } /* ----------------------------- - BIT WIDTH CONTROLS + BIT WIDTH ----------------------------- */ function setBitWidth(n) { const v = clampInt(n, 1, 64); buildBits(v); } + /* ----------------------------- + TOOLBOX TOGGLE + ----------------------------- */ + function setToolboxCollapsed(collapsed) { + document.body.classList.toggle("toolbox-collapsed", collapsed); + toolboxToggle?.setAttribute("aria-expanded", String(!collapsed)); + } + /* ----------------------------- EVENTS ----------------------------- */ - - // Collapsible right panel - const sidePanel = document.getElementById("sidePanel"); - const btnPanelToggle = document.getElementById("btnPanelToggle"); - - btnPanelToggle?.addEventListener("click", () => { - sidePanel?.classList.toggle("isCollapsed"); - // flip the chevron - btnPanelToggle.textContent = sidePanel?.classList.contains("isCollapsed") ? "❮" : "❯"; - }); - - modeToggle?.addEventListener("change", () => { - updateUI(); - }); + modeToggle?.addEventListener("change", () => updateUI()); btnCustomBinary?.addEventListener("click", () => { const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`); @@ -507,14 +476,25 @@ btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1)); btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1)); - bitsInput?.addEventListener("change", () => { - setBitWidth(Number(bitsInput.value)); + bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value))); + + toolboxToggle?.addEventListener("click", () => { + const collapsed = document.body.classList.contains("toolbox-collapsed"); + setToolboxCollapsed(!collapsed); + // ensure binary re-wraps after layout change + requestAnimationFrame(() => updateReadout()); + }); + + // Recompute nibble wrapping on resize + window.addEventListener("resize", () => { + // throttled via rAF to avoid spam + requestAnimationFrame(() => updateReadout()); }); /* ----------------------------- INIT ----------------------------- */ updateModeHint(); + setToolboxCollapsed(false); buildBits(bitCount); })(); - diff --git a/src/styles/binary.css b/src/styles/binary.css index 288f2ef..030b0f3 100644 --- a/src/styles/binary.css +++ b/src/styles/binary.css @@ -1,219 +1,218 @@ :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); + --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); - /* right column sizing */ - --sideW: 720px; - --sidePad: 24px; - - /* header offset (tweak if your header is taller/shorter) */ - --sideTop: 92px; + /* Toolbox sizing */ + --toolboxW: 320px; /* default width */ + --toolboxGap: 20px; /* gap from right edge */ + --toolboxTop: 92px; /* below header area */ } +/* ---------- Fonts ---------- */ +/* Numbers */ @font-face{ - font-family: "DSEG7ClassicRegular"; + 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; + font-weight:400; + font-style:normal; + font-display:swap; } +/* Text */ @font-face{ - font-family: "SevenSegment"; + font-family:"SevenSegment"; src: url("/fonts/Seven-Segment.woff2") format("woff2"), url("/fonts/Seven-Segment.woff") format("woff"); - font-weight: 400; - font-style: normal; - font-display: swap; + font-weight:400; + font-style:normal; + font-display:swap; } +*{ box-sizing:border-box; } + body{ margin:0; - font-family: "SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - background: var(--bg); - color: var(--text); + background:var(--bg); + color:var(--text); + font-family:"SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; } -/* Leave enough room for the fixed right panel */ +/* When toolbox is collapsed, we remove the reserved padding */ +body.toolbox-collapsed{ + --toolboxW: 0px; +} + +/* ---------- Main layout ---------- */ .wrap{ max-width: 1200px; margin: 0 auto; padding: 32px 20px 60px; - padding-right: calc(var(--sideW) + (var(--sidePad) * 2)); + + /* reserve room for the fixed toolbox so it NEVER overlaps */ + padding-right: calc(20px + var(--toolboxW) + var(--toolboxGap)); + transition: padding-right .2s ease; +} + +body.toolbox-collapsed .wrap{ + padding-right: 20px; } .topGrid{ display:grid; grid-template-columns: 1fr; - gap: 28px; + gap:28px; align-items:start; } .readout{ text-align:center; - padding: 10px 10px 0; + padding:10px 10px 0; } .label{ - letter-spacing: .18em; - font-weight: 700; - color: var(--muted); - text-transform: uppercase; - font-size: 14px; - margin-top: 10px; + letter-spacing:.18em; + font-weight:700; + color:var(--muted); + text-transform:uppercase; + font-size:14px; + margin-top:10px; } +/* All numbers */ +.num, +.bitVal, +.bitInput{ + font-family:"DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-weight:400; +} + +/* Denary/Binary display (25% smaller than before) */ .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); + color:var(--accent); + text-shadow:0 0 18px var(--accent-dim); + letter-spacing: 0.03em; /* + a pixel-ish feel */ } -/* 25% smaller */ .denaryValue{ - font-size: 53px; - line-height: 1.0; - margin: 6px 0 10px; + font-size: 52px; /* was ~70 */ + line-height:1.0; + margin:6px 0 10px; } -/* 25% smaller + slightly wider spacing */ .binaryValue{ - font-size: 39px; - letter-spacing: calc(.12em + 2px); - line-height: 1.0; - margin: 6px 0 14px; - white-space: pre; /* preserve spaces */ + font-size: 39px; /* was ~52 */ + line-height:1.05; + margin:6px 0 14px; + white-space: pre-wrap; + word-break: normal; + overflow-wrap: normal; + letter-spacing: 0.06em; /* widen characters slightly */ } +/* Buttons */ .controlsStack{ - margin-top: 10px; + margin-top:10px; display:flex; flex-direction:column; - gap: 10px; + gap:10px; align-items:center; } .controlsRow{ display:flex; - gap: 12px; + 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: 700; - cursor: pointer; - min-width: 170px; - font-family: "SevenSegment", system-ui, sans-serif; + background:rgba(255,255,255,.06); + border:1px solid rgba(255,255,255,.14); + color:#fff; + padding:12px 14px; + border-radius:12px; + font-weight:700; + cursor:pointer; + min-width:170px; + font-family:"SevenSegment", system-ui, sans-serif; + letter-spacing: .06em; } .btn:active{ transform: translateY(1px); } .btnAccent{ - background: rgba(51,255,122,.18); - border-color: rgba(51,255,122,.45); + background:rgba(51,255,122,.18); + border-color:rgba(51,255,122,.45); } .divider{ - margin-top: 26px; - border-top: 1px solid var(--line); + margin-top:26px; + border-top:1px solid var(--line); } -/* ------------------------- - RIGHT COLUMN: pinned + collapsible -------------------------- */ -.panelCol{ - position: fixed; - right: var(--sidePad); - top: var(--sideTop); - width: var(--sideW); +/* ---------- Bits grid ---------- */ +.bitsWrap{ margin-top:22px; } + +/* auto-fit prevents overlap at large values without shrinking the digits */ +.bitsGrid{ + display:grid; + gap:18px; + justify-content:center; + padding-top:18px; + + /* each tile has a safe min width so large labels don't collide */ + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + max-width: 100%; +} + +.bit{ display:flex; flex-direction:column; - gap: 14px; - z-index: 50; - transition: transform .2s ease; -} - -/* collapse leaves a small handle visible */ -.panelCol.isCollapsed{ - transform: translateX(calc(100% - 52px)); -} - -.panelToggle{ - position: absolute; - left: -52px; - top: 0; - width: 44px; - height: 44px; - border-radius: 12px; - border: 1px solid rgba(255,255,255,.14); - background: rgba(255,255,255,.06); - color: #fff; - cursor: pointer; - font-weight: 900; - display:flex; align-items:center; - justify-content:center; - font-family: "SevenSegment", system-ui, sans-serif; + gap:10px; + padding:8px 4px; + text-align:center; + min-width:120px; } -/* cards */ -.card{ - background: var(--panel2); - border: 1px solid rgba(255,255,255,.10); - border-radius: 14px; - padding: 14px; +.bulb{ + /* NO circle/ring. Just the emoji. 25% bigger than prior ~26px -> ~33px */ + font-size: 33px; + line-height:1; + background:transparent; + border:none; + width:auto; + height:auto; + opacity:.45; + filter: grayscale(1); + text-shadow:none; + user-select:none; } -.cardTitle{ - letter-spacing: .18em; - font-weight: 800; - color: var(--muted); - text-transform: uppercase; - font-size: 12px; - margin: 0 0 10px; +.bitVal{ + font-size:28px; + color:var(--text); + opacity:.95; + line-height:1; + min-height:32px; + white-space: nowrap; /* stop wrapping into each other */ + letter-spacing: 0.02em; } -.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: 700; - font-size: 14px; -} - -/* Shared toggle switch */ +/* Shared toggle switch (bit switches + mode) */ .switch{ - position: relative; - width: 56px; - height: 34px; + position:relative; + width:56px; + height:34px; display:inline-block; - flex: 0 0 auto; + flex:0 0 auto; } .switch input{ opacity:0; @@ -223,88 +222,198 @@ body{ .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; + 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; + 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); + background:rgba(51,255,122,.20); + border-color:rgba(51,255,122,.55); } .switch input:checked + .slider::before{ transform: translateX(22px); - background: var(--accent); + background:var(--accent); } -/* Tools + Bit Width side-by-side */ -.sideRow{ - display:flex; - gap: 14px; -} -.sideRow .card{ - flex: 1 1 0; -} +/* ---------- Toolbox (fixed, non-overlapping) ---------- */ +.toolboxToggle{ + position: fixed; + top: 18px; + right: var(--toolboxGap); + z-index: 40; -/* Tools layout: arrows row + reset/random row */ -.toolRow, .toolRow2{ display:flex; - gap: 10px; align-items:center; -} + gap:10px; -/* Reset/Random = 50% current width (explicit) */ -.toolBtn{ - height: 48px; - border-radius: 12px; background: rgba(255,255,255,.06); - border: 1px solid rgba(255,255,255,.14); - color: #fff; - cursor: pointer; - font-weight: 800; - font-family: "SevenSegment", system-ui, sans-serif; + border:1px solid rgba(255,255,255,.14); + color:#fff; + border-radius: 12px; + padding: 10px 12px; + cursor:pointer; + + font-family:"SevenSegment", system-ui, sans-serif; + letter-spacing:.06em; } -/* Reset/Random smaller */ -.toolBtn.toolHalf{ - width: 120px; +.toolboxIcon{ font-size: 16px; line-height:1; } +.toolboxText{ font-weight:800; font-size: 14px; } + +.toolboxFixed{ + position: fixed; + top: var(--toolboxTop); + right: var(--toolboxGap); + width: var(--toolboxW); + z-index: 35; + + transition: transform .2s ease, opacity .2s ease; } -/* Arrows half of that */ -.toolBtn.toolQuarter{ - width: 60px; - font-size: 22px; - padding: 0; +body.toolbox-collapsed .toolboxFixed{ + transform: translateX(calc(100% + var(--toolboxGap))); + opacity: 0; + pointer-events: none; } -/* colour arrows */ -.toolBtn.toolUp{ - background: rgba(51,255,122,.18); - border-color: rgba(51,255,122,.45); -} -.toolBtn.toolDown{ - background: rgba(255,80,80,.18); - border-color: rgba(255,80,80,.45); +/* cards inside toolbox */ +.panelCol{ + display:flex; + flex-direction:column; + gap:14px; } -/* Bit width control */ -.bitWidthRow{ +.card{ + background: var(--panel2); + border:1px solid rgba(255,255,255,.10); + border-radius:14px; + padding:14px; + backdrop-filter: blur(6px); +} + +.cardTitle{ + letter-spacing:.18em; + font-weight:800; + 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:700; + font-size:14px; +} + +/* ---------- Tools merged layout ---------- */ +.toolsTopRow{ + display:grid; + grid-template-columns: 1fr 1fr; + gap:10px; + align-items:center; + margin-bottom: 10px; +} + +.spinGroup{ + display:grid; + grid-template-columns: 1fr 1fr; + gap:10px; +} + +.toolsBottomRow{ + display:grid; + grid-template-columns: 1.25fr 0.75fr; /* bit width gets more space than random */ + gap:10px; + align-items:end; +} + +.toolBtn{ + height:48px; + border-radius:12px; + background:rgba(255,255,255,.06); + border:1px solid rgba(255,255,255,.14); + color:#fff; + cursor:pointer; + font-weight:800; + font-family:"SevenSegment", system-ui, sans-serif; + letter-spacing:.06em; +} + +.toolSpin{ + font-size:22px; + width: 100%; /* kept narrow via grid allocation */ +} + +.toolDown{ + background: rgba(255, 70, 70, .20); + border-color: rgba(255, 70, 70, .45); +} + +.toolUp{ + background: rgba(51,255,122,.20); + border-color: rgba(51,255,122,.55); +} + +.toolReset{ + width: 100%; +} + +.toolRandom{ + width: 100%; + /* make random ~5% narrower by padding reduction (without breaking grid) */ + padding-left: 10px; + padding-right: 10px; +} + +/* bit width mini block */ +.bitWidthMini{ + display:flex; + flex-direction:column; + gap:6px; +} + +.bitWidthMiniLabel{ + color:var(--muted); + font-size:12px; + font-weight:800; + letter-spacing:.18em; + text-transform:uppercase; +} + +.bitWidthMiniRow{ display:grid; grid-template-columns: 44px 1fr 44px; - gap: 10px; + gap:10px; align-items:center; } + .miniBtn{ height:44px; width:44px; @@ -315,107 +424,39 @@ body{ cursor:pointer; font-weight:900; font-size:18px; - font-family: "SevenSegment", system-ui, sans-serif; + font-family:"SevenSegment", system-ui, sans-serif; } -.bitInputWrap{ +.bitInput{ + width: 100%; + min-width: 64px; /* MUST fit 2 digits comfortably */ + text-align:center; 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:800; - letter-spacing:.18em; - text-transform:uppercase; -} -.bitInput{ - width:86px; - text-align:right; - background:transparent; - border:none; + padding:10px 8px; 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: wrap every 8, centred */ -.bitsWrap{ - margin-top: 22px; -} -.bitsGrid{ - display:grid; - gap: 18px; - justify-content:center; - grid-template-columns: repeat(8, minmax(90px, 1fr)); - padding-top: 18px; -} -.bitsGrid.bitsFew{ - grid-template-columns: repeat(var(--cols, 4), minmax(90px, 1fr)); -} - -.bit{ - display:flex; - flex-direction:column; - align-items:center; - gap: 10px; - padding: 8px 4px; - text-align:center; -} - -/* Bulb: no circle here (JS controls the emoji), but keep sizing if used */ -.bulb{ - border: none; - background: transparent; - width: auto; - height: auto; - border-radius: 0; - box-shadow: none; - display:flex; - align-items:center; - justify-content:center; - line-height: 1; - opacity: .55; - font-size: 33px; /* 25% larger from 26px */ -} - -.bitVal{ - font-family:"DSEG7ClassicRegular", ui-monospace, monospace; - font-size: 28px; - color: var(--text); - opacity: .95; - line-height: 1; - min-height: 32px; -} - -/* responsive: stop pinning on small screens */ +/* ---------- Responsive ---------- */ @media (max-width: 980px){ - .panelCol{ - position: static; - width: auto; - transform: none !important; - } - .panelToggle{ display:none; } .wrap{ + /* On small screens, don’t reserve fixed sidebar space; toolbox floats */ padding-right: 20px; } + .toolboxFixed{ + width: min(320px, calc(100vw - 40px)); + } } @media (max-width: 520px){ - .btn{ min-width: 150px; } - .bitsGrid{ grid-template-columns: repeat(4, minmax(90px, 1fr)); } - .toolBtn.toolHalf{ width: 100px; } - .toolBtn.toolQuarter{ width: 52px; } + .btn{ min-width:150px; } } From 7fa90ab1659d9264563de50fabfa3255fcf81426 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 18:53:23 +0000 Subject: [PATCH 09/23] Signed-off-by: Alexander Lyall --- src/pages/binary.astro | 164 ++++++------- src/scripts/binary.js | 185 +++++++------- src/styles/binary.css | 529 ++++++++++++++++++++--------------------- 3 files changed, 424 insertions(+), 454 deletions(-) diff --git a/src/pages/binary.astro b/src/pages/binary.astro index 8e27469..e38c390 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -10,86 +10,11 @@ import "../styles/binary.css"; Binary | Computing:Box - - - - - - - - -
+ +
- -
+ +
Denary
0
@@ -98,12 +23,12 @@ import "../styles/binary.css";
0000 0000
-
+
-
+
@@ -112,14 +37,89 @@ import "../styles/binary.css";
+
+ + +
- diff --git a/src/scripts/binary.js b/src/scripts/binary.js index 73d63d2..877e274 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,6 +1,6 @@ // src/scripts/binary.js // Computing:Box — Binary page logic (Unsigned + Two's Complement) -// Matches IDs/classes in src/pages/binary.astro +// Matches IDs/classes in binary.astro (() => { /* ----------------------------- @@ -28,23 +28,14 @@ const btnBitsDown = document.getElementById("btnBitsDown"); const toolboxToggle = document.getElementById("toolboxToggle"); - const toolboxPanel = document.getElementById("toolboxPanel"); /* ----------------------------- STATE ----------------------------- */ let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); - - // bits[i] is bit value 2^i (LSB at i=0) let bits = new Array(bitCount).fill(false); - - // Random run timer (brief) let randomTimer = null; - // Dynamic wrapping for the big binary display - // "nibbles per row" recalculated on resize - let nibblesPerRow = 2; // default for small widths - /* ----------------------------- HELPERS ----------------------------- */ @@ -62,7 +53,7 @@ } function unsignedMaxExclusive(nBits) { - return pow2Big(nBits); // 2^n + return pow2Big(nBits); } function unsignedMaxValue(nBits) { @@ -79,9 +70,7 @@ function bitsToUnsignedBigInt() { let v = 0n; - for (let i = 0; i < bitCount; i++) { - if (bits[i]) v += pow2Big(i); - } + for (let i = 0; i < bitCount; i++) if (bits[i]) v += pow2Big(i); return v; } @@ -101,70 +90,79 @@ } function signedBigIntToBitsTwos(vSigned) { - const span = pow2Big(bitCount); // 2^n - let v = vSigned; - v = ((v % span) + span) % span; + const span = pow2Big(bitCount); + let v = ((vSigned % span) + span) % span; unsignedBigIntToBits(v); } function updateModeHint() { if (!modeHint) return; - if (isTwosMode()) { - modeHint.textContent = - "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; - } else { - modeHint.textContent = - "Tip: In unsigned binary, all bits represent positive values."; - } + modeHint.textContent = isTwosMode() + ? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value." + : "Tip: In unsigned binary, all bits represent positive values."; } /* ----------------------------- - BIG BINARY DISPLAY WRAP - - Determines how many nibbles (4 bits) fit per row - - Recalculates on window resize + BINARY WRAP (responsive nibbles per row) + - Calculates how many 4-bit groups fit in the visible area + - Re-runs on resize ----------------------------- */ - function computeNibblesPerRow() { - if (!binaryEl) return; - - // available width for the binary number = element width - const w = binaryEl.getBoundingClientRect().width; - - // Approximate "nibble width" in pixels: - // 4 digits + a space; use font size and letter-spacing to estimate. - // This doesn't need to be perfect, just stable and responsive. - const style = window.getComputedStyle(binaryEl); - const fontSize = parseFloat(style.fontSize || "40"); // px - const letterSpacing = parseFloat(style.letterSpacing || "0"); - const digitW = fontSize * 0.62 + letterSpacing; // rough digit width - const nibbleW = digitW * 4 + digitW * 1.2; // include gap between nibbles - - // Always allow at least 2 nibbles per row - const fit = Math.max(2, Math.floor(w / nibbleW)); - nibblesPerRow = fit; + function measureNibbleWidthPx() { + // Measure "0000 " at current binary font settings + const probe = document.createElement("span"); + probe.style.position = "absolute"; + probe.style.visibility = "hidden"; + probe.style.whiteSpace = "pre"; + probe.style.font = getComputedStyle(binaryEl).font; + probe.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing; + probe.textContent = "0000 "; + document.body.appendChild(probe); + const w = probe.getBoundingClientRect().width; + probe.remove(); + return Math.max(1, w); } - function formatBinaryGroupedWrapped() { - // Build MSB..LSB and group into nibbles - const nibbles = []; - let current = ""; + function nibblesPerRow() { + if (!binaryEl) return Math.max(1, Math.ceil(bitCount / 4)); - for (let i = bitCount - 1; i >= 0; i--) { - current += bits[i] ? "1" : "0"; - const posFromRight = (bitCount - i); - if (posFromRight % 4 === 0 || i === 0) { - // if last group is partial, left-pad inside the group - if (current.length < 4) current = current.padStart(4, "0"); - nibbles.push(current); - current = ""; + const nibbleW = measureNibbleWidthPx(); + + // available width is the left column width (the grid reserves toolbox space already) + const container = binaryEl.closest(".readout") || binaryEl.parentElement; + const avail = container?.getBoundingClientRect().width ?? window.innerWidth; + + // leave a little padding so we don't hit the edge + const usable = Math.max(200, avail - 40); + + const perRow = Math.floor(usable / nibbleW); + return Math.max(1, perRow); + } + + function formatBinaryWrapped() { + // Build MSB..LSB grouped by nibbles, then wrap by nibblesPerRow() + const nibCount = Math.ceil(bitCount / 4); + const perRow = nibblesPerRow(); + + let out = []; + for (let n = 0; n < nibCount; n++) { + // nibble index from MSB side + const msbBitIndex = bitCount - 1 - (n * 4); + let nib = ""; + for (let k = 0; k < 4; k++) { + const i = msbBitIndex - k; + if (i < 0) continue; + nib += bits[i] ? "1" : "0"; } + // pad nibble if top group smaller + nib = nib.padStart(4, "0"); + out.push(nib); } - // Now wrap nibbles into lines - const lines = []; - for (let i = 0; i < nibbles.length; i += nibblesPerRow) { - lines.push(nibbles.slice(i, i + nibblesPerRow).join(" ")); + // wrap into lines + let lines = []; + for (let i = 0; i < out.length; i += perRow) { + lines.push(out.slice(i, i + perRow).join(" ")); } - return lines.join("\n"); } @@ -175,7 +173,6 @@ bitCount = clampInt(count, 1, 64); if (bitsInput) bitsInput.value = String(bitCount); - // preserve existing LSBs where possible const oldBits = bits.slice(); bits = new Array(bitCount).fill(false); for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; @@ -215,8 +212,6 @@ UI UPDATE ----------------------------- */ function updateBitLabels() { - // Unsigned: 2^i - // Two's: MSB is -2^(n-1), others are 2^i for (let i = 0; i < bitCount; i++) { const label = document.getElementById(`bitLabel-${i}`); if (!label) continue; @@ -240,17 +235,8 @@ for (let i = 0; i < bitCount; i++) { const bulb = document.getElementById(`bulb-${i}`); if (!bulb) continue; - const on = bits[i] === true; - if (on) { - bulb.style.opacity = "1"; - bulb.style.filter = "grayscale(0)"; - bulb.style.textShadow = "0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45)"; - } else { - bulb.style.opacity = "0.45"; - bulb.style.filter = "grayscale(1)"; - bulb.style.textShadow = "none"; - } + bulb.classList.toggle("on", on); } } @@ -258,16 +244,13 @@ if (!denaryEl || !binaryEl) return; if (isTwosMode()) { - const signed = bitsToSignedBigIntTwos(); - denaryEl.textContent = signed.toString(); + denaryEl.textContent = bitsToSignedBigIntTwos().toString(); } else { - const unsigned = bitsToUnsignedBigInt(); - denaryEl.textContent = unsigned.toString(); + denaryEl.textContent = bitsToUnsignedBigInt().toString(); } - // Ensure nibble wrapping is up-to-date - computeNibblesPerRow(); - binaryEl.textContent = formatBinaryGroupedWrapped(); + // responsive wrap of binary digits (nibbles per row) + binaryEl.textContent = formatBinaryWrapped(); } function updateUI() { @@ -290,7 +273,6 @@ const charFromRight = padded[padded.length - 1 - i]; bits[i] = charFromRight === "1"; } - updateUI(); return true; } @@ -357,8 +339,7 @@ signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); - const v = (bitsToUnsignedBigInt() + 1n) % span; - unsignedBigIntToBits(v); + unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span); } updateUI(); } @@ -372,8 +353,7 @@ signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); - const v = (bitsToUnsignedBigInt() - 1n + span) % span; - unsignedBigIntToBits(v); + unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span); } updateUI(); } @@ -431,22 +411,24 @@ BIT WIDTH ----------------------------- */ function setBitWidth(n) { - const v = clampInt(n, 1, 64); - buildBits(v); + buildBits(clampInt(n, 1, 64)); } /* ----------------------------- TOOLBOX TOGGLE ----------------------------- */ - function setToolboxCollapsed(collapsed) { - document.body.classList.toggle("toolbox-collapsed", collapsed); - toolboxToggle?.setAttribute("aria-expanded", String(!collapsed)); + function setToolboxOpen(open) { + document.body.classList.toggle("toolbox-open", open); + document.body.classList.toggle("toolbox-closed", !open); + toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false"); + // reflow binary wrapping when toolbox changes + requestAnimationFrame(updateUI); } /* ----------------------------- EVENTS ----------------------------- */ - modeToggle?.addEventListener("change", () => updateUI()); + modeToggle?.addEventListener("change", updateUI); btnCustomBinary?.addEventListener("click", () => { const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`); @@ -457,8 +439,8 @@ btnCustomDenary?.addEventListener("click", () => { const v = prompt( isTwosMode() - ? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):` - : `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):` + ? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):` + : `Enter denary (0 to ${unsignedMaxValue(bitCount)}):` ); if (v === null) return; if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width"); @@ -479,22 +461,19 @@ bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value))); toolboxToggle?.addEventListener("click", () => { - const collapsed = document.body.classList.contains("toolbox-collapsed"); - setToolboxCollapsed(!collapsed); - // ensure binary re-wraps after layout change - requestAnimationFrame(() => updateReadout()); + const open = !document.body.classList.contains("toolbox-closed"); + setToolboxOpen(!open); }); - // Recompute nibble wrapping on resize window.addEventListener("resize", () => { - // throttled via rAF to avoid spam - requestAnimationFrame(() => updateReadout()); + // re-wrap the binary display live as the window changes + updateUI(); }); /* ----------------------------- INIT ----------------------------- */ updateModeHint(); - setToolboxCollapsed(false); + setToolboxOpen(true); buildBits(bitCount); })(); diff --git a/src/styles/binary.css b/src/styles/binary.css index 030b0f3..3d00f8d 100644 --- a/src/styles/binary.css +++ b/src/styles/binary.css @@ -1,218 +1,214 @@ :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); + --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); /* Toolbox sizing */ - --toolboxW: 320px; /* default width */ - --toolboxGap: 20px; /* gap from right edge */ - --toolboxTop: 92px; /* below header area */ + --toolbox-w: 320px; + --toolbox-gap: 26px; } -/* ---------- Fonts ---------- */ -/* Numbers */ +/* NUMBERS */ @font-face{ - font-family:"DSEG7ClassicRegular"; + 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; + font-weight: 400; + font-style: normal; + font-display: swap; } -/* Text */ +/* TEXT */ @font-face{ - font-family:"SevenSegment"; + font-family: "SevenSegment"; src: url("/fonts/Seven-Segment.woff2") format("woff2"), url("/fonts/Seven-Segment.woff") format("woff"); - font-weight:400; - font-style:normal; - font-display:swap; + font-weight: 400; + font-style: normal; + font-display: swap; } -*{ box-sizing:border-box; } +*{ box-sizing: border-box; } body{ margin:0; - background:var(--bg); - color:var(--text); - font-family:"SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + background: var(--bg); + color: var(--text); + font-family: "SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; } -/* When toolbox is collapsed, we remove the reserved padding */ -body.toolbox-collapsed{ - --toolboxW: 0px; -} - -/* ---------- Main layout ---------- */ .wrap{ - max-width: 1200px; + max-width: 1400px; margin: 0 auto; padding: 32px 20px 60px; - - /* reserve room for the fixed toolbox so it NEVER overlaps */ - padding-right: calc(20px + var(--toolboxW) + var(--toolboxGap)); - transition: padding-right .2s ease; -} - -body.toolbox-collapsed .wrap{ - padding-right: 20px; } +/* Main layout: + - reserve space for the toolbox when open so it NEVER overlaps + - when closed, the left content re-centres because column disappears +*/ .topGrid{ display:grid; - grid-template-columns: 1fr; - gap:28px; + grid-template-columns: 1fr var(--toolbox-w); + gap: var(--toolbox-gap); align-items:start; } +body.toolbox-closed .topGrid{ + grid-template-columns: 1fr; + gap: 0; +} + +/* LEFT */ .readout{ text-align:center; - padding:10px 10px 0; + padding: 10px 10px 0; } .label{ - letter-spacing:.18em; - font-weight:700; - color:var(--muted); - text-transform:uppercase; - font-size:14px; - margin-top:10px; + letter-spacing: .18em; + font-weight: 700; + color: var(--muted); + text-transform: uppercase; + font-size: 14px; + margin-top: 10px; } -/* All numbers */ -.num, -.bitVal, -.bitInput{ - font-family:"DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-weight:400; -} - -/* Denary/Binary display (25% smaller than before) */ +/* Numbers use DSEG */ .num{ - color:var(--accent); - text-shadow:0 0 18px var(--accent-dim); - letter-spacing: 0.03em; /* + a pixel-ish feel */ + 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 is smaller than it used to be */ .denaryValue{ - font-size: 52px; /* was ~70 */ - line-height:1.0; - margin:6px 0 10px; + font-size: 56px; + line-height: 1.0; + margin: 6px 0 10px; } +/* Binary wraps (JS inserts \n), keep spacing readable */ .binaryValue{ - font-size: 39px; /* was ~52 */ - line-height:1.05; - margin:6px 0 14px; - white-space: pre-wrap; - word-break: normal; - overflow-wrap: normal; - letter-spacing: 0.06em; /* widen characters slightly */ + font-size: 44px; + line-height: 1.05; + margin: 6px 0 14px; + white-space: pre-line; + letter-spacing: 0.02em; } /* Buttons */ .controlsStack{ - margin-top:10px; + margin-top: 10px; display:flex; flex-direction:column; - gap:10px; align-items:center; } +/* extra spacing between custom and shift rows */ .controlsRow{ display:flex; - gap:12px; + gap: 12px; justify-content:center; flex-wrap:wrap; } +.controlsRowCustom{ margin-bottom: 14px; } +.controlsRowShift{ margin-bottom: 0; } .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:700; - cursor:pointer; - min-width:170px; - font-family:"SevenSegment", system-ui, sans-serif; - letter-spacing: .06em; + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.14); + color: #fff; + padding: 12px 14px; + border-radius: 12px; + font-weight: 700; + cursor: pointer; + min-width: 170px; } .btn:active{ transform: translateY(1px); } .btnAccent{ - background:rgba(51,255,122,.18); - border-color:rgba(51,255,122,.45); + background: rgba(51,255,122,.18); + border-color: rgba(51,255,122,.45); } .divider{ - margin-top:26px; - border-top:1px solid var(--line); + margin-top: 26px; + border-top: 1px solid var(--line); } -/* ---------- Bits grid ---------- */ -.bitsWrap{ margin-top:22px; } +/* Bits area */ +.bitsWrap{ margin-top: 22px; } -/* auto-fit prevents overlap at large values without shrinking the digits */ +/* IMPORTANT: Use auto-fit so columns get wider when toolbox is open, + preventing long bit values from overlapping. +*/ .bitsGrid{ display:grid; - gap:18px; + gap: 18px; justify-content:center; - padding-top:18px; - - /* each tile has a safe min width so large labels don't collide */ + padding-top: 18px; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - max-width: 100%; + max-width: 1100px; + margin: 0 auto; } .bit{ display:flex; flex-direction:column; align-items:center; - gap:10px; - padding:8px 4px; + gap: 10px; + padding: 8px 4px; text-align:center; - min-width:120px; } +/* Bulb: emoji only (no circle) */ .bulb{ - /* NO circle/ring. Just the emoji. 25% bigger than prior ~26px -> ~33px */ - font-size: 33px; - line-height:1; - background:transparent; - border:none; - width:auto; - height:auto; - opacity:.45; + font-size: 33px; /* ~25% larger */ + line-height: 1; + opacity: .45; filter: grayscale(1); - text-shadow:none; - user-select:none; + text-shadow: none; + user-select: none; +} +.bulb.on{ + opacity: 1; + filter: grayscale(0); + text-shadow: 0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45); } +/* Bit value */ .bitVal{ - font-size:28px; - color:var(--text); - opacity:.95; - line-height:1; - min-height:32px; - white-space: nowrap; /* stop wrapping into each other */ - letter-spacing: 0.02em; + font-family:"DSEG7ClassicRegular", ui-monospace, monospace; + font-size: 28px; + color: var(--text); + opacity: .95; + line-height: 1.05; + min-height: 34px; + + /* prevent overlap */ + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -/* Shared toggle switch (bit switches + mode) */ +/* Switch styling (reused) */ .switch{ - position:relative; - width:56px; - height:34px; + position: relative; + width: 56px; + height: 34px; display:inline-block; - flex:0 0 auto; + flex: 0 0 auto; } .switch input{ opacity:0; @@ -222,195 +218,170 @@ body.toolbox-collapsed .wrap{ .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; + 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; + 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); + background: rgba(51,255,122,.20); + border-color: rgba(51,255,122,.55); } .switch input:checked + .slider::before{ transform: translateX(22px); - background:var(--accent); + background: var(--accent); } -/* ---------- Toolbox (fixed, non-overlapping) ---------- */ -.toolboxToggle{ - position: fixed; +/* RIGHT TOOLBOX (fixed to right, but never overlaps because layout reserves space) */ +.toolboxDock{ + position: sticky; top: 18px; - right: var(--toolboxGap); - z-index: 40; + justify-self: end; +} - display:flex; +body.toolbox-closed .toolboxDock{ + display: none; +} + +.toolboxToggle{ + display:inline-flex; align-items:center; - gap:10px; - + gap: 10px; background: rgba(255,255,255,.06); - border:1px solid rgba(255,255,255,.14); - color:#fff; + border: 1px solid rgba(255,255,255,.14); + color: #fff; border-radius: 12px; padding: 10px 12px; - cursor:pointer; - - font-family:"SevenSegment", system-ui, sans-serif; - letter-spacing:.06em; + cursor: pointer; + font-weight: 800; + margin-bottom: 12px; } -.toolboxIcon{ font-size: 16px; line-height:1; } -.toolboxText{ font-weight:800; font-size: 14px; } - -.toolboxFixed{ - position: fixed; - top: var(--toolboxTop); - right: var(--toolboxGap); - width: var(--toolboxW); - z-index: 35; - - transition: transform .2s ease, opacity .2s ease; +.toolboxToggleText{ + letter-spacing: .08em; + text-transform: uppercase; + font-size: 12px; } -body.toolbox-collapsed .toolboxFixed{ - transform: translateX(calc(100% + var(--toolboxGap))); - opacity: 0; - pointer-events: none; -} - -/* cards inside toolbox */ -.panelCol{ +.toolboxPanel{ display:flex; flex-direction:column; - gap:14px; + gap: 14px; } +/* Cards */ .card{ background: var(--panel2); - border:1px solid rgba(255,255,255,.10); - border-radius:14px; - padding:14px; - backdrop-filter: blur(6px); + border: 1px solid rgba(255,255,255,.10); + border-radius: 14px; + padding: 14px; } .cardTitle{ - letter-spacing:.18em; - font-weight:800; - color:var(--muted); - text-transform:uppercase; - font-size:12px; - margin:0 0 10px; + letter-spacing: .18em; + font-weight: 800; + 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; + 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; + gap: 10px; } .toggleLabel{ - color:var(--text); - font-weight:700; - font-size:14px; + color: var(--text); + font-weight: 700; + font-size: 14px; } -/* ---------- Tools merged layout ---------- */ -.toolsTopRow{ +/* Tools layout */ +.toolsRowTop{ display:grid; grid-template-columns: 1fr 1fr; - gap:10px; + gap: 10px; align-items:center; - margin-bottom: 10px; + margin-bottom: 12px; } -.spinGroup{ - display:grid; - grid-template-columns: 1fr 1fr; - gap:10px; -} - -.toolsBottomRow{ - display:grid; - grid-template-columns: 1.25fr 0.75fr; /* bit width gets more space than random */ - gap:10px; - align-items:end; -} - -.toolBtn{ - height:48px; - border-radius:12px; - background:rgba(255,255,255,.06); - border:1px solid rgba(255,255,255,.14); - color:#fff; - cursor:pointer; - font-weight:800; - font-family:"SevenSegment", system-ui, sans-serif; - letter-spacing:.06em; -} - -.toolSpin{ - font-size:22px; - width: 100%; /* kept narrow via grid allocation */ -} - -.toolDown{ - background: rgba(255, 70, 70, .20); - border-color: rgba(255, 70, 70, .45); -} - -.toolUp{ - background: rgba(51,255,122,.20); - border-color: rgba(51,255,122,.55); -} - -.toolReset{ - width: 100%; -} - -.toolRandom{ - width: 100%; - /* make random ~5% narrower by padding reduction (without breaking grid) */ - padding-left: 10px; - padding-right: 10px; -} - -/* bit width mini block */ -.bitWidthMini{ +.toolsArrowsCentered{ display:flex; - flex-direction:column; - gap:6px; + justify-content:center; + gap: 10px; } -.bitWidthMiniLabel{ - color:var(--muted); - font-size:12px; - font-weight:800; - letter-spacing:.18em; - text-transform:uppercase; +/* Buttons */ +.toolBtn{ + height: 48px; + border-radius: 12px; + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.14); + color: #fff; + cursor: pointer; + font-weight: 800; + font-size: 20px; + min-width: 44px; } -.bitWidthMiniRow{ +.toolBtnWide{ + width: 100%; + font-size: 16px; +} + +.toolBtnDec{ + background: rgba(255, 80, 80, .16); + border-color: rgba(255, 80, 80, .35); +} + +.toolBtnInc{ + background: rgba(51,255,122,.18); + border-color: rgba(51,255,122,.45); +} + +.bitWidthSubCard{ + width: 100%; + background: rgba(255,255,255,.03); + border: 1px solid rgba(255,255,255,.10); + border-radius: 12px; + padding: 12px; + margin-bottom: 12px; +} + +.bitWidthTitle{ + letter-spacing: .18em; + font-weight: 800; + color: var(--muted); + text-transform: uppercase; + font-size: 12px; + margin: 0 0 10px; +} + +.bitWidthRow{ display:grid; grid-template-columns: 44px 1fr 44px; - gap:10px; + gap: 10px; align-items:center; } @@ -424,19 +395,36 @@ body.toolbox-collapsed .toolboxFixed{ cursor:pointer; font-weight:900; font-size:18px; - font-family:"SevenSegment", system-ui, sans-serif; } -.bitInput{ - width: 100%; - min-width: 64px; /* MUST fit 2 digits comfortably */ - text-align:center; +.bitInputWrap{ background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.14); border-radius:12px; - padding:10px 8px; + padding:10px 12px; + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + min-width: 0; +} + +.bitInputLabel{ + color:var(--muted); + font-size:12px; + font-weight:800; + letter-spacing:.18em; + text-transform:uppercase; +} + +.bitInput{ + width: 72px; /* fits 2 digits comfortably */ + text-align:right; + background:transparent; + border:none; outline:none; color:var(--accent); + font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size:28px; } @@ -446,17 +434,20 @@ body.toolbox-collapsed .toolboxFixed{ margin:0; } -/* ---------- Responsive ---------- */ +.toolsRowBottom{ + display:grid; + grid-template-columns: 1fr; + margin-bottom: 2px; +} + +/* Responsive */ @media (max-width: 980px){ - .wrap{ - /* On small screens, don’t reserve fixed sidebar space; toolbox floats */ - padding-right: 20px; - } - .toolboxFixed{ - width: min(320px, calc(100vw - 40px)); - } + .topGrid{ grid-template-columns: 1fr; } + .toolboxDock{ position: static; justify-self: stretch; } + body.toolbox-closed .toolboxDock{ display:none; } + .bitsGrid{ max-width: 100%; } } @media (max-width: 520px){ - .btn{ min-width:150px; } + .btn{ min-width: 150px; } } From 8bf8b44938f095251fdce818404561f4407f82a6 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 19:17:06 +0000 Subject: [PATCH 10/23] - Make the bulbs 25% bigger - Make the increment/decrement buttons centred to the card - The toolbox button disappears when the toolbox is hidden - Move the custom buttons to their own card in the toolbox - Put the random button in the same card as the custom buttons - Make the random button glow/pulse green when it is running - Move the shift buttons to the "Tools" card - Make the reset button the full width of the card, it should be in the bottom row on its own. - Make the reset button glow/pulse red when hovered over - Rename Mode to "Settings" - Move bit width to the "Settings" card - Keep the "Tools" card named as "Tools" --- src/pages/binary.astro | 214 ++++++++-------- src/scripts/binary.js | 179 +++++++------- src/styles/binary.css | 542 +++++++++++++++++++++++------------------ 3 files changed, 498 insertions(+), 437 deletions(-) diff --git a/src/pages/binary.astro b/src/pages/binary.astro index e38c390..e9614aa 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,4 +1,9 @@ --- +/* + Binary | Computing:Box + - Toolbox is fixed to the right, and can be collapsed without disappearing. + - Main content reserves space when toolbox is open, and recentres when hidden. +*/ import "../styles/binary.css"; --- @@ -6,117 +11,118 @@ import "../styles/binary.css"; - + Binary | Computing:Box - + + + + + + + +
-
- -
-
-
Denary
-
0
+
+
Denary
+
0
-
Binary
-
0000 0000
+
Binary
+
0000 0000
-
-
- - -
+
+
-
- - -
-
-
- -
- - -
-
-
- - - - +
+
diff --git a/src/scripts/binary.js b/src/scripts/binary.js index 877e274..5d7af61 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,6 +1,6 @@ // src/scripts/binary.js // Computing:Box — Binary page logic (Unsigned + Two's Complement) -// Matches IDs/classes in binary.astro +// Matches IDs/classes in your current binary.astro HTML. (() => { /* ----------------------------- @@ -16,6 +16,7 @@ const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); + const btnShiftLeft = document.getElementById("btnShiftLeft"); const btnShiftRight = document.getElementById("btnShiftRight"); @@ -27,13 +28,18 @@ const btnBitsUp = document.getElementById("btnBitsUp"); const btnBitsDown = document.getElementById("btnBitsDown"); + // Toolbox toggle const toolboxToggle = document.getElementById("toolboxToggle"); /* ----------------------------- STATE ----------------------------- */ let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); + + // bits[i] is bit value 2^i (LSB at i=0) let bits = new Array(bitCount).fill(false); + + // Random run timer (brief) let randomTimer = null; /* ----------------------------- @@ -53,7 +59,7 @@ } function unsignedMaxExclusive(nBits) { - return pow2Big(nBits); + return pow2Big(nBits); // 2^n } function unsignedMaxValue(nBits) { @@ -70,7 +76,9 @@ function bitsToUnsignedBigInt() { let v = 0n; - for (let i = 0; i < bitCount; i++) if (bits[i]) v += pow2Big(i); + for (let i = 0; i < bitCount; i++) { + if (bits[i]) v += pow2Big(i); + } return v; } @@ -86,84 +94,40 @@ const u = bitsToUnsignedBigInt(); const signBit = bits[bitCount - 1] === true; if (!signBit) return u; + + // negative: u - 2^n return u - pow2Big(bitCount); } function signedBigIntToBitsTwos(vSigned) { - const span = pow2Big(bitCount); - let v = ((vSigned % span) + span) % span; + const span = pow2Big(bitCount); // 2^n + let v = vSigned; + + // Convert to unsigned representative: v mod 2^n + v = ((v % span) + span) % span; unsignedBigIntToBits(v); } + function formatBinaryGrouped() { + // MSB..LSB with a space every 4 bits + let s = ""; + for (let i = bitCount - 1; i >= 0; i--) { + s += bits[i] ? "1" : "0"; + const posFromRight = (bitCount - i); + if (i !== 0 && posFromRight % 4 === 0) s += " "; + } + return s; + } + function updateModeHint() { if (!modeHint) return; - modeHint.textContent = isTwosMode() - ? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value." - : "Tip: In unsigned binary, all bits represent positive values."; - } - - /* ----------------------------- - BINARY WRAP (responsive nibbles per row) - - Calculates how many 4-bit groups fit in the visible area - - Re-runs on resize - ----------------------------- */ - function measureNibbleWidthPx() { - // Measure "0000 " at current binary font settings - const probe = document.createElement("span"); - probe.style.position = "absolute"; - probe.style.visibility = "hidden"; - probe.style.whiteSpace = "pre"; - probe.style.font = getComputedStyle(binaryEl).font; - probe.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing; - probe.textContent = "0000 "; - document.body.appendChild(probe); - const w = probe.getBoundingClientRect().width; - probe.remove(); - return Math.max(1, w); - } - - function nibblesPerRow() { - if (!binaryEl) return Math.max(1, Math.ceil(bitCount / 4)); - - const nibbleW = measureNibbleWidthPx(); - - // available width is the left column width (the grid reserves toolbox space already) - const container = binaryEl.closest(".readout") || binaryEl.parentElement; - const avail = container?.getBoundingClientRect().width ?? window.innerWidth; - - // leave a little padding so we don't hit the edge - const usable = Math.max(200, avail - 40); - - const perRow = Math.floor(usable / nibbleW); - return Math.max(1, perRow); - } - - function formatBinaryWrapped() { - // Build MSB..LSB grouped by nibbles, then wrap by nibblesPerRow() - const nibCount = Math.ceil(bitCount / 4); - const perRow = nibblesPerRow(); - - let out = []; - for (let n = 0; n < nibCount; n++) { - // nibble index from MSB side - const msbBitIndex = bitCount - 1 - (n * 4); - let nib = ""; - for (let k = 0; k < 4; k++) { - const i = msbBitIndex - k; - if (i < 0) continue; - nib += bits[i] ? "1" : "0"; - } - // pad nibble if top group smaller - nib = nib.padStart(4, "0"); - out.push(nib); + if (isTwosMode()) { + modeHint.textContent = + "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; + } else { + modeHint.textContent = + "Tip: In unsigned binary, all bits represent positive values."; } - - // wrap into lines - let lines = []; - for (let i = 0; i < out.length; i += perRow) { - lines.push(out.slice(i, i + perRow).join(" ")); - } - return lines.join("\n"); } /* ----------------------------- @@ -173,6 +137,7 @@ bitCount = clampInt(count, 1, 64); if (bitsInput) bitsInput.value = String(bitCount); + // resize bits array, preserve existing LSBs where possible const oldBits = bits.slice(); bits = new Array(bitCount).fill(false); for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; @@ -235,8 +200,18 @@ for (let i = 0; i < bitCount; i++) { const bulb = document.getElementById(`bulb-${i}`); if (!bulb) continue; + const on = bits[i] === true; - bulb.classList.toggle("on", on); + if (on) { + bulb.style.opacity = "1"; + bulb.style.filter = "grayscale(0)"; + bulb.style.textShadow = + "0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45)"; + } else { + bulb.style.opacity = "0.45"; + bulb.style.filter = "grayscale(1)"; + bulb.style.textShadow = "none"; + } } } @@ -249,8 +224,7 @@ denaryEl.textContent = bitsToUnsignedBigInt().toString(); } - // responsive wrap of binary digits (nibbles per row) - binaryEl.textContent = formatBinaryWrapped(); + binaryEl.textContent = formatBinaryGrouped(); } function updateUI() { @@ -269,10 +243,12 @@ if (!/^[01]+$/.test(clean)) return false; const padded = clean.slice(-bitCount).padStart(bitCount, "0"); + for (let i = 0; i < bitCount; i++) { const charFromRight = padded[padded.length - 1 - i]; bits[i] = charFromRight === "1"; } + updateUI(); return true; } @@ -339,7 +315,8 @@ signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); - unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span); + const v = (bitsToUnsignedBigInt() + 1n) % span; + unsignedBigIntToBits(v); } updateUI(); } @@ -353,7 +330,8 @@ signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); - unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span); + const v = (bitsToUnsignedBigInt() - 1n + span) % span; + unsignedBigIntToBits(v); } updateUI(); } @@ -382,7 +360,7 @@ } function setRandomOnce() { - const span = unsignedMaxExclusive(bitCount); + const span = unsignedMaxExclusive(bitCount); // 2^n const u = cryptoRandomBigInt(span); unsignedBigIntToBits(u); updateUI(); @@ -394,6 +372,9 @@ randomTimer = null; } + // pulse while running + btnRandom?.classList.add("is-running"); + const start = Date.now(); const durationMs = 900; const tickMs = 80; @@ -403,32 +384,44 @@ if (Date.now() - start >= durationMs) { clearInterval(randomTimer); randomTimer = null; + btnRandom?.classList.remove("is-running"); } }, tickMs); } /* ----------------------------- - BIT WIDTH + BIT WIDTH CONTROLS ----------------------------- */ function setBitWidth(n) { buildBits(clampInt(n, 1, 64)); } /* ----------------------------- - TOOLBOX TOGGLE + TOOLBOX TOGGLE (never disappears) ----------------------------- */ - function setToolboxOpen(open) { - document.body.classList.toggle("toolbox-open", open); - document.body.classList.toggle("toolbox-closed", !open); - toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false"); - // reflow binary wrapping when toolbox changes - requestAnimationFrame(updateUI); + function setToolboxHidden(hidden) { + document.body.classList.toggle("toolbox-hidden", hidden); + if (toolboxToggle) toolboxToggle.setAttribute("aria-expanded", String(!hidden)); + try { localStorage.setItem("computingbox.toolboxHidden", hidden ? "1" : "0"); } catch {} + } + + function initToolboxState() { + let hidden = false; + try { hidden = localStorage.getItem("computingbox.toolboxHidden") === "1"; } catch {} + setToolboxHidden(hidden); } /* ----------------------------- EVENTS ----------------------------- */ - modeToggle?.addEventListener("change", updateUI); + toolboxToggle?.addEventListener("click", () => { + const hidden = document.body.classList.contains("toolbox-hidden"); + setToolboxHidden(!hidden); + }); + + modeToggle?.addEventListener("change", () => { + updateUI(); + }); btnCustomBinary?.addEventListener("click", () => { const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`); @@ -458,22 +451,14 @@ btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1)); btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1)); - bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value))); - - toolboxToggle?.addEventListener("click", () => { - const open = !document.body.classList.contains("toolbox-closed"); - setToolboxOpen(!open); - }); - - window.addEventListener("resize", () => { - // re-wrap the binary display live as the window changes - updateUI(); + bitsInput?.addEventListener("change", () => { + setBitWidth(Number(bitsInput.value)); }); /* ----------------------------- INIT ----------------------------- */ + initToolboxState(); updateModeHint(); - setToolboxOpen(true); buildBits(bitCount); })(); diff --git a/src/styles/binary.css b/src/styles/binary.css index 3d00f8d..c18fa48 100644 --- a/src/styles/binary.css +++ b/src/styles/binary.css @@ -3,18 +3,14 @@ --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); - /* Toolbox sizing */ - --toolbox-w: 320px; - --toolbox-gap: 26px; + --toolbox-width: 320px; /* adjust if needed */ + --toolbox-gap: 28px; } -/* NUMBERS */ @font-face{ font-family: "DSEG7ClassicRegular"; src: @@ -25,7 +21,6 @@ font-display: swap; } -/* TEXT */ @font-face{ font-family: "SevenSegment"; src: @@ -36,7 +31,9 @@ font-display: swap; } -*{ box-sizing: border-box; } +html, body{ + height: 100%; +} body{ margin:0; @@ -45,29 +42,87 @@ body{ font-family: "SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; } +/* --------------------------------- + Toolbox toggle (always visible) +---------------------------------- */ +.toolboxToggle{ + position: fixed; + top: 18px; + right: 18px; + z-index: 9999; + display:flex; + align-items:center; + gap: 10px; + + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.14); + color: #fff; + border-radius: 12px; + padding: 10px 12px; + cursor: pointer; + font-weight: 800; + letter-spacing: .12em; + text-transform: uppercase; +} + +.toolboxToggleIcon{ + display:inline-block; + transform: translateY(1px); +} + +.toolboxToggleText{ + font-size: 12px; +} + +/* --------------------------------- + Fixed toolbox panel +---------------------------------- */ +.toolbox{ + position: fixed; + top: 74px; /* leaves room for the toggle button */ + right: 18px; + width: var(--toolbox-width); + z-index: 9998; +} + +.toolboxInner{ + display:flex; + flex-direction:column; + gap: 14px; +} + +/* When hidden, toolbox slides out but toggle stays */ +body.toolbox-hidden .toolbox{ + transform: translateX(calc(var(--toolbox-width) + 40px)); + opacity: 0; + pointer-events: none; + transition: transform .22s ease, opacity .18s ease; +} + +.toolbox{ + transition: transform .22s ease, opacity .18s ease; +} + +/* --------------------------------- + Main wrap: reserve space for toolbox +---------------------------------- */ .wrap{ - max-width: 1400px; + max-width: 1200px; margin: 0 auto; padding: 32px 20px 60px; + + /* Reserve space so toolbox never overlaps bits */ + padding-right: calc(var(--toolbox-width) + var(--toolbox-gap) + 20px); + transition: padding-right .22s ease; } -/* Main layout: - - reserve space for the toolbox when open so it NEVER overlaps - - when closed, the left content re-centres because column disappears -*/ -.topGrid{ - display:grid; - grid-template-columns: 1fr var(--toolbox-w); - gap: var(--toolbox-gap); - align-items:start; +body.toolbox-hidden .wrap{ + padding-right: 20px; } -body.toolbox-closed .topGrid{ - grid-template-columns: 1fr; - gap: 0; -} - -/* LEFT */ +/* --------------------------------- + Readout +---------------------------------- */ .readout{ text-align:center; padding: 10px 10px 0; @@ -82,7 +137,6 @@ body.toolbox-closed .topGrid{ margin-top: 10px; } -/* Numbers use DSEG */ .num{ font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: 400; @@ -90,49 +144,72 @@ body.toolbox-closed .topGrid{ text-shadow: 0 0 18px var(--accent-dim); } -/* Denary is smaller than it used to be */ +/* (You previously wanted these 25% smaller than earlier large sizes) */ .denaryValue{ - font-size: 56px; + font-size: 52px; line-height: 1.0; margin: 6px 0 10px; } -/* Binary wraps (JS inserts \n), keep spacing readable */ .binaryValue{ - font-size: 44px; - line-height: 1.05; + font-size: 40px; + letter-spacing: .14em; /* +1-2px feel */ + line-height: 1.08; margin: 6px 0 14px; - white-space: pre-line; - letter-spacing: 0.02em; + + /* Allow wrapping at spaces between nibbles */ + white-space: pre-wrap; + word-break: normal; + overflow-wrap: anywhere; + + /* Prevent absurd-wide overflow; respects toolbox space */ + max-width: calc(100vw - var(--toolbox-width) - 120px); + display:inline-block; } -/* Buttons */ -.controlsStack{ +.divider{ + margin-top: 26px; + border-top: 1px solid var(--line); +} + +/* --------------------------------- + Cards & common controls +---------------------------------- */ +.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: 10px; - display:flex; - flex-direction:column; - align-items:center; + line-height: 1.35; } -/* extra spacing between custom and shift rows */ -.controlsRow{ - display:flex; - gap: 12px; - justify-content:center; - flex-wrap:wrap; -} -.controlsRowCustom{ margin-bottom: 14px; } -.controlsRowShift{ margin-bottom: 0; } - .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: 700; + font-weight: 800; cursor: pointer; - min-width: 170px; + min-width: 0; + font-family: "SevenSegment", system-ui, sans-serif; + letter-spacing: .12em; + text-transform: uppercase; } .btn:active{ transform: translateY(1px); } @@ -141,68 +218,27 @@ body.toolbox-closed .topGrid{ border-color: rgba(51,255,122,.45); } -.divider{ - margin-top: 26px; - border-top: 1px solid var(--line); +.btnSmall{ + padding: 10px 10px; + font-size: 12px; } -/* Bits area */ -.bitsWrap{ margin-top: 22px; } - -/* IMPORTANT: Use auto-fit so columns get wider when toolbox is open, - preventing long bit values from overlapping. -*/ -.bitsGrid{ - display:grid; - gap: 18px; - justify-content:center; - padding-top: 18px; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - max-width: 1100px; - margin: 0 auto; -} - -.bit{ +/* --------------------------------- + Settings toggle +---------------------------------- */ +.toggleRow{ display:flex; - flex-direction:column; align-items:center; + justify-content:space-between; gap: 10px; - padding: 8px 4px; - text-align:center; } -/* Bulb: emoji only (no circle) */ -.bulb{ - font-size: 33px; /* ~25% larger */ - line-height: 1; - opacity: .45; - filter: grayscale(1); - text-shadow: none; - user-select: none; -} -.bulb.on{ - opacity: 1; - filter: grayscale(0); - text-shadow: 0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45); -} - -/* Bit value */ -.bitVal{ - font-family:"DSEG7ClassicRegular", ui-monospace, monospace; - font-size: 28px; +.toggleLabel{ color: var(--text); - opacity: .95; - line-height: 1.05; - min-height: 34px; - - /* prevent overlap */ - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + font-weight: 800; + font-size: 14px; } -/* Switch styling (reused) */ .switch{ position: relative; width: 56px; @@ -243,138 +279,23 @@ body.toolbox-closed .topGrid{ background: var(--accent); } -/* RIGHT TOOLBOX (fixed to right, but never overlaps because layout reserves space) */ -.toolboxDock{ - position: sticky; - top: 18px; - justify-self: end; -} - -body.toolbox-closed .toolboxDock{ - display: none; -} - -.toolboxToggle{ - display:inline-flex; - align-items:center; - gap: 10px; - background: rgba(255,255,255,.06); - border: 1px solid rgba(255,255,255,.14); - color: #fff; - border-radius: 12px; - padding: 10px 12px; - cursor: pointer; - font-weight: 800; - margin-bottom: 12px; -} - -.toolboxToggleText{ - letter-spacing: .08em; - text-transform: uppercase; - font-size: 12px; -} - -.toolboxPanel{ - display:flex; - flex-direction:column; - gap: 14px; -} - -/* Cards */ -.card{ - background: var(--panel2); - border: 1px solid rgba(255,255,255,.10); - border-radius: 14px; - padding: 14px; -} - -.cardTitle{ - letter-spacing: .18em; - font-weight: 800; - 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: 700; - font-size: 14px; -} - -/* Tools layout */ -.toolsRowTop{ - display:grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - align-items:center; - margin-bottom: 12px; -} - -.toolsArrowsCentered{ - display:flex; - justify-content:center; - gap: 10px; -} - -/* Buttons */ -.toolBtn{ - height: 48px; - border-radius: 12px; - background: rgba(255,255,255,.06); - border: 1px solid rgba(255,255,255,.14); - color: #fff; - cursor: pointer; - font-weight: 800; - font-size: 20px; - min-width: 44px; -} - -.toolBtnWide{ - width: 100%; - font-size: 16px; -} - -.toolBtnDec{ - background: rgba(255, 80, 80, .16); - border-color: rgba(255, 80, 80, .35); -} - -.toolBtnInc{ - background: rgba(51,255,122,.18); - border-color: rgba(51,255,122,.45); -} - -.bitWidthSubCard{ - width: 100%; +/* --------------------------------- + Settings: Bit width subcard full width +---------------------------------- */ +.subCard{ + margin-top: 12px; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.10); border-radius: 12px; padding: 12px; - margin-bottom: 12px; } -.bitWidthTitle{ +.subCardTitle{ letter-spacing: .18em; - font-weight: 800; + font-weight: 900; color: var(--muted); text-transform: uppercase; - font-size: 12px; + font-size: 11px; margin: 0 0 10px; } @@ -395,6 +316,7 @@ body.toolbox-closed .toolboxDock{ cursor:pointer; font-weight:900; font-size:18px; + font-family: "SevenSegment", system-ui, sans-serif; } .bitInputWrap{ @@ -406,19 +328,18 @@ body.toolbox-closed .toolboxDock{ align-items:center; justify-content:space-between; gap:12px; - min-width: 0; } .bitInputLabel{ color:var(--muted); font-size:12px; - font-weight:800; + font-weight:900; letter-spacing:.18em; text-transform:uppercase; } .bitInput{ - width: 72px; /* fits 2 digits comfortably */ + width: 90px; /* fits 2 digits comfortably */ text-align:right; background:transparent; border:none; @@ -427,27 +348,176 @@ body.toolbox-closed .toolboxDock{ font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size:28px; } - .bitInput::-webkit-outer-spin-button, .bitInput::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; } -.toolsRowBottom{ +/* --------------------------------- + Custom card (includes Random) +---------------------------------- */ +.customGrid{ display:grid; - grid-template-columns: 1fr; - margin-bottom: 2px; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.customGrid .btnRandom{ + grid-column: 1 / -1; +} + +/* Random running pulse (green) */ +@keyframes pulseGreen{ + 0% { box-shadow: 0 0 0 rgba(51,255,122,0.0); } + 50% { box-shadow: 0 0 18px rgba(51,255,122,0.55), 0 0 34px rgba(51,255,122,0.25); } + 100% { box-shadow: 0 0 0 rgba(51,255,122,0.0); } +} +.btnRandom.is-running{ + animation: pulseGreen .8s ease-in-out infinite; +} + +/* --------------------------------- + Tools card +---------------------------------- */ +.toolsTopRow{ + display:flex; + justify-content:center; /* centred to the card */ + gap: 10px; + margin-bottom: 10px; +} + +.toolBtn{ + height: 48px; + 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-family: "SevenSegment", system-ui, sans-serif; + letter-spacing: .12em; + text-transform: uppercase; +} + +.toolSpin{ + width: 64px; /* narrower */ + font-size: 22px; +} + +.toolDec{ + background: rgba(255, 60, 60, .22); + border-color: rgba(255, 60, 60, .40); +} + +.toolInc{ + background: rgba(51,255,122,.18); + border-color: rgba(51,255,122,.45); +} + +.toolsShiftRow{ + display:grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 10px; +} + +.toolReset{ + width: 100%; + height: 54px; + font-size: 14px; +} + +/* Reset hover pulse (red) */ +@keyframes pulseRed{ + 0% { box-shadow: 0 0 0 rgba(255,60,60,0.0); } + 50% { box-shadow: 0 0 18px rgba(255,60,60,0.55), 0 0 34px rgba(255,60,60,0.25); } + 100% { box-shadow: 0 0 0 rgba(255,60,60,0.0); } +} +.toolReset:hover{ + animation: pulseRed .75s ease-in-out infinite; + border-color: rgba(255, 60, 60, .55); +} + +/* --------------------------------- + Bits grid +---------------------------------- */ +.bitsWrap{ + margin-top: 22px; +} + +.bitsGrid{ + display:grid; + gap: 18px; + justify-content:center; + grid-template-columns: repeat(8, minmax(90px, 1fr)); + padding-top: 18px; +} + +/* Each bit tile */ +.bit{ + display:flex; + flex-direction:column; + align-items:center; + gap: 10px; + padding: 8px 4px; + text-align:center; +} + +/* Bulb emoji only (no circle), +25% larger */ +.bulb{ + width:auto; + height:auto; + border:none; + background:transparent; + border-radius:0; + box-shadow:none; + + display:flex; + align-items:center; + justify-content:center; + + font-size: 33px; /* ~25% up from ~26px */ + line-height: 1; + opacity: .45; + filter: grayscale(1); +} + +/* Weight label */ +.bitVal{ + font-family:"DSEG7ClassicRegular", ui-monospace, monospace; + font-size: 28px; /* keep readable */ + color: var(--text); + opacity: .95; + line-height: 1.05; + min-height: 32px; + + /* prevent overlap: allow wrapping + constrain */ + max-width: 90px; + white-space: normal; + overflow-wrap: anywhere; + text-align:center; } /* Responsive */ @media (max-width: 980px){ - .topGrid{ grid-template-columns: 1fr; } - .toolboxDock{ position: static; justify-self: stretch; } - body.toolbox-closed .toolboxDock{ display:none; } - .bitsGrid{ max-width: 100%; } + .wrap{ max-width: 980px; } + .bitsGrid{ grid-template-columns: repeat(6, minmax(90px, 1fr)); } +} + +@media (max-width: 720px){ + .wrap{ + padding-right: 20px; /* on small screens, don’t reserve fixed space */ + } + .toolbox{ + right: 10px; + width: min(var(--toolbox-width), calc(100vw - 20px)); + } + .binaryValue{ + max-width: calc(100vw - 40px); + } } @media (max-width: 520px){ - .btn{ min-width: 150px; } + .bitsGrid{ grid-template-columns: repeat(4, minmax(90px, 1fr)); } } From ae91944cb4e51c3d03542c13fd56d908efc5735f Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 19:35:39 +0000 Subject: [PATCH 11/23] Fixed Changes: - The bit value when in two's compliment mode is not right, it should be on the same line, not wrapped - The random button should glow when the random function is running, it shouldn't be solid green all the time - The reset button, can you make the background of it red when hovered over too? I want it to pulse red on the background as well - Make the title of this page (in head) "Binary Simulator" - Reinstate the BaseLayout layout which we created previously - The shifts for Two's Compliment do not work. An Arithmetic Right Shift would keep the value of the most significant bit. Signed-off-by: Alexander Lyall --- src/pages/binary.astro | 129 ++++++-------- src/scripts/binary.js | 127 ++++++-------- src/styles/binary.css | 383 +++++++++++++++-------------------------- 3 files changed, 247 insertions(+), 392 deletions(-) diff --git a/src/pages/binary.astro b/src/pages/binary.astro index e9614aa..e585a28 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,39 +1,37 @@ --- -/* - Binary | Computing:Box - - Toolbox is fixed to the right, and can be collapsed without disappearing. - - Main content reserves space when toolbox is open, and recentres when hidden. -*/ +import BaseLayout from "../layouts/BaseLayout.astro"; import "../styles/binary.css"; --- - - - - - - Binary | Computing:Box - + + - - - +
+
+ +
+
+
Denary
+
0
- -
+ + +
+ - -
+ +
Custom
-
- - - - +
+ +
-
Random runs briefly then stops automatically.
-
+ - -
+
Random runs briefly then stops automatically.
+ + + +
Tools
- - + +
-
- - +
+ +
- -
- - + + + + +
- -
-
-
Denary
-
0
- -
Binary
-
0000 0000
- -
-
- -
-
-
-
- - - - + +
diff --git a/src/scripts/binary.js b/src/scripts/binary.js index 5d7af61..197efa9 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,6 +1,6 @@ // src/scripts/binary.js -// Computing:Box — Binary page logic (Unsigned + Two's Complement) -// Matches IDs/classes in your current binary.astro HTML. +// Computing:Box — Binary Simulator logic (Unsigned + Two's Complement) +// Matches IDs/classes in binary.astro (() => { /* ----------------------------- @@ -13,10 +13,11 @@ const modeToggle = document.getElementById("modeToggle"); const modeHint = document.getElementById("modeHint"); + const lblUnsigned = document.getElementById("lblUnsigned"); + const lblTwos = document.getElementById("lblTwos"); const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); - const btnShiftLeft = document.getElementById("btnShiftLeft"); const btnShiftRight = document.getElementById("btnShiftRight"); @@ -28,7 +29,7 @@ const btnBitsUp = document.getElementById("btnBitsUp"); const btnBitsDown = document.getElementById("btnBitsDown"); - // Toolbox toggle + const toolbox = document.getElementById("toolbox"); const toolboxToggle = document.getElementById("toolboxToggle"); /* ----------------------------- @@ -94,22 +95,18 @@ const u = bitsToUnsignedBigInt(); const signBit = bits[bitCount - 1] === true; if (!signBit) return u; - - // negative: u - 2^n - return u - pow2Big(bitCount); + return u - pow2Big(bitCount); // negative: u - 2^n } function signedBigIntToBitsTwos(vSigned) { const span = pow2Big(bitCount); // 2^n - let v = vSigned; - - // Convert to unsigned representative: v mod 2^n - v = ((v % span) + span) % span; + // convert to unsigned representative: v mod 2^n + const v = ((vSigned % span) + span) % span; unsignedBigIntToBits(v); } function formatBinaryGrouped() { - // MSB..LSB with a space every 4 bits + // MSB..LSB with space every 4 bits let s = ""; for (let i = bitCount - 1; i >= 0; i--) { s += bits[i] ? "1" : "0"; @@ -122,11 +119,9 @@ function updateModeHint() { if (!modeHint) return; if (isTwosMode()) { - modeHint.textContent = - "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; + modeHint.textContent = "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; } else { - modeHint.textContent = - "Tip: In unsigned binary, all bits represent positive values."; + modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; } } @@ -137,7 +132,7 @@ bitCount = clampInt(count, 1, 64); if (bitsInput) bitsInput.value = String(bitCount); - // resize bits array, preserve existing LSBs where possible + // preserve existing LSBs where possible const oldBits = bits.slice(); bits = new Array(bitCount).fill(false); for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; @@ -150,7 +145,7 @@ bitEl.className = "bit"; bitEl.innerHTML = ` - +
+ - - + + \ No newline at end of file diff --git a/src/pages/hex-colours.astro b/src/pages/hex-colours.astro new file mode 100644 index 0000000..d1e7e40 --- /dev/null +++ b/src/pages/hex-colours.astro @@ -0,0 +1,99 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+ + +
+
+ +
+
+
+
Denary (R, G, B)
+
+ 0 + 0 + 0 +
+
+ +
+
Hexadecimal
+
+ #00 + 00 + 00 +
+
+ +
+
Binary
+
+ 00000000 + 00000000 + 00000000 +
+
+
+ +
+
+
+
Colour
+
+
+
+
Inverted
+
+
+
+ +
+ +
+
+
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/pages/hexadecimal.astro b/src/pages/hexadecimal.astro index edfe098..e74ca17 100644 --- a/src/pages/hexadecimal.astro +++ b/src/pages/hexadecimal.astro @@ -1,8 +1,94 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; -import HexSimulator from "../components/simulators/HexSimulator.astro"; --- - - - + +
+ + +
+
+
+
Denary
+
0
+ +
Hexadecimal
+
00
+ +
Binary
+
00000000
+
+ +
+ +
+
+
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/pages/logic-gates.astro b/src/pages/logic-gates.astro new file mode 100644 index 0000000..7eb4c54 --- /dev/null +++ b/src/pages/logic-gates.astro @@ -0,0 +1,63 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/logic-gates.css"; +--- + + +
+ + + +
+ +
+
+
Interactive Simulator
+
LOGIC GATES
+
+
+ Drag items from the toolbox to the board. Drag from output ports to input ports to wire. Click a wire or node and press Delete to remove it. +
+
+ +
+ +
+ +
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/src/scripts/binary.js b/src/scripts/binary.js index a557ff9..e596092 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,6 +1,3 @@ -// src/scripts/binary.js -// Computing:Box — Binary page logic (Unsigned + Two's Complement) - (() => { /* ----------------------------- DOM @@ -12,6 +9,10 @@ const modeToggle = document.getElementById("modeToggle"); const modeHint = document.getElementById("modeHint"); + + // Connect the text labels to the JS + const lblUnsigned = document.getElementById("lblUnsigned"); + const lblTwos = document.getElementById("lblTwos"); const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); @@ -27,14 +28,13 @@ const btnBitsDown = document.getElementById("btnBitsDown"); const toolboxToggle = document.getElementById("toolboxToggle"); - const toolboxPanel = document.getElementById("toolboxPanel"); + const binaryPage = document.getElementById("binaryPage"); /* ----------------------------- STATE ----------------------------- */ let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); let bits = new Array(bitCount).fill(false); - let randomTimer = null; /* ----------------------------- @@ -54,7 +54,7 @@ } function unsignedMaxExclusive(nBits) { - return pow2Big(nBits); + return pow2Big(nBits); } function unsignedMaxValue(nBits) { @@ -94,7 +94,8 @@ function signedBigIntToBitsTwos(vSigned) { const span = pow2Big(bitCount); - let v = ((vSigned % span) + span) % span; + let v = vSigned; + v = ((v % span) + span) % span; unsignedBigIntToBits(v); } @@ -102,17 +103,33 @@ let s = ""; for (let i = bitCount - 1; i >= 0; i--) { s += bits[i] ? "1" : "0"; - const posFromRight = (bitCount - i); - if (i !== 0 && posFromRight % 4 === 0) s += " "; + const posFromLeft = (bitCount - i); + if (i !== 0 && posFromLeft % 4 === 0) s += " "; } - return s; + return s.trimEnd(); } function updateModeHint() { if (!modeHint) return; - modeHint.textContent = isTwosMode() - ? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value." - : "Tip: In unsigned binary, all bits represent positive values."; + if (isTwosMode()) { + modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value."; + } else { + modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; + } + } + + /* ----------------------------- + RESPONSIVE GRID COLS + ----------------------------- */ + function computeColsForBitsGrid() { + if (!bitsGrid) return; + const wrap = bitsGrid.parentElement; + if (!wrap) return; + + const width = wrap.getBoundingClientRect().width; + const minCell = 100; + const cols = clampInt(Math.floor(width / minCell), 1, 12); + bitsGrid.style.setProperty("--cols", String(Math.min(cols, bitCount))); } /* ----------------------------- @@ -127,18 +144,25 @@ for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; bitsGrid.innerHTML = ""; + bitsGrid.classList.toggle("bitsFew", bitCount < 8); for (let i = bitCount - 1; i >= 0; i--) { const bitEl = document.createElement("div"); bitEl.className = "bit"; + bitEl.innerHTML = ` - +
`; + bitsGrid.appendChild(bitEl); } @@ -150,6 +174,7 @@ }); }); + computeColsForBitsGrid(); updateUI(); } @@ -161,14 +186,14 @@ const label = document.getElementById(`bitLabel-${i}`); if (!label) continue; - // Keep label on ONE LINE (no wrapping) - label.style.whiteSpace = "nowrap"; - + let valStr; if (isTwosMode() && i === bitCount - 1) { - label.textContent = `-${pow2Big(bitCount - 1).toString()}`; + valStr = `-${pow2Big(bitCount - 1).toString()}`; } else { - label.textContent = pow2Big(i).toString(); + valStr = pow2Big(i).toString(); } + label.textContent = valStr; + label.style.setProperty('--len', valStr.length); } } @@ -189,12 +214,23 @@ function updateReadout() { if (!denaryEl || !binaryEl) return; - denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString(); + if (isTwosMode()) { + denaryEl.textContent = bitsToSignedBigIntTwos().toString(); + } else { + denaryEl.textContent = bitsToUnsignedBigInt().toString(); + } binaryEl.textContent = formatBinaryGrouped(); } function updateUI() { updateModeHint(); + + // Toggle the glowing CSS class on the active mode text + if (lblUnsigned && lblTwos) { + lblUnsigned.classList.toggle("activeMode", !isTwosMode()); + lblTwos.classList.toggle("activeMode", isTwosMode()); + } + updateBitLabels(); syncSwitchesToBits(); updateBulbs(); @@ -202,20 +238,25 @@ } /* ----------------------------- - INPUT SETTERS + SET FROM BINARY STRING ----------------------------- */ function setFromBinaryString(binStr) { const clean = String(binStr ?? "").replace(/\s+/g, ""); if (!/^[01]+$/.test(clean)) return false; + const padded = clean.slice(-bitCount).padStart(bitCount, "0"); for (let i = 0; i < bitCount; i++) { const charFromRight = padded[padded.length - 1 - i]; bits[i] = charFromRight === "1"; } + updateUI(); return true; } + /* ----------------------------- + SET FROM DENARY INPUT + ----------------------------- */ function setFromDenaryInput(vStr) { const raw = String(vStr ?? "").trim(); if (!raw) return false; @@ -224,9 +265,7 @@ try { if (!/^-?\d+$/.test(raw)) return false; v = BigInt(raw); - } catch { - return false; - } + } catch { return false; } if (isTwosMode()) { const min = twosMin(bitCount); @@ -234,8 +273,7 @@ if (v < min || v > max) return false; signedBigIntToBitsTwos(v); } else { - if (v < 0n) return false; - if (v > unsignedMaxValue(bitCount)) return false; + if (v < 0n || v > unsignedMaxValue(bitCount)) return false; unsignedBigIntToBits(v); } @@ -247,18 +285,14 @@ SHIFTS ----------------------------- */ function shiftLeft() { - for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1]; + for (let i = bitCount - 1; i >= 1; i--) { bits[i] = bits[i - 1]; } bits[0] = false; updateUI(); } function shiftRight() { - // Unsigned: logical right shift (MSB becomes 0) - // Two's complement: arithmetic right shift (MSB preserved) const msb = bits[bitCount - 1]; - - for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1]; - + for (let i = 0; i < bitCount - 1; i++) { bits[i] = bits[i + 1]; } bits[bitCount - 1] = isTwosMode() ? msb : false; updateUI(); } @@ -267,8 +301,9 @@ CLEAR / INC / DEC ----------------------------- */ function clearAll() { - bits.fill(false); - updateUI(); + bits = []; + if (modeToggle) modeToggle.checked = false; + buildBits(8); } function increment() { @@ -280,8 +315,7 @@ signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); - const v = (bitsToUnsignedBigInt() + 1n) % span; - unsignedBigIntToBits(v); + unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span); } updateUI(); } @@ -295,25 +329,22 @@ signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); - const v = (bitsToUnsignedBigInt() - 1n + span) % span; - unsignedBigIntToBits(v); + unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span); } updateUI(); } /* ----------------------------- - RANDOM (with running pulse + longer run) + RANDOM ----------------------------- */ function cryptoRandomBigInt(maxExclusive) { if (maxExclusive <= 0n) return 0n; - const bitLen = maxExclusive.toString(2).length; const byteLen = Math.ceil(bitLen / 8); while (true) { const bytes = new Uint8Array(byteLen); crypto.getRandomValues(bytes); - let x = 0n; for (const b of bytes) x = (x << 8n) | BigInt(b); @@ -325,23 +356,26 @@ } function setRandomOnce() { - const span = unsignedMaxExclusive(bitCount); // 2^n + const span = unsignedMaxExclusive(bitCount); const u = cryptoRandomBigInt(span); unsignedBigIntToBits(u); updateUI(); } + function setRandomRunning(isRunning) { + if (!btnRandom) return; + btnRandom.classList.toggle("btnRandomRunning", !!isRunning); + } + function runRandomBriefly() { if (randomTimer) { clearInterval(randomTimer); randomTimer = null; } - // pulse while running - btnRandom?.classList.add("is-running"); - + setRandomRunning(true); const start = Date.now(); - const durationMs = 1125; // 25% longer than 900ms + const durationMs = 1125; const tickMs = 80; randomTimer = setInterval(() => { @@ -349,25 +383,27 @@ if (Date.now() - start >= durationMs) { clearInterval(randomTimer); randomTimer = null; - btnRandom?.classList.remove("is-running"); + setRandomRunning(false); } }, tickMs); } /* ----------------------------- - BIT WIDTH + BIT WIDTH CONTROLS ----------------------------- */ function setBitWidth(n) { - buildBits(clampInt(n, 1, 64)); + const v = clampInt(n, 1, 64); + buildBits(v); } /* ----------------------------- - TOOLBOX VISIBILITY + TOOLBOX TOGGLE ----------------------------- */ - function setToolboxVisible(isVisible) { - if (!toolboxPanel) return; - toolboxPanel.style.display = isVisible ? "flex" : "none"; - toolboxToggle?.setAttribute("aria-expanded", String(isVisible)); + function setToolboxCollapsed(collapsed) { + if (!binaryPage) return; + binaryPage.classList.toggle("toolboxCollapsed", !!collapsed); + const expanded = !collapsed; + toolboxToggle?.setAttribute("aria-expanded", expanded ? "true" : "false"); } /* ----------------------------- @@ -384,8 +420,8 @@ btnCustomDenary?.addEventListener("click", () => { const v = prompt( isTwosMode() - ? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):` - : `Enter denary (0 to ${unsignedMaxValue(bitCount)}):` + ? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):` + : `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):` ); if (v === null) return; if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width"); @@ -406,8 +442,12 @@ bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value))); toolboxToggle?.addEventListener("click", () => { - const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false"; - setToolboxVisible(!isOpen); + const isCollapsed = binaryPage?.classList.contains("toolboxCollapsed"); + setToolboxCollapsed(!isCollapsed); + }); + + window.addEventListener("resize", () => { + computeColsForBitsGrid(); }); /* ----------------------------- @@ -415,5 +455,5 @@ ----------------------------- */ updateModeHint(); buildBits(bitCount); - setToolboxVisible(true); -})(); + setToolboxCollapsed(false); +})(); \ No newline at end of file diff --git a/src/scripts/hexColours.js b/src/scripts/hexColours.js new file mode 100644 index 0000000..e64be80 --- /dev/null +++ b/src/scripts/hexColours.js @@ -0,0 +1,241 @@ +// src/scripts/hexColours.js +// Computing:Box — Hex Colours logic + +(() => { + /* ----------------------------- + DOM + ----------------------------- */ + const colorGrid = document.getElementById("colorGrid"); + const denaryEl = document.getElementById("denaryNumber"); + const binaryEl = document.getElementById("binaryNumber"); + const hexEl = document.getElementById("hexNumber"); + const previewColor = document.getElementById("previewColor"); + const previewInverted = document.getElementById("previewInverted"); + + const btnCustomHex = document.getElementById("btnCustomHex"); + const btnCustomRGB = document.getElementById("btnCustomRGB"); + const btnInvert = document.getElementById("btnInvert"); + const btnRandom = document.getElementById("btnRandom"); + const btnClear = document.getElementById("btnClear"); + + const toolboxToggle = document.getElementById("toolboxToggle"); + const colorPage = document.getElementById("colorPage"); + + /* ----------------------------- + STATE + ----------------------------- */ + // rgb[0]=Red, rgb[1]=Green, rgb[2]=Blue (Values 0-255) + let rgb = [0, 0, 0]; + let randomTimer = null; + + /* ----------------------------- + BUILD UI + ----------------------------- */ + function buildGrid() { + if (!colorGrid) return; + colorGrid.innerHTML = ""; + + const colorClasses = ['text-red', 'text-green', 'text-blue']; + + for (let c = 0; c < 3; c++) { + const group = document.createElement("div"); + group.className = "colorGroup"; + + for (let i = 1; i >= 0; i--) { + const col = document.createElement("div"); + col.className = "hexCol"; + + let cardHTML = ` +
+
+ + +
+
0
+
+ `; + + for (let j = 3; j >= 0; j--) { + cardHTML += ` +
+ +
${1 << j}
+
+ `; + } + + cardHTML += ` +
+
+
${16 ** i}
+ `; + + col.innerHTML = cardHTML; + + const incBtn = col.querySelector(`#colorInc-${c}-${i}`); + const decBtn = col.querySelector(`#colorDec-${c}-${i}`); + + incBtn.addEventListener("click", () => { + const weight = 16 ** i; + rgb[c] = (rgb[c] + weight) % 256; + updateUI(); + }); + + decBtn.addEventListener("click", () => { + const weight = 16 ** i; + rgb[c] = (rgb[c] - weight + 256) % 256; + updateUI(); + }); + + group.appendChild(col); + } + colorGrid.appendChild(group); + } + } + + /* ----------------------------- + UI UPDATE + ----------------------------- */ + function updateUI() { + if (denaryEl) { + denaryEl.innerHTML = ` + ${rgb[0]} + ${rgb[1]} + ${rgb[2]} + `; + } + + const hexVals = rgb.map(v => v.toString(16).padStart(2, '0').toUpperCase()); + const fullHexString = `#${hexVals.join('')}`; + + if (hexEl) { + hexEl.innerHTML = ` + #${hexVals[0]} + ${hexVals[1]} + ${hexVals[2]} + `; + } + + if (binaryEl) { + binaryEl.innerHTML = ` + ${rgb[0].toString(2).padStart(8, '0')} + ${rgb[1].toString(2).padStart(8, '0')} + ${rgb[2].toString(2).padStart(8, '0')} + `; + } + + if (previewColor) previewColor.style.backgroundColor = fullHexString; + + const invertedHexString = "#" + rgb.map(v => (255 - v).toString(16).padStart(2, '0').toUpperCase()).join(''); + if (previewInverted) previewInverted.style.backgroundColor = invertedHexString; + + for (let c = 0; c < 3; c++) { + const val = rgb[c]; + const nibbles = [val % 16, Math.floor(val / 16)]; + + for (let i = 0; i < 2; i++) { + const display = document.getElementById(`colorDisplay-${c}-${i}`); + if (display) display.textContent = nibbles[i].toString(16).toUpperCase(); + + for (let j = 0; j < 4; j++) { + const bulb = document.getElementById(`colorBulb-${c}-${i}-${j}`); + if (bulb) { + const isOn = (nibbles[i] & (1 << j)) !== 0; + bulb.classList.toggle("on", isOn); + } + } + } + } + } + + /* ----------------------------- + ACTIONS + ----------------------------- */ + function clearAll() { + rgb = [0, 0, 0]; + updateUI(); + } + + function setRandomOnce() { + const arr = new Uint8Array(3); + crypto.getRandomValues(arr); + rgb = [arr[0], arr[1], arr[2]]; + updateUI(); + } + + function runRandomBriefly() { + if (randomTimer) { + clearInterval(randomTimer); + randomTimer = null; + } + if (btnRandom) btnRandom.classList.add("btnRandomRunning"); + + const start = Date.now(); + const durationMs = 1125; + const tickMs = 80; + + randomTimer = setInterval(() => { + setRandomOnce(); + if (Date.now() - start >= durationMs) { + clearInterval(randomTimer); + randomTimer = null; + if (btnRandom) btnRandom.classList.remove("btnRandomRunning"); + } + }, tickMs); + } + + /* ----------------------------- + EVENTS + ----------------------------- */ + btnCustomHex?.addEventListener("click", () => { + let v = prompt("Enter a 6-character hex code (e.g. FF0055):"); + if (v === null) return; + v = v.replace(/\s+/g, "").replace(/^#/i, "").toUpperCase(); + + if (!/^[0-9A-F]{6}$/.test(v)) return alert("Invalid hex code. Please enter exactly 6 hexadecimal characters."); + + rgb = [ + parseInt(v.substring(0, 2), 16), + parseInt(v.substring(2, 4), 16), + parseInt(v.substring(4, 6), 16) + ]; + updateUI(); + }); + + btnCustomRGB?.addEventListener("click", () => { + const v = prompt("Enter R, G, B values (0-255) separated by commas (e.g. 255, 128, 0):"); + if (v === null) return; + + const parts = v.split(',').map(s => parseInt(s.trim(), 10)); + if (parts.length !== 3 || parts.some(isNaN) || parts.some(n => n < 0 || n > 255)) { + return alert("Invalid input. Please provide three numbers between 0 and 255."); + } + + rgb = parts; + updateUI(); + }); + + btnInvert?.addEventListener("click", () => { + rgb = rgb.map(v => 255 - v); + updateUI(); + }); + + btnClear?.addEventListener("click", clearAll); + btnRandom?.addEventListener("click", runRandomBriefly); + + toolboxToggle?.addEventListener("click", () => { + const isCollapsed = colorPage?.classList.contains("toolboxCollapsed"); + colorPage.classList.toggle("toolboxCollapsed", !isCollapsed); + toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false"); + }); + + /* ----------------------------- + INIT + ----------------------------- */ + buildGrid(); + updateUI(); +})(); \ No newline at end of file diff --git a/src/scripts/hexadecimal.js b/src/scripts/hexadecimal.js new file mode 100644 index 0000000..8b79df1 --- /dev/null +++ b/src/scripts/hexadecimal.js @@ -0,0 +1,312 @@ +// src/scripts/hexadecimal.js +// Computing:Box — Hexadecimal page logic + +(() => { + /* ----------------------------- + DOM + ----------------------------- */ + const hexGrid = document.getElementById("hexGrid"); + const denaryEl = document.getElementById("denaryNumber"); + const binaryEl = document.getElementById("binaryNumber"); + const hexEl = document.getElementById("hexNumber"); + const digitsInput = document.getElementById("digitsInput"); + + const btnCustomBinary = document.getElementById("btnCustomBinary"); + const btnCustomDenary = document.getElementById("btnCustomDenary"); + const btnCustomHex = document.getElementById("btnCustomHex"); + + const btnDec = document.getElementById("btnDec"); + const btnInc = document.getElementById("btnInc"); + const btnClear = document.getElementById("btnClear"); + const btnRandom = document.getElementById("btnRandom"); + + const btnDigitsUp = document.getElementById("btnDigitsUp"); + const btnDigitsDown = document.getElementById("btnDigitsDown"); + + const toolboxToggle = document.getElementById("toolboxToggle"); + const hexPage = document.getElementById("hexPage"); + + /* ----------------------------- + STATE + ----------------------------- */ + let hexCount = clampInt(Number(digitsInput?.value ?? 2), 1, 16); + let nibbles = new Array(hexCount).fill(0); + let randomTimer = null; + + /* ----------------------------- + HELPERS + ----------------------------- */ + function clampInt(n, min, max) { + if (!Number.isFinite(n)) return min; + return Math.max(min, Math.min(max, Math.trunc(n))); + } + + function maxExclusive() { + return 1n << BigInt(hexCount * 4); + } + + function maxValue() { + return maxExclusive() - 1n; + } + + function getValue() { + let v = 0n; + for (let i = 0; i < hexCount; i++) { + v += BigInt(nibbles[i]) << BigInt(i * 4); + } + return v; + } + + function setValue(v) { + if (v < 0n) return false; + if (v > maxValue()) return false; + + for (let i = 0; i < hexCount; i++) { + nibbles[i] = Number((v >> BigInt(i * 4)) & 0xFn); + } + return true; + } + + /* ----------------------------- + RESPONSIVE GRID + ----------------------------- */ + function computeColsForHexGrid() { + if (!hexGrid) return; + hexGrid.style.setProperty("--cols", String(Math.min(hexCount, 8))); + hexGrid.classList.toggle("bitsFew", hexCount < 4); + } + + /* ----------------------------- + BUILD UI (CARDS + BULBS) + ----------------------------- */ + function buildGrid(count) { + hexCount = clampInt(count, 1, 16); + if (digitsInput) digitsInput.value = String(hexCount); + + const oldNibbles = nibbles.slice(); + nibbles = new Array(hexCount).fill(0); + for (let i = 0; i < Math.min(oldNibbles.length, hexCount); i++) { + nibbles[i] = oldNibbles[i]; + } + + hexGrid.innerHTML = ""; + + for (let i = hexCount - 1; i >= 0; i--) { + const col = document.createElement("div"); + col.className = "hexCol"; + + let cardHTML = ` +
+
+ + +
+
0
+
+ `; + + for(let j = 3; j >= 0; j--) { + cardHTML += ` +
+ +
${1 << j}
+
+ `; + } + + cardHTML += ` +
+
+
${(1n << BigInt(i * 4)).toString()}
+ `; + + col.innerHTML = cardHTML; + + const incBtn = col.querySelector(`#hexInc-${i}`); + const decBtn = col.querySelector(`#hexDec-${i}`); + + incBtn.addEventListener("click", () => { + const span = maxExclusive(); + const weight = 1n << BigInt(i * 4); + setValue((getValue() + weight) % span); + updateUI(); + }); + + decBtn.addEventListener("click", () => { + const span = maxExclusive(); + const weight = 1n << BigInt(i * 4); + setValue((getValue() - weight + span) % span); + updateUI(); + }); + + hexGrid.appendChild(col); + } + + computeColsForHexGrid(); + updateUI(); + } + + /* ----------------------------- + UI UPDATE + ----------------------------- */ + function updateUI() { + const val = getValue(); + + if (denaryEl) denaryEl.textContent = val.toString(); + if (hexEl) hexEl.textContent = val.toString(16).toUpperCase().padStart(hexCount, '0'); + + if (binaryEl) { + let binStr = ""; + for (let i = hexCount - 1; i >= 0; i--) { + binStr += nibbles[i].toString(2).padStart(4, '0') + " "; + } + binaryEl.textContent = binStr.trimEnd(); + } + + for (let i = 0; i < hexCount; i++) { + const display = document.getElementById(`hexDisplay-${i}`); + if (display) display.textContent = nibbles[i].toString(16).toUpperCase(); + + for (let j = 0; j < 4; j++) { + const bulb = document.getElementById(`hexBulb-${i}-${j}`); + if (bulb) { + const isOn = (nibbles[i] & (1 << j)) !== 0; + bulb.classList.toggle("on", isOn); + } + } + } + } + + /* ----------------------------- + CLEAR / INC / DEC + ----------------------------- */ + function clearAll() { + nibbles.fill(0); + buildGrid(2); + } + + function increment() { + const span = maxExclusive(); + setValue((getValue() + 1n) % span); + updateUI(); + } + + function decrement() { + const span = maxExclusive(); + setValue((getValue() - 1n + span) % span); + updateUI(); + } + + /* ----------------------------- + RANDOM + ----------------------------- */ + function cryptoRandomBigInt(maxExcl) { + if (maxExcl <= 0n) return 0n; + const bitLen = maxExcl.toString(2).length; + const byteLen = Math.ceil(bitLen / 8); + + while (true) { + const bytes = new Uint8Array(byteLen); + crypto.getRandomValues(bytes); + let x = 0n; + for (const b of bytes) x = (x << 8n) | BigInt(b); + + const extraBits = BigInt(byteLen * 8 - bitLen); + if (extraBits > 0n) x = x >> extraBits; + if (x < maxExcl) return x; + } + } + + function setRandomOnce() { + const u = cryptoRandomBigInt(maxExclusive()); + setValue(u); + updateUI(); + } + + function runRandomBriefly() { + if (randomTimer) { + clearInterval(randomTimer); + randomTimer = null; + } + if (btnRandom) btnRandom.classList.add("btnRandomRunning"); + + const start = Date.now(); + const durationMs = 1125; + const tickMs = 80; + + randomTimer = setInterval(() => { + setRandomOnce(); + if (Date.now() - start >= durationMs) { + clearInterval(randomTimer); + randomTimer = null; + if (btnRandom) btnRandom.classList.remove("btnRandomRunning"); + } + }, tickMs); + } + + /* ----------------------------- + EVENTS + ----------------------------- */ + btnCustomHex?.addEventListener("click", () => { + const v = prompt(`Enter hexadecimal (0-9, A-F). Current width: ${hexCount} digits`); + if (v === null) return; + const clean = v.replace(/\s+/g, "").toUpperCase(); + + if (!/^[0-9A-F]+$/.test(clean)) return alert("Invalid hexadecimal."); + if (clean.length > hexCount) return alert("Value too large for current digit width."); + + if (!setValue(BigInt("0x" + clean))) alert("Value out of range."); + else updateUI(); + }); + + btnCustomBinary?.addEventListener("click", () => { + const v = prompt(`Enter binary (0, 1). Current width: ${hexCount * 4} bits`); + if (v === null) return; + const clean = v.replace(/\s+/g, ""); + + if (!/^[01]+$/.test(clean)) return alert("Invalid binary."); + if (clean.length > hexCount * 4) return alert("Value too large for current digit width."); + + if (!setValue(BigInt("0b" + clean))) alert("Value out of range."); + else updateUI(); + }); + + btnCustomDenary?.addEventListener("click", () => { + const v = prompt(`Enter denary (0 to ${maxValue().toString()}):`); + if (v === null) return; + const clean = v.trim(); + + if (!/^\d+$/.test(clean)) return alert("Invalid denary. Digits only."); + if (!setValue(BigInt(clean))) alert(`Value out of range. Enter a number between 0 and ${maxValue().toString()}.`); + else updateUI(); + }); + + btnInc?.addEventListener("click", increment); + btnDec?.addEventListener("click", decrement); + + btnClear?.addEventListener("click", clearAll); + btnRandom?.addEventListener("click", runRandomBriefly); + + btnDigitsUp?.addEventListener("click", () => buildGrid(hexCount + 1)); + btnDigitsDown?.addEventListener("click", () => buildGrid(hexCount - 1)); + + digitsInput?.addEventListener("change", () => buildGrid(Number(digitsInput.value))); + + toolboxToggle?.addEventListener("click", () => { + const isCollapsed = hexPage?.classList.contains("toolboxCollapsed"); + hexPage.classList.toggle("toolboxCollapsed", !isCollapsed); + toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false"); + }); + + window.addEventListener("resize", computeColsForHexGrid); + + /* ----------------------------- + INIT + ----------------------------- */ + buildGrid(hexCount); + +})(); \ No newline at end of file diff --git a/src/scripts/logicGates.js b/src/scripts/logicGates.js new file mode 100644 index 0000000..a39d333 --- /dev/null +++ b/src/scripts/logicGates.js @@ -0,0 +1,489 @@ +// src/scripts/logicGates.js +// Computing:Box — Drag & Drop Logic Builder + +(() => { + /* --- DOM Elements --- */ + const workspace = document.getElementById("workspace"); + const wireLayer = document.getElementById("wireLayer"); + const ttContainer = document.getElementById("truthTableContainer"); + const toolboxGrid = document.getElementById("toolboxGrid"); + + const btnClearBoard = document.getElementById("btnClearBoard"); + const toolboxToggle = document.getElementById("toolboxToggle"); + const logicPage = document.getElementById("logicPage"); + + /* --- ANSI Gate SVGs (Strict 100x50 with built-in tails) --- */ + const GATE_SVGS = { + 'AND': ``, + 'OR': ``, + 'NOT': ``, + 'NAND': ``, + 'NOR': ``, + 'XOR': ``, + 'XNOR': `` + }; + + const INPUT_SVG = ``; + const OUTPUT_SVG = ``; + + /* --- State --- */ + let nodes = {}; + let connections = []; + + let nextNodeId = 1; + let nextWireId = 1; + let inputCount = 0; + let outputCount = 0; + + let isDraggingNode = null; + let dragOffset = { x: 0, y: 0 }; + let clickStartX = 0, clickStartY = 0; // Fixes switch drag conflict + + let wiringStart = null; + let tempWirePath = null; + + let selectedWireId = null; + let selectedNodeId = null; + + /* --- Setup Toolbox --- */ + function initToolbox() { + if(!toolboxGrid) return; + let html = ` +
+
+
Input
+
+
+
+
Output
+
+ `; + + Object.keys(GATE_SVGS).forEach(gate => { + html += ` +
+ ${GATE_SVGS[gate]} +
${gate}
+
+ `; + }); + + toolboxGrid.innerHTML = html; + + document.querySelectorAll('.drag-item').forEach(item => { + item.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('spawnType', item.dataset.spawn); + if(item.dataset.spawn === 'GATE') e.dataTransfer.setData('gateType', item.dataset.gate); + }); + }); + } + + /* --- Math & Geometry --- */ + function getPortCoords(nodeId, portDataAttr) { + const node = nodes[nodeId]; + if (!node || !node.el) return {x:0, y:0}; + + const portEl = node.el.querySelector(`[data-port="${portDataAttr}"]`); + if (!portEl) return {x:0, y:0}; + + const wsRect = workspace.getBoundingClientRect(); + const portRect = portEl.getBoundingClientRect(); + + return { + x: portRect.left - wsRect.left + (portRect.width / 2), + y: portRect.top - wsRect.top + (portRect.height / 2) + }; + } + + function drawBezier(x1, y1, x2, y2) { + const cpDist = Math.abs(x2 - x1) * 0.6 + 20; + return `M ${x1} ${y1} C ${x1 + cpDist} ${y1}, ${x2 - cpDist} ${y2}, ${x2} ${y2}`; + } + + /* --- Rendering --- */ + function renderWires() { + let svgHTML = ''; + + connections.forEach(conn => { + const from = getPortCoords(conn.fromNode, 'out'); + const to = getPortCoords(conn.toNode, `in${conn.toPort}`); + const sourceNode = nodes[conn.fromNode]; + const isActive = sourceNode && sourceNode.value === true; + const isSelected = conn.id === selectedWireId; + + svgHTML += ``; + }); + + if (wiringStart && tempWirePath) { + svgHTML += ``; + } + + wireLayer.innerHTML = svgHTML; + } + + function updateNodePositions() { + Object.values(nodes).forEach(n => { + if (n.el) { + n.el.style.left = `${n.x}px`; + n.el.style.top = `${n.y}px`; + } + }); + renderWires(); + } + + function clearSelection() { + selectedWireId = null; + selectedNodeId = null; + document.querySelectorAll('.lg-node.selected').forEach(el => el.classList.remove('selected')); + renderWires(); + } + + /* --- Logic Evaluation --- */ + function evaluateGraph(overrideInputs = null) { + let context = {}; + + Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => { + context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value; + }); + + let changed = true; + let loops = 0; + + while (changed && loops < 10) { + changed = false; + loops++; + + Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => { + let in1Conn = connections.find(c => c.toNode === gate.id && c.toPort === '1'); + let in2Conn = connections.find(c => c.toNode === gate.id && c.toPort === '2'); + + let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false; + let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false; + + let res = false; + switch(gate.gateType) { + case 'AND': res = val1 && val2; break; + case 'OR': res = val1 || val2; break; + case 'NOT': res = !val1; break; + case 'NAND': res = !(val1 && val2); break; + case 'NOR': res = !(val1 || val2); break; + case 'XOR': res = val1 !== val2; break; + case 'XNOR': res = val1 === val2; break; + } + + if (context[gate.id] !== res) { + context[gate.id] = res; + changed = true; + } + }); + } + + let outStates = {}; + Object.values(nodes).filter(n => n.type === 'OUTPUT').forEach(out => { + let conn = connections.find(c => c.toNode === out.id); + let res = conn ? (context[conn.fromNode] || false) : false; + outStates[out.id] = res; + }); + + if (!overrideInputs) { + Object.values(nodes).forEach(n => { + if (n.type === 'GATE') n.value = context[n.id] || false; + if (n.type === 'OUTPUT') { + n.value = outStates[n.id] || false; + const bulb = n.el.querySelector('.bulb'); + if (bulb) bulb.classList.toggle('on', n.value); + } + }); + } + + return outStates; + } + + /* --- Truth Table Generation --- */ + function generateTruthTable() { + if (!ttContainer) return; + + const inNodes = Object.values(nodes).filter(n => n.type === 'INPUT').sort((a,b) => a.label.localeCompare(b.label)); + const outNodes = Object.values(nodes).filter(n => n.type === 'OUTPUT').sort((a,b) => a.label.localeCompare(b.label)); + + if (inNodes.length === 0 || outNodes.length === 0) { + ttContainer.innerHTML = '
Add inputs and outputs to generate table.
'; + return; + } + if (inNodes.length > 6) { + ttContainer.innerHTML = '
Maximum 6 inputs supported.
'; + return; + } + + let html = ''; + inNodes.forEach(n => html += ``); + outNodes.forEach(n => html += ``); + html += ''; + + const numRows = Math.pow(2, inNodes.length); + + for (let i = 0; i < numRows; i++) { + let override = {}; + inNodes.forEach((n, idx) => { + override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1; + }); + + let outStates = evaluateGraph(override); + + html += ''; + inNodes.forEach(n => { + let val = override[n.id]; + html += ``; + }); + outNodes.forEach(n => { + let val = outStates[n.id]; + html += ``; + }); + html += ''; + } + + html += '
${n.label}${n.label}
${val ? 1 : 0}${val ? 1 : 0}
'; + ttContainer.innerHTML = html; + } + + function runSimulation() { + evaluateGraph(); + renderWires(); + generateTruthTable(); + } + + /* --- Node Creation --- */ + function createNodeElement(node) { + const el = document.createElement('div'); + el.className = `lg-node`; + el.dataset.id = node.id; + el.style.left = `${node.x}px`; + el.style.top = `${node.y}px`; + + let innerHTML = `
${node.label}
`; + + if (node.type === 'INPUT') { + innerHTML += ` +
+ ${INPUT_SVG} +
+ `; + } + else if (node.type === 'OUTPUT') { + innerHTML += ` +
+ ${OUTPUT_SVG} +
+ `; + } + else if (node.type === 'GATE') { + const isNot = node.gateType === 'NOT'; + innerHTML += ` +
+ ${!isNot ? `
` : ''} + ${GATE_SVGS[node.gateType]} +
+ `; + } + + innerHTML += `
`; + el.innerHTML = innerHTML; + workspace.appendChild(el); + node.el = el; + + if (node.type === 'INPUT') { + // Custom click handler to prevent dragging from toggling the switch + el.querySelector('.switch').addEventListener('click', (e) => { + const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY); + if (isDraggingNode || dist > 3) { + e.preventDefault(); + } else { + node.value = !node.value; + el.querySelector('.switch').classList.toggle('active-sim', node.value); + el.querySelector('.slider').style.background = node.value ? 'rgba(40,240,122,.25)' : ''; + el.querySelector('.slider').style.borderColor = node.value ? 'rgba(40,240,122,.30)' : ''; + el.querySelector('.slider').innerHTML = node.value ? `` : ''; + runSimulation(); + } + }); + } + + return el; + } + + function spawnNode(type, gateType = null, dropX = null, dropY = null) { + let label = ''; + if (type === 'INPUT') { inputCount++; label = String.fromCharCode(64 + inputCount); } + if (type === 'OUTPUT') { outputCount++; label = `Q${outputCount}`; } + if (type === 'GATE') { label = gateType; } + + const id = `node_${nextNodeId++}`; + + const offset = Math.floor(Math.random() * 40); + const x = dropX !== null ? dropX : (type === 'INPUT' ? 50 : (type === 'OUTPUT' ? 600 : 300) + offset); + const y = dropY !== null ? dropY : 150 + offset; + + const node = { id, type, gateType, label, x, y, value: false, el: null }; + nodes[id] = node; + createNodeElement(node); + runSimulation(); + } + + /* --- Global Interaction Handlers --- */ + + workspace.addEventListener('mousedown', (e) => { + clickStartX = e.clientX; + clickStartY = e.clientY; + + const port = e.target.closest('.lg-port'); + if (port) { + const nodeEl = port.closest('.lg-node'); + const portId = port.dataset.port; + + if (portId.startsWith('in')) { + const existingIdx = connections.findIndex(c => c.toNode === nodeEl.dataset.id && c.toPort === portId.replace('in', '')); + if (existingIdx !== -1) { + connections.splice(existingIdx, 1); + runSimulation(); + return; + } + } + + if (portId === 'out') { + const coords = getPortCoords(nodeEl.dataset.id, 'out'); + wiringStart = { node: nodeEl.dataset.id, port: portId, x: coords.x, y: coords.y }; + tempWirePath = { x: coords.x, y: coords.y }; + return; + } + } + + const wire = e.target.closest('.lg-wire'); + if (wire && wire.dataset.connId) { + clearSelection(); + selectedWireId = wire.dataset.connId; + renderWires(); + e.stopPropagation(); + return; + } + + const nodeEl = e.target.closest('.lg-node'); + if (nodeEl) { + clearSelection(); + selectedNodeId = nodeEl.dataset.id; + nodeEl.classList.add('selected'); + + isDraggingNode = nodeEl.dataset.id; + const rect = nodeEl.getBoundingClientRect(); + dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; + return; + } + + clearSelection(); + }); + + window.addEventListener('mousemove', (e) => { + const wsRect = workspace.getBoundingClientRect(); + + if (isDraggingNode) { + const node = nodes[isDraggingNode]; + let newX = e.clientX - wsRect.left - dragOffset.x; + let newY = e.clientY - wsRect.top - dragOffset.y; + node.x = Math.max(10, Math.min(newX, wsRect.width - 80)); + node.y = Math.max(20, Math.min(newY, wsRect.height - 60)); + updateNodePositions(); + } + + if (wiringStart) { + tempWirePath = { + x: e.clientX - wsRect.left, + y: e.clientY - wsRect.top + }; + renderWires(); + } + }); + + window.addEventListener('mouseup', (e) => { + isDraggingNode = null; + + if (wiringStart) { + const port = e.target.closest('.lg-port'); + if (port && port.dataset.port.startsWith('in')) { + const targetNodeId = port.closest('.lg-node').dataset.id; + const targetPortId = port.dataset.port.replace('in', ''); + + if (targetNodeId !== wiringStart.node) { + connections = connections.filter(c => !(c.toNode === targetNodeId && c.toPort === targetPortId)); + + connections.push({ + id: `conn_${nextWireId++}`, + fromNode: wiringStart.node, + fromPort: 'out', + toNode: targetNodeId, + toPort: targetPortId + }); + } + } + wiringStart = null; + tempWirePath = null; + runSimulation(); + } + }); + + /* --- Deletion --- */ + window.addEventListener('keydown', (e) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + if (selectedWireId) { + connections = connections.filter(c => c.id !== selectedWireId); + clearSelection(); + runSimulation(); + } + else if (selectedNodeId) { + connections = connections.filter(c => c.fromNode !== selectedNodeId && c.toNode !== selectedNodeId); + if (nodes[selectedNodeId] && nodes[selectedNodeId].el) { + workspace.removeChild(nodes[selectedNodeId].el); + } + delete nodes[selectedNodeId]; + clearSelection(); + runSimulation(); + } + } + }); + + /* --- Drag and Drop --- */ + workspace.addEventListener('dragover', (e) => { e.preventDefault(); }); + workspace.addEventListener('drop', (e) => { + e.preventDefault(); + const spawnType = e.dataTransfer.getData('spawnType'); + if (spawnType) { + const gateType = e.dataTransfer.getData('gateType'); + const wsRect = workspace.getBoundingClientRect(); + const x = e.clientX - wsRect.left - 40; + const y = e.clientY - wsRect.top - 30; + spawnNode(spawnType, gateType || null, x, y); + } + }); + + /* --- Init --- */ + btnClearBoard?.addEventListener('click', () => { + workspace.querySelectorAll('.lg-node').forEach(el => el.remove()); + nodes = {}; + connections = []; + inputCount = 0; + outputCount = 0; + runSimulation(); + }); + + toolboxToggle?.addEventListener("click", () => { + const isCollapsed = logicPage?.classList.contains("toolboxCollapsed"); + logicPage.classList.toggle("toolboxCollapsed", !isCollapsed); + toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false"); + setTimeout(renderWires, 450); + }); + + initToolbox(); + spawnNode('INPUT', null, 80, 150); + spawnNode('INPUT', null, 80, 250); + spawnNode('GATE', 'AND', 320, 200); + spawnNode('OUTPUT', null, 600, 200); + +})(); \ No newline at end of file diff --git a/src/src/assets/astro.svg b/src/src/assets/astro.svg deleted file mode 100644 index 8cf8fb0..0000000 --- a/src/src/assets/astro.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/src/assets/background.svg b/src/src/assets/background.svg deleted file mode 100644 index 4b2be0a..0000000 --- a/src/src/assets/background.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/src/components/Footer.astro b/src/src/components/Footer.astro deleted file mode 100644 index 2dbcdbd..0000000 --- a/src/src/components/Footer.astro +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
Computer Science Concept Simulators
-
- © 2025 Computing:Box · Created with 💗 by Mr Lyall
- Powered by ADCM Networks -
-
-
- - diff --git a/src/src/components/Header.astro b/src/src/components/Header.astro deleted file mode 100644 index 6c42d5c..0000000 --- a/src/src/components/Header.astro +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/src/src/components/Welcome.astro b/src/src/components/Welcome.astro deleted file mode 100644 index 52e0333..0000000 --- a/src/src/components/Welcome.astro +++ /dev/null @@ -1,210 +0,0 @@ ---- -import astroLogo from '../assets/astro.svg'; -import background from '../assets/background.svg'; ---- - - - - diff --git a/src/src/components/simulators/HexSimulator.astro b/src/src/components/simulators/HexSimulator.astro deleted file mode 100644 index 4f212bb..0000000 --- a/src/src/components/simulators/HexSimulator.astro +++ /dev/null @@ -1,104 +0,0 @@ ---- -import "./hex/hex-simulator.css"; ---- - -
-
-
-
DENARY
-
0
- -
HEXADECIMAL
-
00
- -
BINARY
-
0000 0000
-
- -
- -
-
- - - - - - - - - -
-
Custom
- - - -
-
- -
- - -
-
-
- - -
diff --git a/src/src/components/simulators/hex/hex-simulator.css b/src/src/components/simulators/hex/hex-simulator.css deleted file mode 100644 index 8a15d61..0000000 --- a/src/src/components/simulators/hex/hex-simulator.css +++ /dev/null @@ -1,346 +0,0 @@ -/* ================= Fonts to match Binary ================= */ -/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */ -@font-face { - font-family: "DSEG7Classic"; - src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"), - url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); - font-weight: 400; - font-style: normal; - font-display: swap; -} -@font-face { - font-family: "SevenSegment"; - src: url("/fonts/Seven-Segment.woff2") format("woff2"), - url("/fonts/Seven-Segment.woff") format("woff"); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -.hex-sim { - min-height: 100vh; - background: #14151c; - color: #e7e8ee; - padding: 28px; -} - -.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; } -.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; } - -.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; } - -.hex-readout { text-align: center; } -.hex-label { - font-family: "SevenSegment", ui-sans-serif, system-ui; - font-size: 12px; - letter-spacing: 2px; - opacity: 0.7; -} -.hex-mt { margin-top: 12px; } - -.hex-number { - font-family: "DSEG7Classic", ui-monospace, monospace; - font-size: 76px; - line-height: 1; - font-weight: 400; - color: #46ff8a; - text-shadow: 0 0 18px rgba(70,255,138,0.18); -} -.hex-number--small { font-size: 64px; } -.hex-number--tiny { font-size: 54px; letter-spacing: 6px; } - -.hex-divider { - margin: 26px auto 18px; - height: 1px; - width: min(760px, 90%); - background: rgba(255,255,255,0.10); -} - -/* ================= Main digit columns ================= */ -.hex-digits { - margin-top: 18px; - display: flex; - justify-content: center; - gap: 18px; - flex-wrap: wrap; -} - -.hex-digit-col { - width: 160px; - border-radius: 18px; - background: rgba(255,255,255,0.03); - border: 1px solid rgba(255,255,255,0.10); - padding: 12px; - display: grid; - gap: 10px; - justify-items: center; -} - -.hex-digit-controls { - width: 100%; - display: flex; - justify-content: center; - gap: 10px; -} - -.hex-digit-char { - font-size: 64px; - line-height: 1; - color: #46ff8a; - text-shadow: 0 0 18px rgba(70,255,138,0.18); -} - -.hex-digit-place { - font-family: "SevenSegment", ui-monospace, monospace; - opacity: 0.65; - font-size: 14px; - letter-spacing: 1px; -} - -/* ================= Bulbs (brightness changes) ================= */ -.hex-bulbs { - width: 100%; - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 10px; - align-items: end; -} - -.hex-bulb { - display: grid; - justify-items: center; - gap: 6px; - opacity: 0.35; - filter: grayscale(30%); - transition: opacity 160ms ease, filter 160ms ease; -} - -.hex-bulb .hex-bulb-cap { - width: 18px; - height: 18px; - border-radius: 999px; - background: rgba(255,255,255,0.22); - border: 1px solid rgba(255,255,255,0.14); -} - -.hex-bulb .hex-bulb-glow { - width: 18px; - height: 10px; - border-radius: 999px; - background: rgba(70,255,138,0.0); - box-shadow: 0 0 0 rgba(70,255,138,0.0); - transition: background 160ms ease, box-shadow 160ms ease; -} - -.hex-bulb .hex-bulb-label { - font-family: "SevenSegment", ui-monospace, monospace; - font-size: 12px; - opacity: 0.8; -} - -.hex-bulb.is-on { - opacity: 1; - filter: none; -} -.hex-bulb.is-on .hex-bulb-cap { - background: rgba(255,255,255,0.35); -} -.hex-bulb.is-on .hex-bulb-glow { - background: rgba(70,255,138,0.25); - box-shadow: 0 0 18px rgba(70,255,138,0.35); -} - -/* ================= Buttons (toolbox style reused everywhere) ================= */ -.hex-btn { - padding: 10px 12px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,0.14); - background: rgba(255,255,255,0.06); - color: #e7e8ee; - font-weight: 800; - cursor: pointer; - font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; -} -.hex-btn:hover { background: rgba(255,255,255,0.10); } - -.hex-btn--square { - width: 48px; - height: 48px; - padding: 0; - display: grid; - place-items: center; - font-size: 18px; -} - -.hex-btn--wide { width: 100%; } - -.hex-btn--green { - background: rgba(46, 200, 120, 0.18); - border-color: rgba(46,200,120,0.35); -} -.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); } - -.hex-btn--green2 { - background: rgba(46, 200, 120, 0.18); - border-color: rgba(46,200,120,0.35); -} - -.hex-btn--red { - background: rgba(220, 60, 70, 0.18); - border-color: rgba(220,60,70,0.35); -} - -/* Random = green pulse while running */ -.hex-btn--random.is-running { - border-color: rgba(80, 255, 160, 0.55); - background: rgba(46, 200, 120, 0.22); - box-shadow: 0 0 18px rgba(80, 255, 160, 0.35); - animation: hexPulseGreen 900ms ease-in-out infinite; -} -@keyframes hexPulseGreen { - 0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); } - 50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); } -} - -/* Reset = red background + pulse on hover */ -.hex-btn--reset:hover { - background: rgba(220, 60, 70, 0.28); - border-color: rgba(255, 80, 90, 0.55); - animation: hexPulseRed 900ms ease-in-out infinite; -} -@keyframes hexPulseRed { - 0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); } - 50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); } -} - -/* ================= Toolbox button + panel (slide) ================= */ -.hex-toolbox-btn { - position: fixed; - top: 88px; - right: 28px; - z-index: 30; - display: inline-flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,0.14); - background: rgba(255,255,255,0.06); - color: #e7e8ee; - font-weight: 800; - letter-spacing: 1px; - cursor: pointer; -} - -.hex-toolbox-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - color: #ff4fa6; - filter: drop-shadow(0 0 10px rgba(255,79,166,0.35)); -} - -.hex-toolbox { - position: fixed; - top: 140px; - right: 28px; - width: 340px; - display: grid; - gap: 14px; - z-index: 25; - - transform: translateX(0); - opacity: 1; - transition: transform 220ms ease, opacity 220ms ease; -} -.hex-toolbox:not(.is-open) { - transform: translateX(380px); - opacity: 0; - pointer-events: none; -} - -.hex-panel { - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(255,255,255,0.10); - padding: 14px; -} -.hex-panel-title { - font-family: "SevenSegment", ui-sans-serif, system-ui; - font-size: 12px; - letter-spacing: 2px; - opacity: 0.7; - margin-bottom: 10px; -} - -.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; } - -.hex-width { - display: grid; - grid-template-columns: 48px 1fr 48px; - gap: 10px; - align-items: center; -} -.hex-width-readout { - border-radius: 14px; - background: rgba(0,0,0,0.22); - border: 1px solid rgba(255,255,255,0.10); - padding: 10px 12px; - display: flex; - justify-content: space-between; - align-items: baseline; -} -.hex-width-label { - font-family: "SevenSegment", ui-sans-serif, system-ui; - opacity: 0.7; - font-weight: 800; - letter-spacing: 1px; - font-size: 12px; -} -.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; } - -.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; } - -.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } -.hex-mt-sm { margin-top: 10px; } - -.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; } -.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; } - -/* ================= Dialog ================= */ -.hex-dialog { border: none; padding: 0; background: transparent; } -.hex-dialog::backdrop { background: rgba(0,0,0,0.55); } - -.hex-dialog-card { - width: min(560px, 92vw); - border-radius: 18px; - background: #1a1b24; - border: 1px solid rgba(255,255,255,0.12); - padding: 16px; - color: #e7e8ee; -} - -.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; } - -.hex-dialog-input { - width: 100%; - padding: 12px 12px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,0.14); - background: rgba(0,0,0,0.25); - color: #e7e8ee; - font-size: 18px; -} - -.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; } -.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; } -.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; } - -@media (max-width: 900px) { - .hex-toolbox { width: min(360px, 92vw); right: 16px; } - .hex-toolbox-btn { right: 16px; } - .hex-number { font-size: 60px; } - .hex-number--tiny { font-size: 40px; letter-spacing: 4px; } -} diff --git a/src/src/components/simulators/hex/hex-simulator.ts b/src/src/components/simulators/hex/hex-simulator.ts deleted file mode 100644 index 08bc8aa..0000000 --- a/src/src/components/simulators/hex/hex-simulator.ts +++ /dev/null @@ -1,232 +0,0 @@ -type DialogMode = "hex" | "den" | "bin"; - -const root = document.querySelector("[data-hex-sim]"); -if (!root) throw new Error("Hex simulator root not found"); - -const outDen = root.querySelector('[data-out="denary"]')!; -const outHex = root.querySelector('[data-out="hex"]')!; -const outBin = root.querySelector('[data-out="bin"]')!; -const outDigitsRow = root.querySelector('[data-out="digitsRow"]')!; - -const toolbox = root.querySelector('[data-out="toolbox"]')!; -const toolboxBtn = root.querySelector('[data-action="toggleToolbox"]')!; -const digitsCount = root.querySelector('[data-out="digitsCount"]')!; -const bitsHint = root.querySelector('[data-out="bitsHint"]')!; -const randomBtn = root.querySelector("[data-random]")!; - -const dialog = root.querySelector('[data-out="dialog"]')!; -const dialogTitle = root.querySelector('[data-out="dialogTitle"]')!; -const dialogInput = root.querySelector('[data-out="dialogInput"]')!; -const dialogHint = root.querySelector('[data-out="dialogHint"]')!; -const dialogError = root.querySelector('[data-out="dialogError"]')!; - -let digits = 2; // 1..8 -let value = 0; // unsigned denary -let randomTimer: number | null = null; -let dialogMode: DialogMode | null = null; - -const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n)); -const maxForDigits = (d: number) => (16 ** d) - 1; - -const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0"); -const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0"); -const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim(); - -function stopRandom(): void { - if (randomTimer !== null) window.clearInterval(randomTimer); - randomTimer = null; - randomBtn.classList.remove("is-running"); -} - -function startRandom(): void { - stopRandom(); - const max = maxForDigits(digits); - const start = Date.now(); - - randomBtn.classList.add("is-running"); - - randomTimer = window.setInterval(() => { - value = Math.floor(Math.random() * (max + 1)); - render(); - if (Date.now() - start > 1600) stopRandom(); - }, 90); -} - -function render(): void { - const bits = digits * 4; - - digitsCount.textContent = String(digits); - bitsHint.textContent = `= ${bits} bits`; - - outDen.textContent = String(value); - outHex.textContent = padHex(value, digits); - outBin.textContent = groupBin(padBin(value, bits)); - - renderDigitsRow(); -} - -function renderDigitsRow(): void { - const hex = padHex(value, digits); - outDigitsRow.innerHTML = ""; - - for (let i = 0; i < digits; i++) { - const pow = digits - 1 - i; - const placeValue = 16 ** pow; - - const digitChar = hex[i]; - const digitVal = parseInt(digitChar, 16); - const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1 - - const col = document.createElement("div"); - col.className = "hex-digit-col"; - col.innerHTML = ` -
- - -
- -
${digitChar}
- - -
- ${[8,4,2,1].map((w, idx) => { - const on = nibbleBits[idx] === 1; - return ` -
-
-
-
${w}
-
- `; - }).join("")} -
- -
${placeValue}
- `; - outDigitsRow.appendChild(col); - } -} - -function openDialog(mode: DialogMode): void { - stopRandom(); - dialogMode = mode; - - dialogError.textContent = ""; - dialogInput.value = ""; - - if (mode === "hex") { - dialogTitle.textContent = "Custom Hexadecimal"; - dialogHint.textContent = `Enter 1–${digits} hex digit(s) (0–9, A–F).`; - dialogInput.placeholder = "A1"; - dialogInput.inputMode = "text"; - } else if (mode === "den") { - dialogTitle.textContent = "Custom Denary"; - dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`; - dialogInput.placeholder = "42"; - dialogInput.inputMode = "numeric"; - } else { - dialogTitle.textContent = "Custom Binary"; - dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`; - dialogInput.placeholder = "00101010"; - dialogInput.inputMode = "text"; - } - - dialog.showModal(); - window.setTimeout(() => dialogInput.focus(), 0); -} - -function closeDialog(): void { - dialogMode = null; - dialogError.textContent = ""; - if (dialog.open) dialog.close(); -} - -function applyDialog(): void { - const raw = (dialogInput.value || "").trim(); - if (!dialogMode) return closeDialog(); - if (raw.length === 0) return closeDialog(); - - const max = maxForDigits(digits); - const bits = digits * 4; - - if (dialogMode === "hex") { - const v = raw.toUpperCase(); - if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; } - if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; } - value = clamp(parseInt(v, 16), 0, max); - render(); - return closeDialog(); - } - - if (dialogMode === "den") { - if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; } - const n = Number(raw); - if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; } - value = clamp(n, 0, max); - render(); - return closeDialog(); - } - - // bin - if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; } - if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; } - value = clamp(parseInt(raw, 2), 0, max); - render(); - return closeDialog(); -} - -function applyDigitDelta(i: number, delta: number): void { - stopRandom(); - const hexArr = padHex(value, digits).split(""); - let v = parseInt(hexArr[i], 16); - v = (v + delta) % 16; - if (v < 0) v += 16; - hexArr[i] = v.toString(16).toUpperCase(); - value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits)); - render(); -} - -// dialog cancel / backdrop -dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); }); -dialog.addEventListener("click", (e) => { - const card = dialog.querySelector(".hex-dialog-card"); - if (card && !card.contains(e.target as Node)) closeDialog(); -}); -dialogInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") applyDialog(); - if (e.key === "Escape") closeDialog(); -}); - -// main click handler -root.addEventListener("click", (e) => { - const btn = (e.target as HTMLElement).closest("[data-action]"); - if (!btn) return; - const action = btn.getAttribute("data-action")!; - - if (action === "toggleToolbox") { - toolbox.classList.toggle("is-open"); - toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false"); - return; - } - - if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); } - if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); } - - if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); } - if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); } - - if (action === "reset") { stopRandom(); value = 0; return render(); } - if (action === "random") { return startRandom(); } - - if (action === "customHex") return openDialog("hex"); - if (action === "customDenary") return openDialog("den"); - if (action === "customBinary") return openDialog("bin"); - - if (action === "dialogCancel") return closeDialog(); - if (action === "dialogApply") return applyDialog(); - - if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1); - if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1); -}); - -render(); diff --git a/src/src/layouts/BaseLayout.astro b/src/src/layouts/BaseLayout.astro deleted file mode 100644 index 022fd45..0000000 --- a/src/src/layouts/BaseLayout.astro +++ /dev/null @@ -1,118 +0,0 @@ ---- -const { title = "Computing:Box" } = Astro.props; ---- - - - - - - - {title} - - - - - - - -
- -
- - diff --git a/src/src/layouts/Layout.astro b/src/src/layouts/Layout.astro deleted file mode 100644 index e455c61..0000000 --- a/src/src/layouts/Layout.astro +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - Astro Basics - - - - - - - diff --git a/src/src/pages/binary.astro b/src/src/pages/binary.astro deleted file mode 100644 index fc195e5..0000000 --- a/src/src/pages/binary.astro +++ /dev/null @@ -1,115 +0,0 @@ ---- -import BaseLayout from "../layouts/BaseLayout.astro"; -import "../styles/binary.css"; ---- - - -
- - - -
- -
-
-
Denary
-
0
- -
Binary
- -
00000000
-
- -
- -
-
-
-
- - - -
-
- - -
diff --git a/src/src/pages/hexadecimal.astro b/src/src/pages/hexadecimal.astro deleted file mode 100644 index edfe098..0000000 --- a/src/src/pages/hexadecimal.astro +++ /dev/null @@ -1,8 +0,0 @@ ---- -import BaseLayout from "../layouts/BaseLayout.astro"; -import HexSimulator from "../components/simulators/HexSimulator.astro"; ---- - - - - diff --git a/src/src/pages/index.astro b/src/src/pages/index.astro deleted file mode 100644 index c04f360..0000000 --- a/src/src/pages/index.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import Welcome from '../components/Welcome.astro'; -import Layout from '../layouts/Layout.astro'; - -// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build -// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. ---- - - - - diff --git a/src/src/scripts/binary.js b/src/src/scripts/binary.js deleted file mode 100644 index 41c1c50..0000000 --- a/src/src/scripts/binary.js +++ /dev/null @@ -1,522 +0,0 @@ -// src/scripts/binary.js -// Computing:Box — Binary page logic (Unsigned + Two's Complement) - -(() => { - /* ----------------------------- - DOM - ----------------------------- */ - const bitsGrid = document.getElementById("bitsGrid"); - const denaryEl = document.getElementById("denaryNumber"); - const binaryEl = document.getElementById("binaryNumber"); - const bitsInput = document.getElementById("bitsInput"); - - const modeToggle = document.getElementById("modeToggle"); - const modeHint = document.getElementById("modeHint"); - - const btnCustomBinary = document.getElementById("btnCustomBinary"); - const btnCustomDenary = document.getElementById("btnCustomDenary"); - const btnShiftLeft = document.getElementById("btnShiftLeft"); - const btnShiftRight = document.getElementById("btnShiftRight"); - - const btnDec = document.getElementById("btnDec"); - const btnInc = document.getElementById("btnInc"); - const btnClear = document.getElementById("btnClear"); - const btnRandom = document.getElementById("btnRandom"); - - const btnBitsUp = document.getElementById("btnBitsUp"); - const btnBitsDown = document.getElementById("btnBitsDown"); - - const toolboxToggle = document.getElementById("toolboxToggle"); - const toolboxPanel = document.getElementById("toolboxPanel"); - - /* ----------------------------- - STATE - ----------------------------- */ - let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); - let bits = new Array(bitCount).fill(false); - let randomTimer = null; - - // For responsive wrapping of the top binary display - let nibblesPerLine = null; - let wrapMeasureSpan = null; - - /* ----------------------------- - HELPERS - ----------------------------- */ - function clampInt(n, min, max) { - if (!Number.isFinite(n)) return min; - return Math.max(min, Math.min(max, Math.trunc(n))); - } - - function isTwosMode() { - return !!modeToggle?.checked; - } - - function pow2Big(n) { - return 1n << BigInt(n); - } - - function unsignedMaxExclusive(nBits) { - return pow2Big(nBits); - } - - function unsignedMaxValue(nBits) { - return pow2Big(nBits) - 1n; - } - - function twosMin(nBits) { - return -pow2Big(nBits - 1); - } - - function twosMax(nBits) { - return pow2Big(nBits - 1) - 1n; - } - - function bitsToUnsignedBigInt() { - let v = 0n; - for (let i = 0; i < bitCount; i++) { - if (bits[i]) v += pow2Big(i); - } - return v; - } - - function unsignedBigIntToBits(vUnsigned) { - const span = unsignedMaxExclusive(bitCount); - const v = ((vUnsigned % span) + span) % span; - - for (let i = 0; i < bitCount; i++) { - bits[i] = ((v >> BigInt(i)) & 1n) === 1n; - } - } - - function bitsToSignedBigIntTwos() { - const u = bitsToUnsignedBigInt(); - const signBit = bits[bitCount - 1] === true; - if (!signBit) return u; - return u - pow2Big(bitCount); - } - - function signedBigIntToBitsTwos(vSigned) { - const span = pow2Big(bitCount); - let v = ((vSigned % span) + span) % span; - unsignedBigIntToBits(v); - } - - function updateModeHint() { - if (!modeHint) return; - modeHint.textContent = isTwosMode() - ? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value." - : "Tip: In unsigned binary, all bits represent positive values."; - } - - /* ----------------------------- - TOP BINARY DISPLAY: responsive wrap by nibble count - ----------------------------- */ - function ensureWrapMeasurer() { - if (wrapMeasureSpan || !binaryEl) return; - wrapMeasureSpan = document.createElement("span"); - wrapMeasureSpan.style.position = "absolute"; - wrapMeasureSpan.style.visibility = "hidden"; - wrapMeasureSpan.style.whiteSpace = "pre"; - wrapMeasureSpan.style.pointerEvents = "none"; - // Inherit font/letterspacing from binaryEl - wrapMeasureSpan.style.font = getComputedStyle(binaryEl).font; - wrapMeasureSpan.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing; - document.body.appendChild(wrapMeasureSpan); - } - - function computeNibblesPerLine() { - if (!binaryEl) return null; - ensureWrapMeasurer(); - - // Available width = width of the readout area (binaryEl parent) - const host = binaryEl.parentElement; - if (!host) return null; - - const hostW = host.getBoundingClientRect().width; - if (!Number.isFinite(hostW) || hostW <= 0) return null; - - // Measure one nibble including trailing space ("0000 ") - wrapMeasureSpan.textContent = "0000 "; - const nibbleW = wrapMeasureSpan.getBoundingClientRect().width || 1; - - // Safety: keep at least 1 nibble per line - const max = Math.max(1, Math.floor(hostW / nibbleW)); - return max; - } - - function formatBinaryWrapped() { - // EXACT bitCount digits (no padding to 4) - let raw = ""; - for (let i = bitCount - 1; i >= 0; i--) raw += bits[i] ? "1" : "0"; - - // If <= 4 bits, do NOT insert spaces/newlines at all - if (bitCount <= 4) return raw; - - const groups = []; - for (let i = 0; i < raw.length; i += 4) { - groups.push(raw.slice(i, i + 4)); - } - - const perLine = nibblesPerLine ?? groups.length; - if (perLine >= groups.length) return groups.join(" "); - - const lines = []; - for (let i = 0; i < groups.length; i += perLine) { - lines.push(groups.slice(i, i + perLine).join(" ")); - } - return lines.join("\n"); - } - - function refreshBinaryWrap() { - const next = computeNibblesPerLine(); - // Only update if it actually changes (prevents jitter) - if (next !== nibblesPerLine) nibblesPerLine = next; - updateReadout(); // re-render with new wrap - } - - /* ----------------------------- - BUILD UI (BITS) - ----------------------------- */ - function buildBits(count) { - bitCount = clampInt(count, 1, 64); - if (bitsInput) bitsInput.value = String(bitCount); - - const oldBits = bits.slice(); - bits = new Array(bitCount).fill(false); - for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; - - bitsGrid.innerHTML = ""; - - bitsGrid.classList.toggle("bitsFew", bitCount < 8); - if (bitCount < 8) { - bitsGrid.style.setProperty("--cols", String(bitCount)); - } else { - bitsGrid.style.removeProperty("--cols"); - } - - for (let i = bitCount - 1; i >= 0; i--) { - const bitEl = document.createElement("div"); - bitEl.className = "bit"; - - bitEl.innerHTML = ` - -
- - `; - - bitsGrid.appendChild(bitEl); - } - - bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => { - input.addEventListener("change", () => { - const i = Number(input.dataset.index); - bits[i] = input.checked; - updateUI(); - }); - }); - - // bulb styling + 25% bigger (vs 26px previously) - for (let i = 0; i < bitCount; i++) { - const bulb = document.getElementById(`bulb-${i}`); - if (!bulb) continue; - bulb.style.width = "auto"; - bulb.style.height = "auto"; - bulb.style.border = "none"; - bulb.style.background = "transparent"; - bulb.style.borderRadius = "0"; - bulb.style.boxShadow = "none"; - bulb.style.opacity = "0.45"; - bulb.style.fontSize = "32px"; - bulb.style.lineHeight = "1"; - bulb.style.display = "flex"; - bulb.style.alignItems = "center"; - bulb.style.justifyContent = "center"; - bulb.style.filter = "grayscale(1)"; - bulb.textContent = "💡"; - } - - // wrapping may change when bit width changes - refreshBinaryWrap(); - updateUI(); - } - - /* ----------------------------- - UI UPDATE - ----------------------------- */ - function updateBitLabels() { - for (let i = 0; i < bitCount; i++) { - const label = document.getElementById(`bitLabel-${i}`); - if (!label) continue; - - if (isTwosMode() && i === bitCount - 1) { - // Keep on one line (CSS: white-space:nowrap) - label.textContent = `-${pow2Big(bitCount - 1).toString()}`; - } else { - label.textContent = pow2Big(i).toString(); - } - } - } - - function syncSwitchesToBits() { - bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => { - const i = Number(input.dataset.index); - input.checked = !!bits[i]; - }); - } - - function updateBulbs() { - for (let i = 0; i < bitCount; i++) { - const bulb = document.getElementById(`bulb-${i}`); - if (!bulb) continue; - - const on = bits[i] === true; - if (on) { - bulb.style.opacity = "1"; - bulb.style.filter = "grayscale(0)"; - bulb.style.textShadow = "0 0 18px rgba(255,216,107,.75), 0 0 30px rgba(255,216,107,.45)"; - } else { - bulb.style.opacity = "0.45"; - bulb.style.filter = "grayscale(1)"; - bulb.style.textShadow = "none"; - } - } - } - - function updateReadout() { - if (!denaryEl || !binaryEl) return; - - denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString(); - binaryEl.textContent = formatBinaryWrapped(); - } - - function updateUI() { - updateModeHint(); - updateBitLabels(); - syncSwitchesToBits(); - updateBulbs(); - updateReadout(); - } - - /* ----------------------------- - SET FROM INPUT - ----------------------------- */ - function setFromBinaryString(binStr) { - const clean = String(binStr ?? "").replace(/\s+/g, ""); - if (!/^[01]+$/.test(clean)) return false; - - const padded = clean.slice(-bitCount).padStart(bitCount, "0"); - for (let i = 0; i < bitCount; i++) { - const charFromRight = padded[padded.length - 1 - i]; - bits[i] = charFromRight === "1"; - } - - updateUI(); - return true; - } - - function setFromDenaryInput(vStr) { - const raw = String(vStr ?? "").trim(); - if (!raw) return false; - - let v; - try { - if (!/^-?\d+$/.test(raw)) return false; - v = BigInt(raw); - } catch { - return false; - } - - if (isTwosMode()) { - const min = twosMin(bitCount); - const max = twosMax(bitCount); - if (v < min || v > max) return false; - signedBigIntToBitsTwos(v); - } else { - if (v < 0n) return false; - if (v > unsignedMaxValue(bitCount)) return false; - unsignedBigIntToBits(v); - } - - updateUI(); - return true; - } - - /* ----------------------------- - SHIFTS - ----------------------------- */ - function shiftLeft() { - for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1]; - bits[0] = false; - updateUI(); - } - - function shiftRight() { - if (isTwosMode()) { - // arithmetic right shift: keep MSB - const msb = bits[bitCount - 1]; - for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1]; - bits[bitCount - 1] = msb; - } else { - for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1]; - bits[bitCount - 1] = false; - } - updateUI(); - } - - /* ----------------------------- - CLEAR / INC / DEC - ----------------------------- */ - function clearAll() { - bits.fill(false); - updateUI(); - } - - function increment() { - if (isTwosMode()) { - const min = twosMin(bitCount); - const max = twosMax(bitCount); - let v = bitsToSignedBigIntTwos() + 1n; - if (v > max) v = min; - signedBigIntToBitsTwos(v); - } else { - const span = unsignedMaxExclusive(bitCount); - unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span); - } - updateUI(); - } - - function decrement() { - if (isTwosMode()) { - const min = twosMin(bitCount); - const max = twosMax(bitCount); - let v = bitsToSignedBigIntTwos() - 1n; - if (v < min) v = max; - signedBigIntToBitsTwos(v); - } else { - const span = unsignedMaxExclusive(bitCount); - unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span); - } - updateUI(); - } - - /* ----------------------------- - RANDOM - ----------------------------- */ - function cryptoRandomBigInt(maxExclusive) { - if (maxExclusive <= 0n) return 0n; - const bitLen = maxExclusive.toString(2).length; - const byteLen = Math.ceil(bitLen / 8); - - while (true) { - const bytes = new Uint8Array(byteLen); - crypto.getRandomValues(bytes); - - let x = 0n; - for (const b of bytes) x = (x << 8n) | BigInt(b); - - const extraBits = BigInt(byteLen * 8 - bitLen); - if (extraBits > 0n) x = x >> extraBits; - if (x < maxExclusive) return x; - } - } - - function setRandomOnce() { - const span = unsignedMaxExclusive(bitCount); - const u = cryptoRandomBigInt(span); - unsignedBigIntToBits(u); - updateUI(); - } - - function runRandomBriefly() { - if (randomTimer) { - clearInterval(randomTimer); - randomTimer = null; - } - - const start = Date.now(); - const durationMs = 1125; // (your “~25% longer” vs 900ms) - const tickMs = 80; - - randomTimer = setInterval(() => { - setRandomOnce(); - if (Date.now() - start >= durationMs) { - clearInterval(randomTimer); - randomTimer = null; - } - }, tickMs); - } - - /* ----------------------------- - BIT WIDTH CONTROLS - ----------------------------- */ - function setBitWidth(n) { - buildBits(clampInt(n, 1, 64)); - } - - /* ----------------------------- - TOOLBOX TOGGLE (simple open/close state) - ----------------------------- */ - function setToolboxOpen(open) { - document.body.classList.toggle("toolboxClosed", !open); - toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false"); - refreshBinaryWrap(); // width changes when toolbox closes/opens - } - - /* ----------------------------- - EVENTS - ----------------------------- */ - modeToggle?.addEventListener("change", () => updateUI()); - - btnCustomBinary?.addEventListener("click", () => { - const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`); - if (v === null) return; - if (!setFromBinaryString(v)) alert("Invalid binary"); - }); - - btnCustomDenary?.addEventListener("click", () => { - const v = prompt( - isTwosMode() - ? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):` - : `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):` - ); - if (v === null) return; - if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width"); - }); - - btnShiftLeft?.addEventListener("click", shiftLeft); - btnShiftRight?.addEventListener("click", shiftRight); - - btnInc?.addEventListener("click", increment); - btnDec?.addEventListener("click", decrement); - - btnClear?.addEventListener("click", clearAll); - btnRandom?.addEventListener("click", runRandomBriefly); - - btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1)); - btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1)); - - bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value))); - - toolboxToggle?.addEventListener("click", () => { - const isOpen = !document.body.classList.contains("toolboxClosed"); - setToolboxOpen(!isOpen); - }); - - // Recompute wrapping live when the window size changes - let resizeT = null; - window.addEventListener("resize", () => { - if (resizeT) clearTimeout(resizeT); - resizeT = setTimeout(() => refreshBinaryWrap(), 60); - }); - - /* ----------------------------- - INIT - ----------------------------- */ - updateModeHint(); - buildBits(bitCount); - setToolboxOpen(true); -})(); diff --git a/src/src/styles/binary.css b/src/src/styles/binary.css deleted file mode 100644 index 4398c01..0000000 --- a/src/src/styles/binary.css +++ /dev/null @@ -1,342 +0,0 @@ -/* - Binary page styles (keeps the last-working simulator markup + binary.js). - - Goals: - - Do NOT change any IDs/classes expected by src/scripts/binary.js - - Toolbox button toggles the ENTIRE right-hand column via body.toolboxClosed - - Fix toolbox button positioning (no overlap, consistent with header container) - - Fix spacing/consistency of cards + buttons - - Keep binary readout wrapping/bit-width behaviour from JS (\n in output) -*/ - -:root{ - --panel-w: 360px; - --gap: 22px; -} - -/* Page wrapper (inside BaseLayout .pageWrap) */ -.wrap{ - max-width: 1400px; - margin: 0 auto; - padding: 22px 20px 48px; - display: flex; - flex-direction: column; - gap: 14px; -} - -/* Toolbox toggle button (sits below navbar, aligned right, never overlaps) */ -.toolboxToggle{ - align-self: flex-end; - position: sticky; - top: calc(var(--nav-h, 108px) + 14px); - z-index: 30; - - display: inline-flex; - align-items: center; - gap: 10px; - height: 40px; - padding: 0 14px; - border-radius: 12px; - border: 1px solid rgba(255,255,255,.14); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.92); - cursor: pointer; -} -.toolboxToggle:hover{ background: rgba(255,255,255,.08); } -.toolboxText{ - letter-spacing: .12em; - font-weight: 900; -} - -/* Main layout grid */ -.topGrid{ - display: grid; - grid-template-columns: 1fr var(--panel-w); - gap: var(--gap); - align-items: start; -} - -/* Hide ENTIRE toolbox column when toggled closed */ -body.toolboxClosed .topGrid{ grid-template-columns: 1fr; } -body.toolboxClosed #toolboxPanel{ display: none; } - -.mainCol{ min-width: 0; } - -/* Readout */ -.readout{ - text-align: center; - margin-top: 8px; -} - -.label{ - opacity: .8; - letter-spacing: .12em; - text-transform: uppercase; - font-size: 12px; -} - -/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */ -.num{ - display: inline-block; - width: fit-content; - max-width: 100%; - white-space: pre-line; /* allows JS \n wraps */ - letter-spacing: 2px; -} - -.denaryValue{ - font-size: 54px; - margin: 6px 0 10px; -} - -.binaryValue{ - font-size: 56px; - margin: 4px 0 18px; -} - -.divider{ - height: 1px; - background: rgba(255,255,255,.10); - margin: 14px auto 24px; - max-width: 900px; -} - -/* Bits area */ -.bitsWrap{ padding-top: 6px; } - -.bitsGrid{ - display: grid; - gap: 24px; - justify-content: center; - grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); - max-width: 1200px; - margin: 0 auto; -} - -.bitsGrid.bitsFew{ justify-content: center; } - -.bit{ - display: grid; - justify-items: center; - gap: 8px; -} - -.bulb{ - font-size: 32px; /* JS also bumps this */ - line-height: 1; - opacity: .45; -} - -.bitVal{ - font-size: 22px; - line-height: 1.05; - text-align: center; - white-space: nowrap; /* keep -128 on one line */ -} - -/* Switch (existing classes assumed) */ -.switch{ - position: relative; - display: inline-block; - width: 52px; - height: 28px; -} -.switch input{ display:none; } -.slider{ - position:absolute; - inset:0; - border-radius:999px; - background: rgba(255,255,255,.18); - border: 1px solid rgba(255,255,255,.14); -} -.slider:before{ - content:""; - position:absolute; - height: 22px; - width: 22px; - left: 3px; - top: 2.5px; - border-radius: 999px; - background: #fff; - transition: transform .18s ease; -} -.switch input:checked + .slider:before{ transform: translateX(22px); } - -/* Toolbox column */ -.panelCol{ - position: sticky; - top: calc(var(--nav-h, 108px) + 72px); /* leaves space for sticky toolbox button */ - align-self: start; - display: grid; - gap: 16px; -} - -/* Cards */ -.card{ - border: 1px solid rgba(255,255,255,.12); - border-radius: 16px; - background: rgba(255,255,255,.05); - padding: 14px; -} - -.cardTitle{ - opacity: .8; - letter-spacing: .14em; - text-transform: uppercase; - font-size: 12px; - margin-bottom: 10px; -} - -.hint{ - opacity: .7; - font-size: 11px; - margin-top: 10px; - line-height: 1.35; -} - -/* Keep mode labels on one line */ -.toggleRow{ - display: grid; - grid-template-columns: 1fr auto 1fr; - gap: 10px; - align-items: center; -} - -.toggleLabel{ - font-size: 12px; - font-weight: 800; - letter-spacing: .12em; - text-transform: uppercase; - white-space: nowrap; -} - -.subCard{ - margin-top: 12px; - border: 1px solid rgba(255,255,255,.10); - border-radius: 14px; - background: rgba(0,0,0,.12); - padding: 12px; -} - -.subTitle{ - opacity: .8; - letter-spacing: .14em; - text-transform: uppercase; - font-size: 11px; - margin-bottom: 10px; -} - -.bitWidthRow{ - display: grid; - grid-template-columns: 44px 1fr 44px; - gap: 10px; - align-items: center; -} - -.bitInputWrap{ - display: grid; - grid-template-columns: auto 1fr; - gap: 10px; - align-items: center; - padding: 10px 12px; - border: 1px solid rgba(255,255,255,.10); - border-radius: 12px; - background: rgba(255,255,255,.04); -} - -.bitInputLabel{ - opacity: .75; - letter-spacing: .14em; - text-transform: uppercase; - font-size: 11px; - white-space: nowrap; -} - -.bitInput{ - width: 100%; - min-width: 0; - background: transparent; - border: none; - outline: none; - color: inherit; - font-size: 20px; - text-align: right; -} - -.miniBtn{ - height: 44px; - border-radius: 12px; - border: 1px solid rgba(255,255,255,.12); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.9); - font-size: 18px; - cursor: pointer; -} -.miniBtn:hover{ background: rgba(255,255,255,.08); } - -/* Buttons */ -.controlsRow{ - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-bottom: 10px; -} - -.btn{ - border-radius: 12px; - border: 1px solid rgba(255,255,255,.12); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.92); - padding: 12px 12px; - font-weight: 800; - letter-spacing: .10em; - text-transform: uppercase; - cursor: pointer; -} -.btn:hover{ background: rgba(255,255,255,.08); } - -.btnWide{ width: 100%; } - -.btnAccent{ - background: rgba(0,255,140,.12); - border-color: rgba(0,255,140,.22); -} -.btnAccent:hover{ background: rgba(0,255,140,.16); } - -.toolRowCentered{ - display: flex; - justify-content: center; - gap: 12px; - margin: 10px 0 12px; -} - -.toolBtn{ - width: 56px; - height: 56px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,.12); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.92); - font-size: 18px; - cursor: pointer; -} - -.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); } -.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); } - -.toolRow2{ - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-bottom: 12px; -} - -/* Reset stays white text */ -.btnReset{ color: rgba(255,255,255,.92); } - -/* Responsive */ -@media (max-width: 980px){ - .topGrid{ grid-template-columns: 1fr; } - .panelCol{ position: static; } - .toolboxToggle{ position: static; align-self: flex-start; } -} diff --git a/src/src/styles/global.css b/src/src/styles/global.css deleted file mode 100644 index a71bbdf..0000000 --- a/src/src/styles/global.css +++ /dev/null @@ -1,85 +0,0 @@ -:root{ - --bg: #1f2027; - --panel: #22242d; - --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); -} - -*{ box-sizing:border-box; } - -body{ - margin:0; - font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - background: var(--bg); - color: var(--text); -} - -.siteHeader{ - position: sticky; - top: 0; - z-index: 10; - background: rgba(0,0,0,.15); - backdrop-filter: blur(8px); - border-bottom: 1px solid rgba(255,255,255,.06); -} - -.siteHeaderInner{ - max-width: 1200px; - margin: 0 auto; - padding: 14px 20px; - display:flex; - align-items:center; - justify-content:space-between; - gap: 16px; -} - -.brand{ - color: var(--text); - text-decoration:none; - font-weight: 900; - letter-spacing:.02em; -} - -.nav{ - display:flex; - gap: 14px; - flex-wrap:wrap; - justify-content:flex-end; -} -.nav a{ - color: var(--muted); - text-decoration:none; - font-weight: 700; - font-size: 14px; -} -.nav a:hover{ color: var(--text); } - -.siteMain{ - min-height: calc(100vh - 140px); -} - -.siteFooter{ - border-top: 1px solid rgba(255,255,255,.08); - margin-top: 32px; - background: rgba(0,0,0,.10); -} - -.siteFooterInner{ - max-width: 1200px; - margin: 0 auto; - padding: 18px 20px 26px; - color: var(--muted); - font-size: 12px; - line-height: 1.6; -} - -.footerTitle{ - color: var(--text); - opacity:.9; - font-weight: 800; - margin-bottom: 6px; -} diff --git a/src/src/styles/site.css b/src/src/styles/site.css deleted file mode 100644 index bcaa838..0000000 --- a/src/src/styles/site.css +++ /dev/null @@ -1,75 +0,0 @@ -:root{ - --bg: #1f2027; - --panel: rgba(255,255,255,.04); - --panel-border: rgba(255,255,255,.10); - --text: #e8e8ee; - --muted: #a9acb8; - --accent: #33ff7a; - --accent-dim: rgba(51,255,122,.15); - --line: rgba(255,255,255,.12); -} - -*{ box-sizing: border-box; } - -body{ - margin:0; - font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - background: var(--bg); - color: var(--text); -} - -.site-header{ - border-bottom: 1px solid rgba(255,255,255,.08); - background: rgba(0,0,0,.12); -} - -.site-header__inner{ - max-width: 1200px; - margin: 0 auto; - padding: 14px 20px; - display:flex; - align-items:center; - justify-content:space-between; - gap: 18px; -} - -.brand{ - font-weight: 800; - letter-spacing: .02em; -} - -.nav{ - display:flex; - gap: 14px; - flex-wrap: wrap; - justify-content:flex-end; -} - -.nav__link{ - color: var(--muted); - text-decoration:none; - font-weight: 700; - font-size: 13px; -} -.nav__link:hover{ color: var(--text); } - -.site-main{ - max-width: 1200px; - margin: 0 auto; - padding: 28px 20px 40px; - min-height: calc(100vh - 140px); -} - -.site-footer{ - border-top: 1px solid rgba(255,255,255,.08); - background: rgba(0,0,0,.10); -} - -.site-footer__inner{ - max-width: 1200px; - margin: 0 auto; - padding: 16px 20px; - color: var(--muted); - font-size: 12px; - line-height: 1.5; -} diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..46400b8 --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,52 @@ +/* src/styles/base.css */ +@import "./md3-tokens.css"; +html, body{ height:100%; } +body{ + margin:0; + font-family: var(--font-sans); + background: var(--md-surface-2); + color: var(--md-on-surface); +} +a{ color: var(--md-primary); text-decoration: none; } +a:hover{ text-decoration: underline; } +.container{ + max-width: 1100px; + margin: 0 auto; + padding: 16px; +} +.card{ + background: var(--md-surface); + border: 1px solid var(--md-outline); + border-radius: var(--radius-2); + box-shadow: var(--shadow-1); + padding: 16px; +} +.btn{ + display:inline-flex; + gap:8px; + align-items:center; + justify-content:center; + border-radius: 999px; + border: 1px solid var(--md-outline); + background: var(--md-surface); + color: var(--md-on-surface); + padding: 10px 14px; + font-weight: 600; + cursor: pointer; +} +.btn:hover{ filter: brightness(0.98); } +.btn:focus{ outline:none; box-shadow: var(--md-focus); } +.btn-primary{ + background: var(--md-primary); + color: var(--md-on-primary); + border-color: transparent; +} +.badge{ + display:inline-block; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + border: 1px solid var(--md-outline); + background: var(--md-surface-2); +} +code, pre{ font-family: var(--font-mono); } diff --git a/src/styles/binary.css b/src/styles/binary.css deleted file mode 100644 index 6655a70..0000000 --- a/src/styles/binary.css +++ /dev/null @@ -1,326 +0,0 @@ -/* Binary page styles (moved OUT of binary.astro) */ - -:root{ - --panel-w: 360px; - --gap: 22px; -} - -.wrap{ - max-width: 1400px; - margin: 0 auto; - padding: 26px 20px 48px; - position: relative; -} - -.topGrid{ - display: grid; - grid-template-columns: 1fr var(--panel-w); - gap: var(--gap); - align-items: start; -} - -/* When toolbox is hidden, reclaim space + centre content */ -body.toolboxClosed .topGrid{ - grid-template-columns: 1fr; -} -body.toolboxClosed #toolboxPanel{ - display: none; -} - -.mainCol{ - min-width: 0; -} - -.readout{ - text-align: center; - margin-top: 8px; -} - -.label{ - opacity: .8; - letter-spacing: .12em; - text-transform: uppercase; - font-size: 12px; -} - -/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */ -.num{ - display: inline-block; - width: fit-content; - max-width: 100%; - white-space: pre-line; /* allows JS \n wraps */ - letter-spacing: 2px; -} - -.denaryValue{ - font-size: 54px; - margin: 6px 0 10px; -} - -.binaryValue{ - font-size: 56px; - margin: 4px 0 18px; -} - -.divider{ - height: 1px; - background: rgba(255,255,255,.10); - margin: 14px auto 24px; - max-width: 900px; -} - -.bitsWrap{ - padding-top: 6px; -} - -.bitsGrid{ - display: grid; - gap: 24px; - justify-content: center; -} - -/* Default: a single row of bits (will wrap automatically as bit count grows) */ -.bitsGrid{ - grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); - max-width: 1200px; - margin: 0 auto; -} - -.bitsGrid.bitsFew{ - justify-content: center; -} - -/* Bit tile */ -.bit{ - display: grid; - justify-items: center; - gap: 8px; -} - -.bulb{ - font-size: 32px; /* JS also bumps this */ - line-height: 1; - opacity: .45; -} - -.bitVal{ - font-size: 22px; - line-height: 1.05; - text-align: center; - white-space: nowrap; /* keep -128 on one line */ -} - -/* Switch (existing classes assumed) */ -.switch{ - position: relative; - display: inline-block; - width: 52px; - height: 28px; -} -.switch input{ display:none; } -.slider{ - position:absolute; - inset:0; - border-radius:999px; - background: rgba(255,255,255,.18); - border: 1px solid rgba(255,255,255,.14); -} -.slider:before{ - content:""; - position:absolute; - height: 22px; - width: 22px; - left: 3px; - top: 2.5px; - border-radius: 999px; - background: #fff; - transition: transform .18s ease; -} -.switch input:checked + .slider:before{ - transform: translateX(22px); -} - -/* Toolbox toggle button */ -.toolboxToggle{ - position: absolute; - right: 20px; - top: 18px; - z-index: 20; - display: inline-flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - border-radius: 12px; - border: 1px solid rgba(255,255,255,.14); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.92); - cursor: pointer; -} -.toolboxText{ - letter-spacing: .12em; - font-weight: 900; -} - -/* Toolbox panel */ -.panelCol{ - position: sticky; - top: calc(var(--nav-h, 72px) + 18px); - align-self: start; - display: grid; - gap: 16px; -} - -/* Cards */ -.card{ - border: 1px solid rgba(255,255,255,.12); - border-radius: 16px; - background: rgba(255,255,255,.05); - padding: 14px; -} -.cardTitle{ - opacity: .8; - letter-spacing: .14em; - text-transform: uppercase; - font-size: 12px; - margin-bottom: 10px; -} - -.hint{ - opacity: .7; - font-size: 11px; - margin-top: 10px; - line-height: 1.35; -} - -/* Keep mode labels on one line */ -.toggleRow{ - display: grid; - grid-template-columns: 1fr auto 1fr; - gap: 10px; - align-items: center; -} -.toggleLabel{ - font-size: 12px; - font-weight: 800; - letter-spacing: .12em; - text-transform: uppercase; - white-space: nowrap; -} - -.subCard{ - margin-top: 12px; - border: 1px solid rgba(255,255,255,.10); - border-radius: 14px; - background: rgba(0,0,0,.12); - padding: 12px; -} -.subTitle{ - opacity: .8; - letter-spacing: .14em; - text-transform: uppercase; - font-size: 11px; - margin-bottom: 10px; -} - -.bitWidthRow{ - display: grid; - grid-template-columns: 44px 1fr 44px; - gap: 10px; - align-items: center; -} - -.bitInputWrap{ - display: grid; - grid-template-columns: auto 1fr; - gap: 10px; - align-items: center; - padding: 10px 12px; - border: 1px solid rgba(255,255,255,.10); - border-radius: 12px; - background: rgba(255,255,255,.04); -} -.bitInputLabel{ - opacity: .75; - letter-spacing: .14em; - text-transform: uppercase; - font-size: 11px; - white-space: nowrap; -} -.bitInput{ - width: 100%; - min-width: 0; - background: transparent; - border: none; - outline: none; - color: inherit; - font-size: 20px; - text-align: right; -} - -.miniBtn{ - height: 44px; - border-radius: 12px; - border: 1px solid rgba(255,255,255,.12); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.9); - font-size: 18px; - cursor: pointer; -} - -/* Buttons */ -.controlsRow{ - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-bottom: 10px; -} - -.btn{ - border-radius: 12px; - border: 1px solid rgba(255,255,255,.12); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.92); - padding: 12px 12px; - font-weight: 800; - letter-spacing: .10em; - text-transform: uppercase; - cursor: pointer; -} -.btnWide{ width: 100%; } - -.btnAccent{ - background: rgba(0,255,140,.12); - border-color: rgba(0,255,140,.22); -} - -.toolRowCentered{ - display: flex; - justify-content: center; - gap: 12px; - margin: 10px 0 12px; -} - -.toolBtn{ - width: 56px; - height: 56px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,.12); - background: rgba(255,255,255,.06); - color: rgba(255,255,255,.92); - font-size: 18px; - cursor: pointer; -} -.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); } -.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); } - -.toolRow2{ - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-bottom: 12px; -} - -/* Reset stays white text */ -.btnReset{ - color: rgba(255,255,255,.92); -} diff --git a/src/styles/fonts.css b/src/styles/fonts.css new file mode 100644 index 0000000..768cb39 --- /dev/null +++ b/src/styles/fonts.css @@ -0,0 +1,11 @@ +@font-face { + font-family: "DSEG7"; + src: url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +.dseg { + font-family: "DSEG7", monospace; + letter-spacing: 0.15em; +} diff --git a/src/styles/global.css b/src/styles/global.css index a71bbdf..0a6b418 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,85 +1,190 @@ -:root{ +/* Global fonts */ +@font-face { + font-family: "SevenSegment"; + src: url("/fonts/Seven-Segment.woff2") format("woff2"), + url("/fonts/Seven-Segment.woff") format("woff"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "DSEG7Classic"; + src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"), + url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +:root { + --nav-h: 92px; --bg: #1f2027; - --panel: #22242d; - --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); + --line: rgba(255,255,255,.10); + + --ui-font: "Inter", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + --num-font: "DSEG7Classic", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + --bit-font: "SevenSegment", monospace; } -*{ box-sizing:border-box; } +* { box-sizing: border-box; } +html, body { height: 100%; } +body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--ui-font); display: flex; flex-direction: column; } -body{ - margin:0; - font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - background: var(--bg); - color: var(--text); +/* --- BASE LAYOUT --- */ +.siteNav { position: sticky; top: 0; z-index: 50; height: var(--nav-h); background: rgba(0,0,0,.10); border-bottom: 1px solid var(--line); backdrop-filter: blur(8px); } +.navInner { height: 100%; max-width: 1400px; margin: 0 auto; padding: 0 20px; display: flex; align-items: center; justify-content: space-between; gap: 24px; } +.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--text); } +.brandLogo { width: 2.5em; height: 2.5em; image-rendering: pixelated; } +.brandName { letter-spacing: .12em; font-weight: 900; font-size: 18px; } +.navLinks { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; } +.navLinks a { color: var(--muted); text-decoration: none; font-weight: 800; letter-spacing: .12em; font-size: 16px; } +.navLinks a:hover, .navLinks a.active { color: #e8e8ee; } + +.pageWrap { flex: 1; max-width: 1400px; margin: 0 auto; padding: 0 20px 40px; width: 100%; display: flex; flex-direction: column; } +.siteFooter { border-top: 1px solid var(--line); background: rgba(0,0,0,.08); } +.footerInner { max-width: 1400px; margin: 0 auto; padding: 18px 20px; color: var(--muted); font-size: 12px; letter-spacing: .08em; display: flex; flex-direction: column; gap: 6px; } + +/* --- APP LAYOUT --- */ +.binaryPage { + --toolbox-w: 360px; + --toolbox-gap: 22px; + --toolbox-toggle-top: calc(var(--nav-h) + 16px); + --toolbox-top: calc(var(--toolbox-toggle-top) + 60px); + position: relative; padding-top: 16px; flex: 1; display: flex; flex-direction: column; +} +.binaryPage:not(.toolboxCollapsed) { padding-right: calc(var(--toolbox-w) + var(--toolbox-gap)); } +.binaryPage.toolboxCollapsed { padding-right: 0; } +.topGrid { display: flex; align-items: stretch; gap: 28px; flex: 1; } +.leftCol { flex: 1 1 auto; min-width: 0; container-type: inline-size; display: flex; flex-direction: column; } + +/* --- READOUT FORMATTING --- */ +.readoutContainer { display: flex; align-items: center; justify-content: center; gap: 64px; width: 100%; } +.readout { display: flex; flex-direction: column; align-items: center; gap: 16px; padding-top: 4px; } +.readoutBlock { display: flex; flex-direction: column; align-items: center; gap: 8px; } +.label { font-family: var(--bit-font); letter-spacing: .14em; text-transform: uppercase; font-size: 18px; opacity: .75; margin: 0; } +.num { font-family: var(--num-font); color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); letter-spacing: 2px; } + +.denaryValue, .hexValue, .binaryValue { display: flex; gap: 16px; justify-content: center; white-space: pre-wrap; text-align: center; margin: 0; line-height: 1; } +.denaryValue { font-size: 56px; } +.hexValue { font-size: 48px; } +.binaryValue { font-size: 40px; } +.divider { height: 1px; background: rgba(255,255,255,.08); margin: 16px 0 16px; } + +/* --- GRIDS & BITS --- */ +.bitsWrap { width: 100%; } +.bitsGrid { --cols: 8; display: grid; grid-template-columns: repeat(var(--cols), minmax(92px, 1fr)); gap: 26px 22px; align-items: start; justify-items: center; } +.bitsGrid.bitsFew { justify-content: center; } +.bit { width: 100%; max-width: 140px; display: flex; flex-direction: column; align-items: center; gap: 8px; container-type: inline-size; } +.bitVal { font-family: var(--bit-font); font-size: min(32px, calc(140cqw / var(--len, 1))); letter-spacing: 2px; color: rgba(232,232,238,.85); white-space: nowrap; line-height: 1; } + +.bulb { width: 44px; height: 44px; color: rgba(255,255,255,.15); margin-bottom: 8px; flex-shrink: 0; transition: 0.2s ease; background: transparent; display: flex; align-items: center; justify-content: center; } +.bulb svg { width: 100%; height: 100%; display: block; } +.bulb.on { color: #ffd86b !important; filter: drop-shadow(0 0 14px rgba(255, 216, 107, 1)) !important; } +.bulb.on svg { fill: #ffd86b !important; } + +.switch { position: relative; width: 56px; height: 28px; display: inline-block; } +.switch input { display: none; } +.slider { position: absolute; inset: 0; background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.14); border-radius: 999px; transition: .2s ease; } +.slider::before { content: ""; position: absolute; width: 22px; height: 22px; left: 3px; top: 2px; background: rgba(255,255,255,.92); border-radius: 999px; transition: .2s ease; pointer-events: none; } +.switch input:checked + .slider { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.30); } +.switch input:checked + .slider::before { transform: translateX(28px); } + +/* --- HEXADECIMAL --- */ +.hexGrid { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), minmax(160px, 1fr)); gap: 32px 20px; align-items: start; justify-items: center; width: 100%; } +.hexGrid.bitsFew { justify-content: center; } +.hexCol { display: flex; flex-direction: column; align-items: center; width: 100%; } + +/* --- HEX COLOURS SPECIFIC --- */ +.colorGroupWrap { display: flex; flex-wrap: nowrap; gap: 16px; justify-content: center; width: 100%; } +.colorGroup { display: flex; gap: 12px; padding: 12px; background: rgba(255,255,255,.02); border-radius: 20px; border: 1px solid rgba(255,255,255,.05); flex: 0 1 auto; min-width: 0; } +.colorPreviewSide { display: flex; gap: 24px; align-items: center; justify-content: center; } +.colorBoxWrap { display: flex; flex-direction: column; align-items: center; gap: 8px; } +.colorBox { width: 60px; height: 60px; border-radius: 12px; border: 2px solid rgba(255,255,255,.15); box-shadow: 0 4px 16px rgba(0,0,0,.4); background-color: #000000; transition: background-color 0.2s ease; } +.colorBoxLabel { font-family: var(--ui-font); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); } + +.text-red { color: #ff5555 !important; text-shadow: 0 0 18px rgba(255,85,85,.35) !important; } +.text-green { color: #28f07a !important; text-shadow: 0 0 18px rgba(40,240,122,.35) !important; } +.text-blue { color: #55aaff !important; text-shadow: 0 0 18px rgba(85,170,255,.35) !important; } + +/* HEX CARD */ +.hexCard { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 190px; flex: 0 1 auto; min-width: 0; box-shadow: 0 4px 24px rgba(0,0,0,.2); backdrop-filter: blur(10px); } +.hexCardButtons { display: flex; gap: 10px; } +.hexCardBtn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; color: rgba(232,232,238,.92); transition: all 0.2s ease; } +.hexCardBtn.inc { background: rgba(40,240,122,.15); border-color: rgba(40,240,122,.25); } +.hexCardBtn.inc:hover { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.4); } +.hexCardBtn.dec { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.25); } +.hexCardBtn.dec:hover { background: rgba(255,80,80,.28); border-color: rgba(255,80,80,.4); } +.hexDigitDisplay { font-family: var(--num-font); font-size: 48px; color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); line-height: 1; } +.hexNibbleRow { display: flex; gap: 10px; justify-content: center; width: 100%; } +.hexNibbleBit { display: flex; flex-direction: column; align-items: center; gap: 6px; } +.hexNibbleBulb { width: 32px !important; height: 32px !important; margin-bottom: 2px !important; } +.hexNibbleLabel { font-family: var(--bit-font); font-size: 24px; color: rgba(232,232,238,.6); } +.hexColWeight { font-family: var(--bit-font); font-size: 40px; color: rgba(232,232,238,.6); margin-top: 14px; } + + +/* --- TOOLBOX --- */ +.toolboxToggle { position: fixed; top: var(--toolbox-toggle-top); right: 22px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); font-weight: 800; font-size: 16px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; } +.toolboxIcon { font-size: 20px; filter: drop-shadow(0 0 8px rgba(255,105,180,.35)); } +.toolboxToggle:hover { border-color: rgba(255,255,255,.22); } +.panelCol { position: fixed; top: var(--toolbox-top); right: 22px; width: var(--toolbox-w); z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); opacity: 1; transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; } +.binaryPage.toolboxCollapsed .panelCol { transform: translateX(calc(var(--toolbox-w) + 32px)); opacity: 0; pointer-events: none; } +.card { background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.10); border-radius: 16px; padding: 16px; backdrop-filter: blur(10px); } +.cardTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 18px; color: rgba(232,232,238,.9); margin-bottom: 12px; } +.hint { font-family: var(--bit-font); font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: rgba(232,232,238,.55); margin-top: 10px; line-height: 1.35; } +.toggleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.toggleLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; color: var(--muted); transition: color 0.2s, text-shadow 0.2s; } +.toggleLabel.activeMode { color: #28f07a; text-shadow: 0 0 12px rgba(40,240,122,.45); } +.subCard { margin-top: 12px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.10); background: rgba(0,0,0,.12); } +.subTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 16px; margin-bottom: 10px; opacity: .85; } +.bitWidthRow { display: flex; align-items: center; gap: 10px; } +.miniBtn { width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.9); font-family: var(--bit-font); font-weight: 900; font-size: 22px; cursor: pointer; } +.miniBtn:hover { border-color: rgba(255,255,255,.22); } +.bitInputWrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.04); padding: 10px 12px; } +.bitInputLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; } +.bitInput { width: 70px; text-align: right; font-family: var(--num-font); font-size: 28px; letter-spacing: 2px; color: #28f07a; background: transparent; border: none; outline: none; } +.btn { border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); border-radius: 12px; padding: 10px 12px; font-family: var(--bit-font); font-size: 14px; letter-spacing: .12em; text-transform: uppercase; font-weight: 900; cursor: pointer; } +.btn:hover { border-color: rgba(255,255,255,.22); } +.btnAccent { background: rgba(40,240,122,.12); border-color: rgba(40,240,122,.22); } +.btnAccent:hover { border-color: rgba(40,240,122,.35); } +.btnHalf { width: calc(50% - 6px); } +.btnWide { width: 100%; } +.controlsRow { display: flex; gap: 12px; margin-bottom: 12px; } +.toolRowCentered { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; } +.toolBtn { width: 46px; height: 46px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; } +.toolDec { background: rgba(255,80,80,.20); border-color: rgba(255,80,80,.25); } +.toolInc { background: rgba(40,240,122,.18); border-color: rgba(40,240,122,.25); } +.btnReset { color: rgba(232,232,238,.95); } +.btnReset:hover { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.35); } + +/* === CONTAINER QUERIES === */ +@container (max-width: 1050px) { + .readoutContainer { gap: 40px; } + .colorGroupWrap { gap: 10px; } + .colorGroup { padding: 10px; gap: 8px; border-radius: 16px; } + .hexCard { padding: 12px 8px; width: 140px; gap: 12px; } + .hexDigitDisplay { font-size: 40px; } + .hexNibbleBulb { width: 24px !important; height: 24px !important; } + .hexNibbleLabel { font-size: 20px; } + .hexColWeight { font-size: 26px; margin-top: 10px; } + .hexCardBtn { width: 34px; height: 34px; font-size: 14px; } } -.siteHeader{ - position: sticky; - top: 0; - z-index: 10; - background: rgba(0,0,0,.15); - backdrop-filter: blur(8px); - border-bottom: 1px solid rgba(255,255,255,.06); +@container (max-width: 800px) { + .readoutContainer { flex-direction: column; gap: 24px; } + .colorPreviewSide { padding-top: 0; } + .colorGroupWrap { gap: 6px; } + .colorGroup { padding: 6px; gap: 6px; border-radius: 12px; } + .hexCard { padding: 8px 4px; width: 90px; gap: 8px; border-radius: 10px; } + .hexDigitDisplay { font-size: 32px; } + .hexNibbleBulb { width: 16px !important; height: 16px !important; } + .hexNibbleLabel { font-size: 16px; } + .hexColWeight { font-size: 20px; margin-top: 6px; } + .hexCardBtn { width: 28px; height: 28px; font-size: 12px; } + .denaryValue, .hexValue, .binaryValue { font-size: 32px; gap: 10px; } } -.siteHeaderInner{ - max-width: 1200px; - margin: 0 auto; - padding: 14px 20px; - display:flex; - align-items:center; - justify-content:space-between; - gap: 16px; -} - -.brand{ - color: var(--text); - text-decoration:none; - font-weight: 900; - letter-spacing:.02em; -} - -.nav{ - display:flex; - gap: 14px; - flex-wrap:wrap; - justify-content:flex-end; -} -.nav a{ - color: var(--muted); - text-decoration:none; - font-weight: 700; - font-size: 14px; -} -.nav a:hover{ color: var(--text); } - -.siteMain{ - min-height: calc(100vh - 140px); -} - -.siteFooter{ - border-top: 1px solid rgba(255,255,255,.08); - margin-top: 32px; - background: rgba(0,0,0,.10); -} - -.siteFooterInner{ - max-width: 1200px; - margin: 0 auto; - padding: 18px 20px 26px; - color: var(--muted); - font-size: 12px; - line-height: 1.6; -} - -.footerTitle{ - color: var(--text); - opacity:.9; - font-weight: 800; - margin-bottom: 6px; -} +@media (max-width: 1100px) { .binaryPage { --toolbox-w: 330px; } .denaryValue { font-size: 48px; } .hexValue { font-size: 40px; } .binaryValue { font-size: 32px; } } +@media (max-width: 900px) { .binaryPage { --toolbox-w: 320px; } .bitsGrid { grid-template-columns: repeat(var(--cols), minmax(84px, 1fr)); } .hexGrid { grid-template-columns: repeat(var(--cols), minmax(130px, 1fr)); } } \ No newline at end of file diff --git a/src/styles/logic-gates.css b/src/styles/logic-gates.css new file mode 100644 index 0000000..36d054d --- /dev/null +++ b/src/styles/logic-gates.css @@ -0,0 +1,163 @@ +/* === LOGIC GATES CANVAS CSS === */ +.lg-workspace { + position: relative; + flex: 1; + width: 100%; + min-height: 750px; + background-color: transparent; + background-image: radial-gradient(rgba(255,255,255,0.12) 1px, transparent 1px); + background-size: 24px 24px; + border: none; + border-radius: 0; + box-shadow: none; + overflow: hidden; +} + +.lg-svg-layer { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +/* Wires */ +.lg-wire { + stroke: rgba(255,255,255,0.25); + stroke-width: 6; + fill: none; + stroke-linecap: round; + transition: stroke 0.1s ease, filter 0.1s ease, stroke-width 0.1s ease; + pointer-events: stroke; /* Allows wires to be clicked */ + cursor: pointer; +} +.lg-wire:hover { + stroke: rgba(255,255,255,0.6); + stroke-width: 10; +} +.lg-wire.active { + stroke: #28f07a; + filter: drop-shadow(0 0 6px rgba(40,240,122,0.6)); +} +.lg-wire.active:hover { stroke: #5dff9e; } +.lg-wire.selected { + stroke: #ff5555 !important; + stroke-width: 8 !important; + stroke-dasharray: 8 8; + filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important; + animation: wireDash 1s linear infinite; +} +@keyframes wireDash { to { stroke-dashoffset: -16; } } + +.lg-wire-temp { + stroke: rgba(255,255,255,0.4); + stroke-dasharray: 8 8; + pointer-events: none; +} + +/* Nodes (Borderless & Transparent) */ +.lg-node { + position: absolute; + background: transparent; + border: none; + border-radius: 0; + padding: 4px; + display: flex; + flex-direction: column; + align-items: center; + cursor: grab; + z-index: 10; + box-shadow: none; + backdrop-filter: none; + user-select: none; + transition: filter 0.2s; +} +.lg-node:active { cursor: grabbing; z-index: 20; } +.lg-node.selected { + filter: drop-shadow(0 0 10px rgba(255,85,85,0.8)); +} + +/* Node Labels (Seven-Segment, +2 Sizes Bigger) */ +.lg-header { + font-size: 24px; + color: var(--muted); + font-family: var(--bit-font); + letter-spacing: 2px; + pointer-events: none; + margin-bottom: 6px; +} + +/* Container mapping SVGs to absolutely positioned connection dots */ +.lg-gate-container { + position: relative; + display: inline-flex; + align-items: center; +} + +.lg-gate-svg { + width: 100px; + height: 50px; + display: block; +} + +.lg-line-svg { + width: 30px; + height: 50px; + display: block; +} + +/* Connection Ports */ +.lg-port { + width: 16px; height: 16px; background: #a9acb8; border-radius: 50%; cursor: crosshair; + border: 3px solid var(--bg); box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s; + position: absolute; z-index: 5; + transform: translate(-50%, -50%); /* Centers the dot exactly over the coordinate */ +} +.lg-port:hover { transform: translate(-50%, -50%) scale(1.3); background: #fff; } +.lg-port.active { background: #28f07a; box-shadow: 0 0 12px rgba(40,240,122,0.8); border-color: #1f2027; } + +/* Draggable Toolbox Visual Gates Grid */ +.tb-icon-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} +.tb-icon-box { + background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); + border-radius: 12px; width: 100%; padding: 12px 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; + cursor: grab; transition: background 0.2s, border-color 0.2s; +} +.tb-icon-box:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); } +.tb-icon-label { font-family: var(--ui-font); font-size: 11px; font-weight: 800; color: var(--text); letter-spacing: 1px; text-transform: uppercase; } + +/* Toolbox Scroll Fix */ +.panelCol { + max-height: calc(100vh - var(--nav-h) - 30px) !important; + overflow-y: auto; + padding-bottom: 30px; + pointer-events: auto; +} + +/* Truth Table */ +.tt-summary { + font-family: var(--ui-font); font-size: 14px; font-weight: 800; + color: var(--accent, #28f07a); cursor: pointer; user-select: none; + outline: none; margin-bottom: 10px; text-transform: uppercase; +} +.tt-table-wrap { + width: 100%; max-height: 300px; overflow-y: auto; overflow-x: auto; + border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2); +} +.tt-table { + width: 100%; border-collapse: collapse; text-align: center; + font-family: var(--num-font); font-size: 14px; color: #e8e8ee; +} +.tt-table th { + position: sticky; top: 0; background: rgba(31,32,39,0.95); + padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.15); + color: var(--muted); font-family: var(--bit-font); font-weight: normal; +} +.tt-table td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); } +.tt-table .tt-on { color: #28f07a; text-shadow: 0 0 8px rgba(40,240,122,0.5); } \ No newline at end of file diff --git a/src/styles/md3-tokens.css b/src/styles/md3-tokens.css new file mode 100644 index 0000000..beb9cf3 --- /dev/null +++ b/src/styles/md3-tokens.css @@ -0,0 +1,43 @@ +/* src/styles/md3-tokens.css */ +/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */ +:root{ + /* Typography */ + --font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + /* Spacing + shape */ + --radius-1: 10px; + --radius-2: 16px; + --radius-3: 22px; + --shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06); + /* Color roles (keep simple) */ + --md-surface: #ffffff; + --md-surface-2: #f6f7fb; + --md-on-surface: #111318; + --md-primary: #2f6fed; /* calm blue */ + --md-on-primary: #ffffff; + --md-secondary: #5a5f72; /* muted */ + --md-on-secondary: #ffffff; + --md-tertiary: #0f766e; /* teal for "practical" tools */ + --md-on-tertiary: #ffffff; + --md-outline: #d7dbe7; + --md-success: #1a7f37; + --md-warning: #b54708; + --md-danger: #b42318; + /* Focus ring for accessibility */ + --md-focus: 0 0 0 3px rgba(47,111,237,.28); +} +@media (prefers-color-scheme: dark){ + :root{ + --md-surface: #0b0e14; + --md-surface-2: #121725; + --md-on-surface: #e8eaf2; + --md-primary: #9bb6ff; + --md-on-primary: #0b0e14; + --md-secondary: #b8bccd; + --md-on-secondary: #0b0e14; + --md-tertiary: #4fd1c5; + --md-on-tertiary: #0b0e14; + --md-outline: #2b3244; + --md-focus: 0 0 0 3px rgba(155,182,255,.25); + } +} From e0e72c17e8563a662f9f4100fbae2ab96b5ea7c9 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Sun, 1 Mar 2026 16:32:27 +0000 Subject: [PATCH 19/23] Fully functional logic gates page Signed-off-by: Alexander Lyall --- src/assets/astro.svg | 1 - src/components/Welcome.astro | 210 ---------- src/components/simulators/HexSimulator.astro | 104 ----- .../simulators/hex/hex-simulator.css | 346 ---------------- .../simulators/hex/hex-simulator.ts | 232 ----------- src/components/site-footer.astro | 8 - src/components/site-header.astro | 25 -- src/components/tools/BinarySimulator.astro | 381 ------------------ src/pages/index.astro | 11 - src/pages/logic-gates.astro | 32 +- src/scripts/logicGates.js | 36 +- src/styles/base.css | 52 --- src/styles/fonts.css | 11 - src/styles/logic-gates.css | 186 ++++++--- src/styles/md3-tokens.css | 43 -- src/styles/site.css | 75 ---- 16 files changed, 168 insertions(+), 1585 deletions(-) delete mode 100644 src/assets/astro.svg delete mode 100644 src/components/Welcome.astro delete mode 100644 src/components/simulators/HexSimulator.astro delete mode 100644 src/components/simulators/hex/hex-simulator.css delete mode 100644 src/components/simulators/hex/hex-simulator.ts delete mode 100644 src/components/site-footer.astro delete mode 100644 src/components/site-header.astro delete mode 100644 src/components/tools/BinarySimulator.astro delete mode 100644 src/pages/index.astro delete mode 100644 src/styles/base.css delete mode 100644 src/styles/fonts.css delete mode 100644 src/styles/md3-tokens.css delete mode 100644 src/styles/site.css diff --git a/src/assets/astro.svg b/src/assets/astro.svg deleted file mode 100644 index 8cf8fb0..0000000 --- a/src/assets/astro.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/Welcome.astro b/src/components/Welcome.astro deleted file mode 100644 index 52e0333..0000000 --- a/src/components/Welcome.astro +++ /dev/null @@ -1,210 +0,0 @@ ---- -import astroLogo from '../assets/astro.svg'; -import background from '../assets/background.svg'; ---- - - - - diff --git a/src/components/simulators/HexSimulator.astro b/src/components/simulators/HexSimulator.astro deleted file mode 100644 index 4f212bb..0000000 --- a/src/components/simulators/HexSimulator.astro +++ /dev/null @@ -1,104 +0,0 @@ ---- -import "./hex/hex-simulator.css"; ---- - -
-
-
-
DENARY
-
0
- -
HEXADECIMAL
-
00
- -
BINARY
-
0000 0000
-
- -
- -
-
- - - - - - - - - -
-
Custom
- - - -
-
- -
- - -
-
-
- - -
diff --git a/src/components/simulators/hex/hex-simulator.css b/src/components/simulators/hex/hex-simulator.css deleted file mode 100644 index 8a15d61..0000000 --- a/src/components/simulators/hex/hex-simulator.css +++ /dev/null @@ -1,346 +0,0 @@ -/* ================= Fonts to match Binary ================= */ -/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */ -@font-face { - font-family: "DSEG7Classic"; - src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"), - url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"); - font-weight: 400; - font-style: normal; - font-display: swap; -} -@font-face { - font-family: "SevenSegment"; - src: url("/fonts/Seven-Segment.woff2") format("woff2"), - url("/fonts/Seven-Segment.woff") format("woff"); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -.hex-sim { - min-height: 100vh; - background: #14151c; - color: #e7e8ee; - padding: 28px; -} - -.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; } -.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; } - -.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; } - -.hex-readout { text-align: center; } -.hex-label { - font-family: "SevenSegment", ui-sans-serif, system-ui; - font-size: 12px; - letter-spacing: 2px; - opacity: 0.7; -} -.hex-mt { margin-top: 12px; } - -.hex-number { - font-family: "DSEG7Classic", ui-monospace, monospace; - font-size: 76px; - line-height: 1; - font-weight: 400; - color: #46ff8a; - text-shadow: 0 0 18px rgba(70,255,138,0.18); -} -.hex-number--small { font-size: 64px; } -.hex-number--tiny { font-size: 54px; letter-spacing: 6px; } - -.hex-divider { - margin: 26px auto 18px; - height: 1px; - width: min(760px, 90%); - background: rgba(255,255,255,0.10); -} - -/* ================= Main digit columns ================= */ -.hex-digits { - margin-top: 18px; - display: flex; - justify-content: center; - gap: 18px; - flex-wrap: wrap; -} - -.hex-digit-col { - width: 160px; - border-radius: 18px; - background: rgba(255,255,255,0.03); - border: 1px solid rgba(255,255,255,0.10); - padding: 12px; - display: grid; - gap: 10px; - justify-items: center; -} - -.hex-digit-controls { - width: 100%; - display: flex; - justify-content: center; - gap: 10px; -} - -.hex-digit-char { - font-size: 64px; - line-height: 1; - color: #46ff8a; - text-shadow: 0 0 18px rgba(70,255,138,0.18); -} - -.hex-digit-place { - font-family: "SevenSegment", ui-monospace, monospace; - opacity: 0.65; - font-size: 14px; - letter-spacing: 1px; -} - -/* ================= Bulbs (brightness changes) ================= */ -.hex-bulbs { - width: 100%; - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 10px; - align-items: end; -} - -.hex-bulb { - display: grid; - justify-items: center; - gap: 6px; - opacity: 0.35; - filter: grayscale(30%); - transition: opacity 160ms ease, filter 160ms ease; -} - -.hex-bulb .hex-bulb-cap { - width: 18px; - height: 18px; - border-radius: 999px; - background: rgba(255,255,255,0.22); - border: 1px solid rgba(255,255,255,0.14); -} - -.hex-bulb .hex-bulb-glow { - width: 18px; - height: 10px; - border-radius: 999px; - background: rgba(70,255,138,0.0); - box-shadow: 0 0 0 rgba(70,255,138,0.0); - transition: background 160ms ease, box-shadow 160ms ease; -} - -.hex-bulb .hex-bulb-label { - font-family: "SevenSegment", ui-monospace, monospace; - font-size: 12px; - opacity: 0.8; -} - -.hex-bulb.is-on { - opacity: 1; - filter: none; -} -.hex-bulb.is-on .hex-bulb-cap { - background: rgba(255,255,255,0.35); -} -.hex-bulb.is-on .hex-bulb-glow { - background: rgba(70,255,138,0.25); - box-shadow: 0 0 18px rgba(70,255,138,0.35); -} - -/* ================= Buttons (toolbox style reused everywhere) ================= */ -.hex-btn { - padding: 10px 12px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,0.14); - background: rgba(255,255,255,0.06); - color: #e7e8ee; - font-weight: 800; - cursor: pointer; - font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; -} -.hex-btn:hover { background: rgba(255,255,255,0.10); } - -.hex-btn--square { - width: 48px; - height: 48px; - padding: 0; - display: grid; - place-items: center; - font-size: 18px; -} - -.hex-btn--wide { width: 100%; } - -.hex-btn--green { - background: rgba(46, 200, 120, 0.18); - border-color: rgba(46,200,120,0.35); -} -.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); } - -.hex-btn--green2 { - background: rgba(46, 200, 120, 0.18); - border-color: rgba(46,200,120,0.35); -} - -.hex-btn--red { - background: rgba(220, 60, 70, 0.18); - border-color: rgba(220,60,70,0.35); -} - -/* Random = green pulse while running */ -.hex-btn--random.is-running { - border-color: rgba(80, 255, 160, 0.55); - background: rgba(46, 200, 120, 0.22); - box-shadow: 0 0 18px rgba(80, 255, 160, 0.35); - animation: hexPulseGreen 900ms ease-in-out infinite; -} -@keyframes hexPulseGreen { - 0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); } - 50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); } -} - -/* Reset = red background + pulse on hover */ -.hex-btn--reset:hover { - background: rgba(220, 60, 70, 0.28); - border-color: rgba(255, 80, 90, 0.55); - animation: hexPulseRed 900ms ease-in-out infinite; -} -@keyframes hexPulseRed { - 0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); } - 50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); } -} - -/* ================= Toolbox button + panel (slide) ================= */ -.hex-toolbox-btn { - position: fixed; - top: 88px; - right: 28px; - z-index: 30; - display: inline-flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,0.14); - background: rgba(255,255,255,0.06); - color: #e7e8ee; - font-weight: 800; - letter-spacing: 1px; - cursor: pointer; -} - -.hex-toolbox-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - color: #ff4fa6; - filter: drop-shadow(0 0 10px rgba(255,79,166,0.35)); -} - -.hex-toolbox { - position: fixed; - top: 140px; - right: 28px; - width: 340px; - display: grid; - gap: 14px; - z-index: 25; - - transform: translateX(0); - opacity: 1; - transition: transform 220ms ease, opacity 220ms ease; -} -.hex-toolbox:not(.is-open) { - transform: translateX(380px); - opacity: 0; - pointer-events: none; -} - -.hex-panel { - border-radius: 16px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(255,255,255,0.10); - padding: 14px; -} -.hex-panel-title { - font-family: "SevenSegment", ui-sans-serif, system-ui; - font-size: 12px; - letter-spacing: 2px; - opacity: 0.7; - margin-bottom: 10px; -} - -.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; } - -.hex-width { - display: grid; - grid-template-columns: 48px 1fr 48px; - gap: 10px; - align-items: center; -} -.hex-width-readout { - border-radius: 14px; - background: rgba(0,0,0,0.22); - border: 1px solid rgba(255,255,255,0.10); - padding: 10px 12px; - display: flex; - justify-content: space-between; - align-items: baseline; -} -.hex-width-label { - font-family: "SevenSegment", ui-sans-serif, system-ui; - opacity: 0.7; - font-weight: 800; - letter-spacing: 1px; - font-size: 12px; -} -.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; } - -.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; } - -.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } -.hex-mt-sm { margin-top: 10px; } - -.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; } -.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; } - -/* ================= Dialog ================= */ -.hex-dialog { border: none; padding: 0; background: transparent; } -.hex-dialog::backdrop { background: rgba(0,0,0,0.55); } - -.hex-dialog-card { - width: min(560px, 92vw); - border-radius: 18px; - background: #1a1b24; - border: 1px solid rgba(255,255,255,0.12); - padding: 16px; - color: #e7e8ee; -} - -.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; } - -.hex-dialog-input { - width: 100%; - padding: 12px 12px; - border-radius: 14px; - border: 1px solid rgba(255,255,255,0.14); - background: rgba(0,0,0,0.25); - color: #e7e8ee; - font-size: 18px; -} - -.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; } -.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; } -.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; } - -@media (max-width: 900px) { - .hex-toolbox { width: min(360px, 92vw); right: 16px; } - .hex-toolbox-btn { right: 16px; } - .hex-number { font-size: 60px; } - .hex-number--tiny { font-size: 40px; letter-spacing: 4px; } -} diff --git a/src/components/simulators/hex/hex-simulator.ts b/src/components/simulators/hex/hex-simulator.ts deleted file mode 100644 index 08bc8aa..0000000 --- a/src/components/simulators/hex/hex-simulator.ts +++ /dev/null @@ -1,232 +0,0 @@ -type DialogMode = "hex" | "den" | "bin"; - -const root = document.querySelector("[data-hex-sim]"); -if (!root) throw new Error("Hex simulator root not found"); - -const outDen = root.querySelector('[data-out="denary"]')!; -const outHex = root.querySelector('[data-out="hex"]')!; -const outBin = root.querySelector('[data-out="bin"]')!; -const outDigitsRow = root.querySelector('[data-out="digitsRow"]')!; - -const toolbox = root.querySelector('[data-out="toolbox"]')!; -const toolboxBtn = root.querySelector('[data-action="toggleToolbox"]')!; -const digitsCount = root.querySelector('[data-out="digitsCount"]')!; -const bitsHint = root.querySelector('[data-out="bitsHint"]')!; -const randomBtn = root.querySelector("[data-random]")!; - -const dialog = root.querySelector('[data-out="dialog"]')!; -const dialogTitle = root.querySelector('[data-out="dialogTitle"]')!; -const dialogInput = root.querySelector('[data-out="dialogInput"]')!; -const dialogHint = root.querySelector('[data-out="dialogHint"]')!; -const dialogError = root.querySelector('[data-out="dialogError"]')!; - -let digits = 2; // 1..8 -let value = 0; // unsigned denary -let randomTimer: number | null = null; -let dialogMode: DialogMode | null = null; - -const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n)); -const maxForDigits = (d: number) => (16 ** d) - 1; - -const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0"); -const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0"); -const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim(); - -function stopRandom(): void { - if (randomTimer !== null) window.clearInterval(randomTimer); - randomTimer = null; - randomBtn.classList.remove("is-running"); -} - -function startRandom(): void { - stopRandom(); - const max = maxForDigits(digits); - const start = Date.now(); - - randomBtn.classList.add("is-running"); - - randomTimer = window.setInterval(() => { - value = Math.floor(Math.random() * (max + 1)); - render(); - if (Date.now() - start > 1600) stopRandom(); - }, 90); -} - -function render(): void { - const bits = digits * 4; - - digitsCount.textContent = String(digits); - bitsHint.textContent = `= ${bits} bits`; - - outDen.textContent = String(value); - outHex.textContent = padHex(value, digits); - outBin.textContent = groupBin(padBin(value, bits)); - - renderDigitsRow(); -} - -function renderDigitsRow(): void { - const hex = padHex(value, digits); - outDigitsRow.innerHTML = ""; - - for (let i = 0; i < digits; i++) { - const pow = digits - 1 - i; - const placeValue = 16 ** pow; - - const digitChar = hex[i]; - const digitVal = parseInt(digitChar, 16); - const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1 - - const col = document.createElement("div"); - col.className = "hex-digit-col"; - col.innerHTML = ` -
- - -
- -
${digitChar}
- - -
- ${[8,4,2,1].map((w, idx) => { - const on = nibbleBits[idx] === 1; - return ` -
-
-
-
${w}
-
- `; - }).join("")} -
- -
${placeValue}
- `; - outDigitsRow.appendChild(col); - } -} - -function openDialog(mode: DialogMode): void { - stopRandom(); - dialogMode = mode; - - dialogError.textContent = ""; - dialogInput.value = ""; - - if (mode === "hex") { - dialogTitle.textContent = "Custom Hexadecimal"; - dialogHint.textContent = `Enter 1–${digits} hex digit(s) (0–9, A–F).`; - dialogInput.placeholder = "A1"; - dialogInput.inputMode = "text"; - } else if (mode === "den") { - dialogTitle.textContent = "Custom Denary"; - dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`; - dialogInput.placeholder = "42"; - dialogInput.inputMode = "numeric"; - } else { - dialogTitle.textContent = "Custom Binary"; - dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`; - dialogInput.placeholder = "00101010"; - dialogInput.inputMode = "text"; - } - - dialog.showModal(); - window.setTimeout(() => dialogInput.focus(), 0); -} - -function closeDialog(): void { - dialogMode = null; - dialogError.textContent = ""; - if (dialog.open) dialog.close(); -} - -function applyDialog(): void { - const raw = (dialogInput.value || "").trim(); - if (!dialogMode) return closeDialog(); - if (raw.length === 0) return closeDialog(); - - const max = maxForDigits(digits); - const bits = digits * 4; - - if (dialogMode === "hex") { - const v = raw.toUpperCase(); - if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; } - if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; } - value = clamp(parseInt(v, 16), 0, max); - render(); - return closeDialog(); - } - - if (dialogMode === "den") { - if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; } - const n = Number(raw); - if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; } - value = clamp(n, 0, max); - render(); - return closeDialog(); - } - - // bin - if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; } - if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; } - value = clamp(parseInt(raw, 2), 0, max); - render(); - return closeDialog(); -} - -function applyDigitDelta(i: number, delta: number): void { - stopRandom(); - const hexArr = padHex(value, digits).split(""); - let v = parseInt(hexArr[i], 16); - v = (v + delta) % 16; - if (v < 0) v += 16; - hexArr[i] = v.toString(16).toUpperCase(); - value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits)); - render(); -} - -// dialog cancel / backdrop -dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); }); -dialog.addEventListener("click", (e) => { - const card = dialog.querySelector(".hex-dialog-card"); - if (card && !card.contains(e.target as Node)) closeDialog(); -}); -dialogInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") applyDialog(); - if (e.key === "Escape") closeDialog(); -}); - -// main click handler -root.addEventListener("click", (e) => { - const btn = (e.target as HTMLElement).closest("[data-action]"); - if (!btn) return; - const action = btn.getAttribute("data-action")!; - - if (action === "toggleToolbox") { - toolbox.classList.toggle("is-open"); - toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false"); - return; - } - - if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); } - if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); } - - if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); } - if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); } - - if (action === "reset") { stopRandom(); value = 0; return render(); } - if (action === "random") { return startRandom(); } - - if (action === "customHex") return openDialog("hex"); - if (action === "customDenary") return openDialog("den"); - if (action === "customBinary") return openDialog("bin"); - - if (action === "dialogCancel") return closeDialog(); - if (action === "dialogApply") return applyDialog(); - - if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1); - if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1); -}); - -render(); diff --git a/src/components/site-footer.astro b/src/components/site-footer.astro deleted file mode 100644 index bb1625e..0000000 --- a/src/components/site-footer.astro +++ /dev/null @@ -1,8 +0,0 @@ -
- -
diff --git a/src/components/site-header.astro b/src/components/site-header.astro deleted file mode 100644 index 3ce3958..0000000 --- a/src/components/site-header.astro +++ /dev/null @@ -1,25 +0,0 @@ ---- -const nav = [ - { href: "/about", label: "About" }, - { href: "/binary", label: "Binary" }, - { href: "/hexadecimal", label: "Hexadecimal" }, - { href: "/hex-colours", label: "Hex Colours" }, - { href: "/logic-gates", label: "Logic Gates" }, -]; ---- - diff --git a/src/components/tools/BinarySimulator.astro b/src/components/tools/BinarySimulator.astro deleted file mode 100644 index 43d5ce1..0000000 --- a/src/components/tools/BinarySimulator.astro +++ /dev/null @@ -1,381 +0,0 @@ ---- -const { mode = "unsigned", defaultBits = 8 } = Astro.props; - -// For unsigned: min 1 bit, max 16 bits (tweak if you want) -const minBits = 4; -const maxBits = 16; -const initialBits = Math.min(Math.max(defaultBits, minBits), maxBits); ---- - -
-
-
- - -
- -
-
DENARY
-
0
- -
BINARY
-
00000000
-
- -
- - - - - -
-
Bits
-
- - {initialBits} - -
-
-
-
- -
-
- - - - diff --git a/src/pages/index.astro b/src/pages/index.astro deleted file mode 100644 index c04f360..0000000 --- a/src/pages/index.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import Welcome from '../components/Welcome.astro'; -import Layout from '../layouts/Layout.astro'; - -// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build -// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. ---- - - - - diff --git a/src/pages/logic-gates.astro b/src/pages/logic-gates.astro index 7eb4c54..dd4dbad 100644 --- a/src/pages/logic-gates.astro +++ b/src/pages/logic-gates.astro @@ -4,41 +4,31 @@ import "../styles/logic-gates.css"; --- -
+
-
- -
-
-
Interactive Simulator
-
LOGIC GATES
-
-
- Drag items from the toolbox to the board. Drag from output ports to input ports to wire. Click a wire or node and press Delete to remove it. -
+
+
Interactive Logic Circuit Builder
+
+ Drag items from the toolbox to the board. Drag from output ports to input ports to wire. Click a wire or node and press Delete to remove it.
+
-
- -
- -
- -
- -
`; - Object.keys(GATE_SVGS).forEach(gate => { html += `
@@ -65,7 +70,6 @@
`; }); - toolboxGrid.innerHTML = html; document.querySelectorAll('.drag-item').forEach(item => { @@ -76,7 +80,21 @@ }); } - /* --- Math & Geometry --- */ + /* --- Camera Math --- */ + function updateViewport() { + viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; + workspace.style.backgroundSize = `${24 * zoom}px ${24 * zoom}px`; + workspace.style.backgroundPosition = `${panX}px ${panY}px`; + } + + function zoomWorkspace(factor, mouseX, mouseY) { + const newZoom = Math.min(Math.max(0.2, zoom * factor), 3); + panX = mouseX - (mouseX - panX) * (newZoom / zoom); + panY = mouseY - (mouseY - panY) * (newZoom / zoom); + zoom = newZoom; + updateViewport(); + } + function getPortCoords(nodeId, portDataAttr) { const node = nodes[nodeId]; if (!node || !node.el) return {x:0, y:0}; @@ -87,9 +105,10 @@ const wsRect = workspace.getBoundingClientRect(); const portRect = portEl.getBoundingClientRect(); + // Calculate backwards through camera scale/pan to find true local coordinates return { - x: portRect.left - wsRect.left + (portRect.width / 2), - y: portRect.top - wsRect.top + (portRect.height / 2) + x: (portRect.left - wsRect.left - panX + portRect.width / 2) / zoom, + y: (portRect.top - wsRect.top - panY + portRect.height / 2) / zoom }; } @@ -101,37 +120,30 @@ /* --- Rendering --- */ function renderWires() { let svgHTML = ''; - connections.forEach(conn => { const from = getPortCoords(conn.fromNode, 'out'); const to = getPortCoords(conn.toNode, `in${conn.toPort}`); const sourceNode = nodes[conn.fromNode]; const isActive = sourceNode && sourceNode.value === true; const isSelected = conn.id === selectedWireId; - svgHTML += ``; }); if (wiringStart && tempWirePath) { svgHTML += ``; } - wireLayer.innerHTML = svgHTML; } function updateNodePositions() { Object.values(nodes).forEach(n => { - if (n.el) { - n.el.style.left = `${n.x}px`; - n.el.style.top = `${n.y}px`; - } + if (n.el) { n.el.style.left = `${n.x}px`; n.el.style.top = `${n.y}px`; } }); renderWires(); } function clearSelection() { - selectedWireId = null; - selectedNodeId = null; + selectedWireId = null; selectedNodeId = null; document.querySelectorAll('.lg-node.selected').forEach(el => el.classList.remove('selected')); renderWires(); } @@ -139,22 +151,16 @@ /* --- Logic Evaluation --- */ function evaluateGraph(overrideInputs = null) { let context = {}; - Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => { context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value; }); - let changed = true; - let loops = 0; - + let changed = true; let loops = 0; while (changed && loops < 10) { - changed = false; - loops++; - + changed = false; loops++; Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => { let in1Conn = connections.find(c => c.toNode === gate.id && c.toPort === '1'); let in2Conn = connections.find(c => c.toNode === gate.id && c.toPort === '2'); - let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false; let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false; @@ -168,11 +174,7 @@ case 'XOR': res = val1 !== val2; break; case 'XNOR': res = val1 === val2; break; } - - if (context[gate.id] !== res) { - context[gate.id] = res; - changed = true; - } + if (context[gate.id] !== res) { context[gate.id] = res; changed = true; } }); } @@ -193,7 +195,6 @@ } }); } - return outStates; } @@ -205,12 +206,10 @@ const outNodes = Object.values(nodes).filter(n => n.type === 'OUTPUT').sort((a,b) => a.label.localeCompare(b.label)); if (inNodes.length === 0 || outNodes.length === 0) { - ttContainer.innerHTML = '
Add inputs and outputs to generate table.
'; - return; + ttContainer.innerHTML = '
Add inputs and outputs to generate table.
'; return; } if (inNodes.length > 6) { - ttContainer.innerHTML = '
Maximum 6 inputs supported.
'; - return; + ttContainer.innerHTML = '
Maximum 6 inputs supported.
'; return; } let html = ''; @@ -219,27 +218,16 @@ html += ''; const numRows = Math.pow(2, inNodes.length); - for (let i = 0; i < numRows; i++) { let override = {}; - inNodes.forEach((n, idx) => { - override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1; - }); - + inNodes.forEach((n, idx) => { override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1; }); let outStates = evaluateGraph(override); html += ''; - inNodes.forEach(n => { - let val = override[n.id]; - html += ``; - }); - outNodes.forEach(n => { - let val = outStates[n.id]; - html += ``; - }); + inNodes.forEach(n => { let val = override[n.id]; html += ``; }); + outNodes.forEach(n => { let val = outStates[n.id]; html += ``; }); html += ''; } - html += '
${val ? 1 : 0}${val ? 1 : 0}${val ? 1 : 0}${val ? 1 : 0}
'; ttContainer.innerHTML = html; } @@ -252,28 +240,22 @@ /* --- Smart Label Generation --- */ function getNextInputLabel() { - let charCode = 65; // Starts at 'A' - while (Object.values(nodes).some(n => n.type === 'INPUT' && n.label === String.fromCharCode(charCode))) { - charCode++; - } + let charCode = 65; + while (Object.values(nodes).some(n => n.type === 'INPUT' && n.label === String.fromCharCode(charCode))) { charCode++; } return String.fromCharCode(charCode); } function getNextOutputLabel() { let idx = 1; - while (Object.values(nodes).some(n => n.type === 'OUTPUT' && n.label === ('Q' + idx))) { - idx++; - } + while (Object.values(nodes).some(n => n.type === 'OUTPUT' && n.label === ('Q' + idx))) { idx++; } return 'Q' + idx; } /* --- Node Creation --- */ function createNodeElement(node) { const el = document.createElement('div'); - el.className = `lg-node`; - el.dataset.id = node.id; - el.style.left = `${node.x}px`; - el.style.top = `${node.y}px`; + el.className = `lg-node`; el.dataset.id = node.id; + el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; let innerHTML = `
${node.label}
`; @@ -281,12 +263,12 @@ innerHTML += `
${INPUT_SVG} -
+
`; } else if (node.type === 'OUTPUT') { innerHTML += ` -
+
${OUTPUT_SVG}
`; @@ -294,34 +276,33 @@ else if (node.type === 'GATE') { const isNot = node.gateType === 'NOT'; innerHTML += ` -
- ${!isNot ? `
` : ''} +
+ ${!isNot ? `
` : ''} ${GATE_SVGS[node.gateType]} -
+
`; } - innerHTML += `
`; el.innerHTML = innerHTML; - workspace.appendChild(el); + + viewport.appendChild(el); node.el = el; if (node.type === 'INPUT') { el.querySelector('.switch').addEventListener('click', (e) => { const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY); - if (isDraggingNode || dist > 3) { - e.preventDefault(); + if (dist > 3) { + e.preventDefault(); // Prevents toggle if it was a drag motion } else { node.value = !node.value; el.querySelector('.switch').classList.toggle('active-sim', node.value); el.querySelector('.slider').style.background = node.value ? 'rgba(40,240,122,.25)' : ''; el.querySelector('.slider').style.borderColor = node.value ? 'rgba(40,240,122,.30)' : ''; - el.querySelector('.slider').innerHTML = node.value ? `` : ''; + el.querySelector('.slider').innerHTML = node.value ? `` : ''; runSimulation(); } }); } - return el; } @@ -332,7 +313,6 @@ if (type === 'GATE') label = gateType; const id = `node_${nextNodeId++}`; - const offset = Math.floor(Math.random() * 40); const x = dropX !== null ? dropX : (type === 'INPUT' ? 50 : (type === 'OUTPUT' ? 600 : 300) + offset); const y = dropY !== null ? dropY : 150 + offset; @@ -344,10 +324,27 @@ } /* --- Global Interaction Handlers --- */ + + // Camera Zoom Controls + document.getElementById("btnZoomIn")?.addEventListener('click', () => { + const r = workspace.getBoundingClientRect(); zoomWorkspace(1.2, r.width/2, r.height/2); + }); + document.getElementById("btnZoomOut")?.addEventListener('click', () => { + const r = workspace.getBoundingClientRect(); zoomWorkspace(1/1.2, r.width/2, r.height/2); + }); + document.getElementById("btnZoomReset")?.addEventListener('click', () => { + panX = 0; panY = 0; zoom = 1; updateViewport(); + }); + + workspace.addEventListener('wheel', (e) => { + e.preventDefault(); + const wsRect = workspace.getBoundingClientRect(); + const factor = e.deltaY < 0 ? 1.1 : (1/1.1); + zoomWorkspace(factor, e.clientX - wsRect.left, e.clientY - wsRect.top); + }); workspace.addEventListener('mousedown', (e) => { - clickStartX = e.clientX; - clickStartY = e.clientY; + clickStartX = e.clientX; clickStartY = e.clientY; const port = e.target.closest('.lg-port'); if (port) { @@ -356,11 +353,7 @@ if (portId.startsWith('in')) { const existingIdx = connections.findIndex(c => c.toNode === nodeEl.dataset.id && c.toPort === portId.replace('in', '')); - if (existingIdx !== -1) { - connections.splice(existingIdx, 1); - runSimulation(); - return; - } + if (existingIdx !== -1) { connections.splice(existingIdx, 1); runSimulation(); return; } } if (portId === 'out') { @@ -385,32 +378,41 @@ clearSelection(); selectedNodeId = nodeEl.dataset.id; nodeEl.classList.add('selected'); - isDraggingNode = nodeEl.dataset.id; + const rect = nodeEl.getBoundingClientRect(); - dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; + dragOffset = { x: (e.clientX - rect.left) / zoom, y: (e.clientY - rect.top) / zoom }; return; } + // Clicked empty space -> Pan Camera clearSelection(); + isPanning = true; + panStart = { x: e.clientX - panX, y: e.clientY - panY }; }); window.addEventListener('mousemove', (e) => { const wsRect = workspace.getBoundingClientRect(); + if (isPanning) { + panX = e.clientX - panStart.x; + panY = e.clientY - panStart.y; + updateViewport(); + return; + } + if (isDraggingNode) { const node = nodes[isDraggingNode]; - let newX = e.clientX - wsRect.left - dragOffset.x; - let newY = e.clientY - wsRect.top - dragOffset.y; - node.x = Math.max(10, Math.min(newX, wsRect.width - 80)); - node.y = Math.max(20, Math.min(newY, wsRect.height - 60)); + let newX = (e.clientX - wsRect.left - panX) / zoom - dragOffset.x; + let newY = (e.clientY - wsRect.top - panY) / zoom - dragOffset.y; + node.x = newX; node.y = newY; updateNodePositions(); } if (wiringStart) { tempWirePath = { - x: e.clientX - wsRect.left, - y: e.clientY - wsRect.top + x: (e.clientX - wsRect.left - panX) / zoom, + y: (e.clientY - wsRect.top - panY) / zoom }; renderWires(); } @@ -418,6 +420,7 @@ window.addEventListener('mouseup', (e) => { isDraggingNode = null; + isPanning = false; if (wiringStart) { const port = e.target.closest('.lg-port'); @@ -427,18 +430,10 @@ if (targetNodeId !== wiringStart.node) { connections = connections.filter(c => !(c.toNode === targetNodeId && c.toPort === targetPortId)); - - connections.push({ - id: `conn_${nextWireId++}`, - fromNode: wiringStart.node, - fromPort: 'out', - toNode: targetNodeId, - toPort: targetPortId - }); + connections.push({ id: `conn_${nextWireId++}`, fromNode: wiringStart.node, fromPort: 'out', toNode: targetNodeId, toPort: targetPortId }); } } - wiringStart = null; - tempWirePath = null; + wiringStart = null; tempWirePath = null; runSimulation(); } }); @@ -448,17 +443,15 @@ if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedWireId) { connections = connections.filter(c => c.id !== selectedWireId); - clearSelection(); - runSimulation(); + clearSelection(); runSimulation(); } else if (selectedNodeId) { connections = connections.filter(c => c.fromNode !== selectedNodeId && c.toNode !== selectedNodeId); if (nodes[selectedNodeId] && nodes[selectedNodeId].el) { - workspace.removeChild(nodes[selectedNodeId].el); + viewport.removeChild(nodes[selectedNodeId].el); } delete nodes[selectedNodeId]; - clearSelection(); - runSimulation(); + clearSelection(); runSimulation(); } } }); @@ -471,17 +464,16 @@ if (spawnType) { const gateType = e.dataTransfer.getData('gateType'); const wsRect = workspace.getBoundingClientRect(); - const x = e.clientX - wsRect.left - 40; - const y = e.clientY - wsRect.top - 30; + const x = (e.clientX - wsRect.left - panX) / zoom - 40; + const y = (e.clientY - wsRect.top - panY) / zoom - 30; spawnNode(spawnType, gateType || null, x, y); } }); /* --- Init --- */ btnClearBoard?.addEventListener('click', () => { - workspace.querySelectorAll('.lg-node').forEach(el => el.remove()); - nodes = {}; - connections = []; + viewport.querySelectorAll('.lg-node').forEach(el => el.remove()); + nodes = {}; connections = []; runSimulation(); }); @@ -493,5 +485,4 @@ }); initToolbox(); - // Starts completely blank as requested! })(); \ No newline at end of file diff --git a/src/styles/logic-gates.css b/src/styles/logic-gates.css index 2766597..fdba724 100644 --- a/src/styles/logic-gates.css +++ b/src/styles/logic-gates.css @@ -1,161 +1,109 @@ /* === FULL PAGE OVERRIDES FOR LOGIC GATES === */ -body:has(#logicPage) { - overflow: hidden; /* Prevents the entire page from scrolling */ -} - +body:has(#logicPage) { overflow: hidden; } body:has(#logicPage) .pageWrap { - max-width: 100% !important; /* Forces edge-to-edge canvas */ + max-width: 100% !important; padding: 0 !important; margin: 0 !important; height: calc(100vh - var(--nav-h)); display: flex; flex-direction: column; } - -#logicPage { - padding: 0 !important; /* CRITICAL: Stops the page/header from shifting when toolbox opens */ - margin: 0 !important; -} +#logicPage { padding: 0 !important; margin: 0 !important; } /* === MAIN CONTAINER === */ .lg-container { - flex: 1; - display: flex; - flex-direction: column; - position: relative; - width: 100%; - height: 100%; - overflow: hidden; + flex: 1; display: flex; flex-direction: column; position: relative; + width: 100%; height: 100%; overflow: hidden; } -/* === FIXED HEADER (Ultra Compact) === */ +/* === FIXED HEADER === */ .lg-top-header { - width: 100%; - text-align: center; - padding: 8px 20px 8px; /* Extremely tight padding to maximize canvas */ - background: var(--bg); - z-index: 10; - flex-shrink: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border-bottom: 1px solid rgba(255,255,255,0.08); /* Clean separation line */ + width: 100%; text-align: center; padding: 8px 20px 8px; + background: var(--bg); z-index: 10; flex-shrink: 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; + border-bottom: 1px solid rgba(255,255,255,0.08); } - .lg-title { - font-family: var(--bit-font); - font-size: 32px; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--text); - margin: 0 0 2px 0; /* Minimal gap between title and subtitle */ - line-height: 1; + font-family: var(--bit-font); font-size: 32px; letter-spacing: 0.12em; + text-transform: uppercase; color: var(--text); margin: 0 0 2px 0; line-height: 1; } - .lg-subtitle { - color: var(--muted); - font-size: 14px; - font-family: var(--ui-font); - font-weight: 500; - margin: 0; - line-height: 1.2; + color: var(--muted); font-size: 14px; font-family: var(--ui-font); + font-weight: 500; margin: 0; line-height: 1.2; } - .lg-subtitle kbd { - background: rgba(255,255,255,0.1); - padding: 2px 6px; - border-radius: 4px; - font-family: var(--ui-font); - color: #e8e8ee; + background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px; + font-family: var(--ui-font); color: #e8e8ee; } -/* === DYNAMIC CANVAS === */ +/* === DYNAMIC CANVAS & CAMERA VIEWPORT === */ .lg-workspace { - flex: 1; /* Automatically fills all remaining vertical space */ - position: relative; - width: 100%; + flex: 1; position: relative; width: 100%; background-color: transparent; - background-image: radial-gradient(rgba(255,255,255,0.12) 1px, transparent 1px); - background-size: 24px 24px; - overflow: hidden; + background-image: radial-gradient(rgba(255,255,255,0.15) 1px, transparent 1px); + background-size: 24px 24px; overflow: hidden; + cursor: grab; /* Indicates pannable area */ } +.lg-workspace:active { cursor: grabbing; } -.lg-svg-layer { +.lg-viewport { position: absolute; inset: 0; width: 100%; height: 100%; - pointer-events: none; - z-index: 1; + transform-origin: 0 0; + pointer-events: none; /* Let events reach workspace, nodes will re-enable it */ +} + +/* Zoom UI Controls */ +.lg-zoom-controls { + position: absolute; bottom: 20px; left: 20px; z-index: 100; + display: flex; gap: 8px; +} +.lg-zoom-btn { + width: 40px; height: 40px; border-radius: 8px; font-family: var(--ui-font); + font-size: 22px; font-weight: 800; display: flex; align-items: center; justify-content: center; + background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.15); + color: #e8e8ee; cursor: pointer; backdrop-filter: blur(8px); transition: all 0.2s; +} +.lg-zoom-btn:hover { background: rgba(255,255,255,0.1); border-color: #28f07a; color: #28f07a; } + +.lg-svg-layer { + position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; } /* Wires */ .lg-wire { - stroke: rgba(255,255,255,0.25); - stroke-width: 6; - fill: none; - stroke-linecap: round; + stroke: rgba(255,255,255,0.25); stroke-width: 6; fill: none; stroke-linecap: round; transition: stroke 0.1s ease, filter 0.1s ease, stroke-width 0.1s ease; - pointer-events: stroke; - cursor: pointer; -} -.lg-wire:hover { - stroke: rgba(255,255,255,0.6); - stroke-width: 10; -} -.lg-wire.active { - stroke: #28f07a; - filter: drop-shadow(0 0 6px rgba(40,240,122,0.6)); + pointer-events: stroke; cursor: pointer; } +.lg-wire:hover { stroke: rgba(255,255,255,0.6); stroke-width: 10; } +.lg-wire.active { stroke: #28f07a; filter: drop-shadow(0 0 6px rgba(40,240,122,0.6)); } .lg-wire.active:hover { stroke: #5dff9e; } .lg-wire.selected { - stroke: #ff5555 !important; - stroke-width: 8 !important; - stroke-dasharray: 8 8; - filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important; - animation: wireDash 1s linear infinite; + stroke: #ff5555 !important; stroke-width: 8 !important; stroke-dasharray: 8 8; + filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important; animation: wireDash 1s linear infinite; } @keyframes wireDash { to { stroke-dashoffset: -16; } } - -.lg-wire-temp { - stroke: rgba(255,255,255,0.4); - stroke-dasharray: 8 8; - pointer-events: none; -} +.lg-wire-temp { stroke: rgba(255,255,255,0.4); stroke-dasharray: 8 8; pointer-events: none; } /* Nodes */ .lg-node { - position: absolute; - background: transparent; - border: none; - border-radius: 0; - padding: 4px; - display: flex; - flex-direction: column; - align-items: center; - cursor: grab; - z-index: 10; - user-select: none; - transition: filter 0.2s; + position: absolute; background: transparent; border: none; border-radius: 0; padding: 4px; + display: flex; flex-direction: column; align-items: center; cursor: grab; + z-index: 10; user-select: none; transition: filter 0.2s; + pointer-events: auto; /* Re-enables interaction inside the viewport */ } .lg-node:active { cursor: grabbing; z-index: 20; } .lg-node.selected { filter: drop-shadow(0 0 10px rgba(255,85,85,0.8)); } .lg-header { - font-size: 24px; - color: var(--muted); - font-family: var(--bit-font); - letter-spacing: 2px; - pointer-events: none; - margin-bottom: 6px; + font-size: 24px; color: var(--muted); font-family: var(--bit-font); + letter-spacing: 2px; pointer-events: none; margin-bottom: 6px; } -.lg-gate-container { - position: relative; - display: inline-flex; - align-items: center; -} +.lg-gate-container { position: relative; display: inline-flex; align-items: center; } .lg-gate-svg { width: 100px; height: 50px; display: block; } .lg-line-svg { width: 30px; height: 50px; display: block; } @@ -163,18 +111,14 @@ body:has(#logicPage) .pageWrap { .lg-port { width: 16px; height: 16px; background: #a9acb8; border-radius: 50%; cursor: crosshair; border: 3px solid var(--bg); box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s; - position: absolute; z-index: 5; - transform: translate(-50%, -50%); + position: absolute; z-index: 5; transform: translate(-50%, -50%); } .lg-port:hover { transform: translate(-50%, -50%) scale(1.3); background: #fff; } .lg-port.active { background: #28f07a; box-shadow: 0 0 12px rgba(40,240,122,0.8); border-color: #1f2027; } /* === FLOATING TOOLBOX === */ .toolboxToggle { - position: absolute; - top: 10px; /* Snug inside the thinner header */ - right: 20px; - z-index: 90; + position: absolute; top: 10px; right: 20px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 8px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); @@ -184,53 +128,29 @@ body:has(#logicPage) .pageWrap { .toolboxToggle:hover { border-color: rgba(255,255,255,.22); } .lg-toolbox { - position: absolute; - top: 60px; /* Sits right under the new thin header */ - right: 20px; - bottom: 20px; /* Constrains the height so it scrolls internally */ - width: var(--toolbox-w, 360px); - z-index: 80; - display: flex; - flex-direction: column; - gap: 16px; - transform: translateX(0); + position: absolute; top: 60px; right: 20px; bottom: 20px; width: var(--toolbox-w, 360px); + z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; - overflow-y: auto; - pointer-events: auto; - padding-right: 6px; + overflow-y: auto; pointer-events: auto; padding-right: 6px; } - -/* Faded Subdued Scrollbars */ .lg-toolbox::-webkit-scrollbar { width: 6px; } .lg-toolbox::-webkit-scrollbar-track { background: transparent; } .lg-toolbox::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.05); border-radius: 10px; transition: background 0.3s; } .lg-toolbox:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); } -.lg-toolbox::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); } +.lg-container.toolboxCollapsed .lg-toolbox { transform: translateX(calc(100% + 40px)); opacity: 0; pointer-events: none; } -.lg-container.toolboxCollapsed .lg-toolbox { - transform: translateX(calc(100% + 40px)); - opacity: 0; - pointer-events: none; -} - -/* Toolbox Grid */ -.tb-icon-grid { - display: grid; grid-template-columns: 1fr 1fr; gap: 12px; -} +.tb-icon-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .tb-icon-box { - background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); - border-radius: 12px; width: 100%; padding: 12px 0; + background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; width: 100%; padding: 12px 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: grab; transition: background 0.2s, border-color 0.2s; } .tb-icon-box:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); } .tb-icon-label { font-family: var(--ui-font); font-size: 11px; font-weight: 800; color: var(--text); letter-spacing: 1px; text-transform: uppercase; } -/* Truth Table */ .tt-summary { - font-family: var(--ui-font); font-size: 14px; font-weight: 800; - color: var(--accent, #28f07a); cursor: pointer; user-select: none; - outline: none; margin-bottom: 10px; text-transform: uppercase; + font-family: var(--ui-font); font-size: 14px; font-weight: 800; color: var(--accent, #28f07a); + cursor: pointer; user-select: none; outline: none; margin-bottom: 10px; text-transform: uppercase; } .tt-table-wrap { width: 100%; max-height: 250px; overflow-y: auto; overflow-x: auto; @@ -239,8 +159,6 @@ body:has(#logicPage) .pageWrap { .tt-table-wrap::-webkit-scrollbar { width: 6px; height: 6px; } .tt-table-wrap::-webkit-scrollbar-track { background: transparent; } .tt-table-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; } -.tt-table-wrap:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); } - .tt-table { width: 100%; border-collapse: collapse; text-align: center; font-family: var(--num-font); font-size: 14px; color: #e8e8ee; } .tt-table th { position: sticky; top: 0; background: rgba(31,32,39,0.95); padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.15); color: var(--muted); font-family: var(--bit-font); font-weight: normal; } .tt-table td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); } From 09e0499ba337bc01884c77be4b01cb7fc41f98ce Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Sun, 1 Mar 2026 17:30:41 +0000 Subject: [PATCH 21/23] Full initial 2.0 build Signed-off-by: Alexander Lyall --- dist/binary/index.html | 21 +- dist/hexadecimal/index.html | 21 +- dist/index.html | 26 +- src/layouts/BaseLayout.astro | 26 +- src/pages/about.astro | 37 +++ src/pages/binary.astro | 1 + src/pages/copyright.astro | 30 +++ src/pages/hex-colours.astro | 1 + src/pages/hexadecimal.astro | 1 + src/pages/index.astro | 23 ++ src/pages/legal-code.astro | 20 ++ src/pages/pc-builder.astro | 65 +++++ src/scripts/pcBuilder.js | 423 +++++++++++++++++++++++++++++++ src/styles/global.css | 120 +-------- src/styles/number-simulators.css | 114 +++++++++ src/styles/pc-builder.css | 120 +++++++++ 16 files changed, 920 insertions(+), 129 deletions(-) create mode 100644 src/pages/about.astro create mode 100644 src/pages/copyright.astro create mode 100644 src/pages/index.astro create mode 100644 src/pages/legal-code.astro create mode 100644 src/pages/pc-builder.astro create mode 100644 src/scripts/pcBuilder.js create mode 100644 src/styles/number-simulators.css create mode 100644 src/styles/pc-builder.css diff --git a/dist/binary/index.html b/dist/binary/index.html index be4f129..e72913a 100644 --- a/dist/binary/index.html +++ b/dist/binary/index.html @@ -1,3 +1,20 @@ - Binary Simulator | Computing:Box
Denary
0
Binary
00000000
Computer Science Concept Simulators
© 2026 Computing:Box • Created with ♥ by Alexander Lyall
\ No newline at end of file diff --git a/dist/hexadecimal/index.html b/dist/hexadecimal/index.html index f88bac9..71344d9 100644 --- a/dist/hexadecimal/index.html +++ b/dist/hexadecimal/index.html @@ -1,3 +1,20 @@ - Hexadecimal Simulator | Computing:Box
Denary
0
Hexadecimal
00
Binary
00000000
Computer Science Concept Simulators
© 2026 Computing:Box • Created with ♥ by Alexander Lyall
\ No newline at end of file diff --git a/dist/index.html b/dist/index.html index f669b69..2065b3f 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,7 +1,19 @@ - Astro Basics \ No newline at end of file + Welcome | Computing:Box

Version 2.0 Now Live

Understand Computing concepts better.

+Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum. +

Computing Box Logo
Computer Science Concept Simulators
© 2026 Computing:Box • Created with ♥ by Alexander Lyall
\ No newline at end of file diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 6fa4a20..cf04c9d 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,6 +1,5 @@ --- -import '../styles/global.css'; - +import "../styles/global.css"; const { title = "Computing:Box" } = Astro.props; --- @@ -11,6 +10,22 @@ const { title = "Computing:Box" } = Astro.props; {title} + @@ -29,6 +44,7 @@ const { title = "Computing:Box" } = Astro.props; Hexadecimal Hex Colours Logic Gates + PC Components
@@ -40,7 +56,11 @@ const { title = "Computing:Box" } = Astro.props;
Computer Science Concept Simulators
-
© {new Date().getFullYear()} Computing:Box • Created with ♥ by Mr A Lyall
+
© {new Date().getFullYear()} Computing:Box • Created with ♥ by Alexander Lyall
+
diff --git a/src/pages/about.astro b/src/pages/about.astro new file mode 100644 index 0000000..80122a7 --- /dev/null +++ b/src/pages/about.astro @@ -0,0 +1,37 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+
+

The New Computing:Box Experience

+

+ The platform has been rebuilt from the ground up to provide a deeper, more professional simulation environment. + We've introduced several key simulators and interface upgrades to support the modern Computing classroom: +

+ +
+ +
    +
  • New User Interface (Responsive)
  • +
  • Two's Complement Simulator
  • +
  • Extended Binary Simulator (Custom bit sizes)
  • +
  • Unified Binary Simulator (Unsigned & Two's Complement)
  • +
  • Extended Hexadecimal Simulator
  • +
  • Unified Hexadecimal Simulator (GCSE & A Level)
  • +
  • Enhanced Gate Simulator (Truth Table Creator)
  • +
  • Compound Gate Simulator
  • +
  • Computer Components Simulator
  • +
+
+ +
+

Educational Impact

+

+ Computing:Box is designed to help students learn computing concepts step by step, using clear visuals and simple interactions. + By changing values and seeing results straight away, students can build understanding at their own pace. +

+
+
+
\ No newline at end of file diff --git a/src/pages/binary.astro b/src/pages/binary.astro index 0caf8dc..5233717 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; --- diff --git a/src/pages/copyright.astro b/src/pages/copyright.astro new file mode 100644 index 0000000..2b6f65f --- /dev/null +++ b/src/pages/copyright.astro @@ -0,0 +1,30 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+

Copyright Notice

+

+ Computing:Box + © 2024 by Alexander Lyall is licensed under + Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. +

+ +
+ +

What Does This Mean For You?

+

You are free to:

+
    +
  • Share — copy and redistribute the material in any medium or format.
  • +
  • Adapt — remix, transform, and build upon the material.
  • +
+ +

Under the following terms:

+
    +
  • Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
  • +
  • NonCommercial — You may not use the material for commercial purposes.
  • +
  • ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
  • +
+
+
\ No newline at end of file diff --git a/src/pages/hex-colours.astro b/src/pages/hex-colours.astro index d1e7e40..21a5624 100644 --- a/src/pages/hex-colours.astro +++ b/src/pages/hex-colours.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; --- diff --git a/src/pages/hexadecimal.astro b/src/pages/hexadecimal.astro index e74ca17..a981dc5 100644 --- a/src/pages/hexadecimal.astro +++ b/src/pages/hexadecimal.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/number-simulators.css"; --- diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..7490d18 --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,23 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+
+

Version 2.0 Now Live

+

Understand Computing concepts better.

+

+ Interactive simulators for Binary, Hexadecimal, Logic Gates, and Computer Components designed for the UK curriculum. +

+ +
+ +
+ Computing Box Logo +
+
+
\ No newline at end of file diff --git a/src/pages/legal-code.astro b/src/pages/legal-code.astro new file mode 100644 index 0000000..51c7256 --- /dev/null +++ b/src/pages/legal-code.astro @@ -0,0 +1,20 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+

Legal Code

+ +
+

About the license and Creative Commons

+

+ Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. +

+
+ +

+ By using this licensed material, you accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License. +

+
+
\ No newline at end of file diff --git a/src/pages/pc-builder.astro b/src/pages/pc-builder.astro new file mode 100644 index 0000000..5f68a10 --- /dev/null +++ b/src/pages/pc-builder.astro @@ -0,0 +1,65 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/pc-builder.css"; +--- + + +
+ + + +
+
PC Part Simulator
+
+ Build inside the Case! Snap the Motherboard into the chassis, then populate the slots. Add the side panel when done. Double-Click parts to inspect in 3D. +
+
+ +
+
+ + + +
+ +
+ + + +
+
+ + + +
+
×
+
+
+
+
+
Move mouse to rotate component. Scroll to zoom.
+
+ +
+ + +
\ No newline at end of file diff --git a/src/scripts/pcBuilder.js b/src/scripts/pcBuilder.js new file mode 100644 index 0000000..b23e2b2 --- /dev/null +++ b/src/scripts/pcBuilder.js @@ -0,0 +1,423 @@ +// src/scripts/pcBuilder.js +// Computing:Box — Advanced PC Sandbox + +(() => { + const workspace = document.getElementById("workspace"); + const viewport = document.getElementById("viewport"); + const wireLayer = document.getElementById("wireLayer"); + const specsContainer = document.getElementById("buildSpecsContainer"); + const toolboxGrid = document.getElementById("toolboxGrid"); + const btnClearBoard = document.getElementById("btnClearBoard"); + const toolboxToggle = document.getElementById("toolboxToggle"); + const pcPage = document.getElementById("pcPage"); + + /* --- Extensive PC Component Library --- */ + const PC_PARTS = { + 'CASE': { + name: 'ATX PC Case', w: 600, h: 550, z: 5, ports: [], + slots: { + 'MB1': { x: 20, y: 20, accepts: 'MB' }, + 'PSU1': { x: 20, y: 440, accepts: 'PSU' }, + 'HDD1': { x: 440, y: 20, accepts: 'HDD' }, + 'HDD2': { x: 440, y: 170, accepts: 'HDD' }, + 'SATA_SSD1': { x: 440, y: 320, accepts: 'SATA_SSD' }, + 'SATA_SSD2': { x: 440, y: 400, accepts: 'SATA_SSD' } + }, + svg: `` + }, + 'MB': { + name: 'Motherboard', w: 360, h: 400, z: 10, + ports: [ + { id: 'atx_pwr', x: 340, y: 150 }, { id: 'sata1', x: 340, y: 300 }, { id: 'sata2', x: 340, y: 330 }, + { id: 'usb1', x: 10, y: 40 }, { id: 'usb2', x: 10, y: 70 }, { id: 'usb3', x: 10, y: 100 }, { id: 'usb4', x: 10, y: 130 }, + { id: 'audio', x: 10, y: 170 }, { id: 'disp', x: 10, y: 210 } + ], + slots: { + 'CPU1': { x: 120, y: 40, accepts: 'CPU' }, + 'COOLER1': { x: 100, y: 20, accepts: 'COOLER' }, + 'RAM1': { x: 230, y: 30, accepts: 'RAM' }, 'RAM2': { x: 250, y: 30, accepts: 'RAM' }, + 'RAM3': { x: 270, y: 30, accepts: 'RAM' }, 'RAM4': { x: 290, y: 30, accepts: 'RAM' }, + 'M2_1': { x: 120, y: 170, accepts: 'M2_SSD' }, 'M2_2': { x: 120, y: 250, accepts: 'M2_SSD' }, + 'PCIE1': { x: 40, y: 200, accepts: 'GPU' }, 'PCIE2': { x: 40, y: 300, accepts: 'GPU' } + }, + // Uses a lighter slate grey #2C303A to stand out from the case + svg: `` + }, + 'CPU': { name: 'Processor', w: 80, h: 80, z: 20, ports: [], slots: {}, svg: `CPU` }, + 'COOLER': { name: 'CPU Fan', w: 120, h: 120, z: 30, ports: [], slots: {}, svg: `` }, + 'RAM': { name: 'DDR4 Memory', w: 15, h: 100, z: 20, ports: [], slots: {}, svg: `` }, + 'GPU': { name: 'Graphics Card', w: 280, h: 60, z: 40, slots: {}, ports: [{ id: 'pwr_in', x: 270, y: 10 }, { id: 'disp_out', x: 10, y: 30 }], svg: `` }, + 'M2_SSD': { name: 'M.2 NVMe SSD', w: 80, h: 15, z: 20, ports: [], slots: {}, svg: `` }, + 'SATA_SSD': { name: '2.5" SATA SSD', w: 100, h: 70, z: 20, slots: {}, ports: [{id:'data', x:90, y:20}, {id:'pwr', x:90, y:50}], svg: `SSD` }, + 'HDD': { name: '3.5" Mech HDD', w: 120, h: 140, z: 20, slots: {}, ports: [{id:'data', x:110, y:20}, {id:'pwr', x:110, y:120}], svg: `` }, + 'PSU': { name: 'Power Supply', w: 160, h: 90, z: 20, slots: {}, ports: [{id:'out1',x:150,y:20}, {id:'out2',x:150,y:40}, {id:'out3',x:150,y:60}, {id:'out4',x:150,y:80}], svg: `` }, + 'MONITOR': { name: 'Monitor', w: 240, h: 160, z: 30, slots: {}, ports: [{id:'disp', x:120, y:140}], svg: `` }, + 'KEYBOARD': { name: 'Keyboard', w: 180, h: 60, z: 30, slots: {}, ports: [{id:'usb', x:90, y:10}], svg: `` }, + 'MOUSE': { name: 'Mouse', w: 30, h: 50, z: 30, slots: {}, ports: [{id:'usb', x:15, y:5}], svg: `` }, + 'SPEAKER': { name: 'Speakers', w: 40, h: 80, z: 30, slots: {}, ports: [{id:'audio', x:20, y:10}], svg: `` } + }; + + let nodes = {}; + let connections = []; + let nextNodeId = 1, nextWireId = 1; + + let isDraggingNode = null, dragOffset = { x: 0, y: 0 }; + let wiringStart = null, tempWirePath = null; + let selectedWireId = null, selectedNodeId = null; + + let panX = 0, panY = 0, zoom = 1; + let isPanning = false, panStart = { x: 0, y: 0 }; + + /* --- Setup Toolbox --- */ + function initToolbox() { + if(!toolboxGrid) return; + let html = ''; + Object.keys(PC_PARTS).forEach(partKey => { + html += ` +
+ ${PC_PARTS[partKey].svg} +
${partKey}
+
+ `; + }); + toolboxGrid.innerHTML = html; + document.querySelectorAll('.drag-item').forEach(item => { + item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('spawnType', item.dataset.spawn); }); + }); + } + + /* --- Camera Math --- */ + function updateViewport() { + viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; + workspace.style.backgroundSize = `${32 * zoom}px ${32 * zoom}px`; + workspace.style.backgroundPosition = `${panX}px ${panY}px`; + } + function zoomWorkspace(factor, mouseX, mouseY) { + const newZoom = Math.min(Math.max(0.1, zoom * factor), 2); + panX = mouseX - (mouseX - panX) * (newZoom / zoom); + panY = mouseY - (mouseY - panY) * (newZoom / zoom); + zoom = newZoom; updateViewport(); + } + function getPortCoords(nodeId, portDataAttr) { + const node = nodes[nodeId]; + if (!node || !node.el) return {x:0, y:0}; + const portEl = node.el.querySelector(`[data-port="${portDataAttr}"]`); + if (!portEl) return {x:0, y:0}; + const wsRect = workspace.getBoundingClientRect(); + const portRect = portEl.getBoundingClientRect(); + return { + x: (portRect.left - wsRect.left - panX + portRect.width / 2) / zoom, + y: (portRect.top - wsRect.top - panY + portRect.height / 2) / zoom + }; + } + function drawBezier(x1, y1, x2, y2) { + const cpDist = Math.abs(x2 - x1) * 0.6 + 20; + return `M ${x1} ${y1} C ${x1 + cpDist} ${y1}, ${x2 - cpDist} ${y2}, ${x2} ${y2}`; + } + + /* --- Rendering --- */ + function renderWires() { + let svgHTML = ''; + connections.forEach(conn => { + const from = getPortCoords(conn.fromNode, conn.fromPort); + const to = getPortCoords(conn.toNode, conn.toPort); + const isSelected = conn.id === selectedWireId; + svgHTML += ``; + }); + if (wiringStart && tempWirePath) { + svgHTML += ``; + } + wireLayer.innerHTML = svgHTML; + } + + function updateNodePositions() { + Object.values(nodes).forEach(n => { + if (n.el) { n.el.style.left = `${n.x}px`; n.el.style.top = `${n.y}px`; } + }); + renderWires(); + } + + function clearSelection() { + selectedWireId = null; selectedNodeId = null; + document.querySelectorAll('.pb-node.selected').forEach(el => el.classList.remove('selected')); + renderWires(); + } + + /* --- Seven-Segment Diagnostics Engine --- */ + function evaluateBuild() { + if(!specsContainer) return; + + let hasCase = false, hasMB = false, hasCPU = false, hasCooler = false, hasRAM = false, hasPSU = false; + let hasStorage = false, hasGPU = false; + let mbPwr = false, gpuPwr = false; + let usbCount = 0, dispConn = false, audConn = false; + + let caseNode = Object.values(nodes).find(n => n.type === 'CASE'); + let mbNode = Object.values(nodes).find(n => n.type === 'MB'); + + if (caseNode) { + hasCase = true; + if (caseNode.slots['MB1']) hasMB = true; + if (caseNode.slots['PSU1']) hasPSU = true; + if (caseNode.slots['HDD1'] || caseNode.slots['HDD2'] || caseNode.slots['SATA_SSD1'] || caseNode.slots['SATA_SSD2']) hasStorage = true; + } else if (mbNode) { + hasMB = true; // Motherboard exists outside case + } + + if (mbNode) { + if (mbNode.slots['CPU1']) hasCPU = true; + if (mbNode.slots['COOLER1']) hasCooler = true; + if (mbNode.slots['RAM1'] || mbNode.slots['RAM2'] || mbNode.slots['RAM3'] || mbNode.slots['RAM4']) hasRAM = true; + if (mbNode.slots['PCIE1'] || mbNode.slots['PCIE2']) hasGPU = true; + if (mbNode.slots['M2_1'] || mbNode.slots['M2_2']) hasStorage = true; + } + + // Check Cables + connections.forEach(c => { + let n1 = nodes[c.fromNode], n2 = nodes[c.toNode]; + if(!n1 || !n2) return; + let types = [n1.type, n2.type]; + + if(types.includes('MB') && types.includes('PSU')) mbPwr = true; + if(types.includes('GPU') && types.includes('PSU')) gpuPwr = true; + if(types.includes('MB') && ['KEYBOARD','MOUSE','WEBCAM','MIC','PRINTER'].some(t => types.includes(t))) usbCount++; + if(types.includes('MB') && types.includes('SPEAKER')) audConn = true; + if((types.includes('MB') || types.includes('GPU')) && types.includes('MONITOR')) dispConn = true; + }); + + const isBootable = (hasMB && hasCPU && hasCooler && hasRAM && hasPSU && hasStorage && mbPwr && (hasGPU ? gpuPwr : true) && dispConn); + + specsContainer.innerHTML = ` +
Core System
+
CHASSIS${hasCase ? 'OK' : 'ERR'}
+
MOTHERBOARD${hasMB ? 'OK' : 'ERR'}
+
CPU${hasCPU ? 'OK' : 'ERR'}
+
COOLING${hasCooler ? 'OK' : 'ERR'}
+
MEMORY${hasRAM ? 'OK' : 'ERR'}
+
POWER SPLY${hasPSU ? 'OK' : 'ERR'}
+
Connections
+
MB POWER${mbPwr ? 'OK' : 'ERR'}
+
STORAGE${hasStorage ? 'OK' : 'ERR'}
+
GPU POWER${!hasGPU ? 'N/A' : (gpuPwr ? 'OK' : 'ERR')}
+
DISPLAY${dispConn ? 'OK' : 'ERR'}
+
USB DEVS${usbCount}
+
+
+ ${isBootable ? 'BOOTING...' : 'HALTED'} +
+ `; + } + + /* --- Node Creation & Snapping --- */ + function createNodeElement(node) { + const el = document.createElement('div'); + el.className = `pb-node`; el.dataset.id = node.id; + el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; + el.style.width = `${PC_PARTS[node.type].w}px`; el.style.height = `${PC_PARTS[node.type].h}px`; + el.style.zIndex = PC_PARTS[node.type].z; + + let innerHTML = `${PC_PARTS[node.type].svg}`; + PC_PARTS[node.type].ports.forEach(p => { + innerHTML += `
`; + }); + + // Debug Labels for bare parts + if(node.type !== 'CASE' && node.type !== 'MB') { + innerHTML += `
${node.type}
`; + } + + el.innerHTML = innerHTML; + viewport.appendChild(el); + node.el = el; + return el; + } + + function spawnNode(type, dropX = null, dropY = null) { + const id = `node_${nextNodeId++}`; + const x = dropX !== null ? dropX : 300 + Math.random()*40; + const y = dropY !== null ? dropY : 150 + Math.random()*40; + + const node = { id, type, x, y, snappedTo: null, el: null }; + if (PC_PARTS[type].slots) node.slots = { ...PC_PARTS[type].slots }; // Copy slots schema, values will be filled with IDs + + // Reset slot values to null + if(node.slots) { + for(let k in node.slots) { node.slots[k] = null; } + } + + nodes[id] = node; + createNodeElement(node); + evaluateBuild(); + } + + // Recursive movement to handle nested snaps (MB inside CASE inside ...) + function moveNodeRecursive(nodeId, dx, dy) { + const n = nodes[nodeId]; + if(!n) return; + n.x += dx; n.y += dy; + if(n.slots) { + Object.keys(n.slots).forEach(k => { + if(typeof n.slots[k] === 'string') moveNodeRecursive(n.slots[k], dx, dy); + }); + } + } + + /* --- Inspect Mode --- */ + let inspectZoom = 1, inspectRotX = 0, inspectRotY = 0; + workspace.addEventListener('dblclick', (e) => { + const nodeEl = e.target.closest('.pb-node'); + if (nodeEl) { + const node = nodes[nodeEl.dataset.id]; + document.getElementById('inspectModal').classList.add('active'); + document.getElementById('inspectObject').innerHTML = `${PC_PARTS[node.type].svg}`; + document.getElementById('inspectName').innerText = PC_PARTS[node.type].name; + inspectZoom = 1.5; inspectRotX = 0; inspectRotY = 0; updateInspectTransform(); clearSelection(); + } + }); + document.getElementById('inspectStage')?.addEventListener('mousemove', (e) => { + const rect = e.currentTarget.getBoundingClientRect(); + inspectRotY = (e.clientX - rect.left - rect.width/2) / 5; + inspectRotX = -(e.clientY - rect.top - rect.height/2) / 5; + updateInspectTransform(); + }); + document.getElementById('inspectStage')?.addEventListener('wheel', (e) => { + e.preventDefault(); inspectZoom += e.deltaY < 0 ? 0.1 : -0.1; + inspectZoom = Math.max(0.5, Math.min(inspectZoom, 4)); updateInspectTransform(); + }); + function updateInspectTransform() { const obj = document.getElementById('inspectObject'); if(obj) obj.style.transform = `scale(${inspectZoom}) rotateX(${inspectRotX}deg) rotateY(${inspectRotY}deg)`; } + document.getElementById('inspectClose')?.addEventListener('click', () => { document.getElementById('inspectModal').classList.remove('active'); }); + + /* --- Interaction --- */ + document.getElementById("btnZoomIn")?.addEventListener('click', () => { const r = workspace.getBoundingClientRect(); zoomWorkspace(1.2, r.width/2, r.height/2); }); + document.getElementById("btnZoomOut")?.addEventListener('click', () => { const r = workspace.getBoundingClientRect(); zoomWorkspace(1/1.2, r.width/2, r.height/2); }); + document.getElementById("btnZoomReset")?.addEventListener('click', () => { panX = 0; panY = 0; zoom = 1; updateViewport(); }); + + workspace.addEventListener('wheel', (e) => { e.preventDefault(); const wsRect = workspace.getBoundingClientRect(); zoomWorkspace(e.deltaY < 0 ? 1.1 : (1/1.1), e.clientX - wsRect.left, e.clientY - wsRect.top); }); + + workspace.addEventListener('mousedown', (e) => { + const port = e.target.closest('.pb-port'); + if (port) { + const nodeEl = port.closest('.pb-node'); + const portId = port.dataset.port; + const existingIdx = connections.findIndex(c => (c.toNode === nodeEl.dataset.id && c.toPort === portId) || (c.fromNode === nodeEl.dataset.id && c.fromPort === portId)); + if (existingIdx !== -1) { connections.splice(existingIdx, 1); evaluateBuild(); renderWires(); return; } + const coords = getPortCoords(nodeEl.dataset.id, portId); + wiringStart = { node: nodeEl.dataset.id, port: portId, x: coords.x, y: coords.y }; + tempWirePath = { x: coords.x, y: coords.y }; return; + } + + const wire = e.target.closest('.pb-wire'); + if (wire && wire.dataset.connId) { clearSelection(); selectedWireId = wire.dataset.connId; renderWires(); e.stopPropagation(); return; } + + const nodeEl = e.target.closest('.pb-node'); + if (nodeEl) { + clearSelection(); selectedNodeId = nodeEl.dataset.id; nodeEl.classList.add('selected'); isDraggingNode = nodeEl.dataset.id; + const rect = nodeEl.getBoundingClientRect(); dragOffset = { x: (e.clientX - rect.left) / zoom, y: (e.clientY - rect.top) / zoom }; + + // Unsnap from parent when picked up + const node = nodes[isDraggingNode]; + if (node.snappedTo) { + const parent = nodes[node.snappedTo.id]; + if (parent && parent.slots[node.snappedTo.key] === node.id) parent.slots[node.snappedTo.key] = null; + node.snappedTo = null; + node.el.style.zIndex = PC_PARTS[node.type].z; // Reset Z + evaluateBuild(); + } + return; + } + + clearSelection(); isPanning = true; panStart = { x: e.clientX - panX, y: e.clientY - panY }; + }); + + window.addEventListener('mousemove', (e) => { + const wsRect = workspace.getBoundingClientRect(); + if (isPanning) { panX = e.clientX - panStart.x; panY = e.clientY - panStart.y; updateViewport(); return; } + if (isDraggingNode) { + const node = nodes[isDraggingNode]; + let newX = (e.clientX - wsRect.left - panX) / zoom - dragOffset.x; + let newY = (e.clientY - wsRect.top - panY) / zoom - dragOffset.y; + moveNodeRecursive(node.id, newX - node.x, newY - node.y); + updateNodePositions(); + } + if (wiringStart) { tempWirePath = { x: (e.clientX - wsRect.left - panX) / zoom, y: (e.clientY - wsRect.top - panY) / zoom }; renderWires(); } + }); + + window.addEventListener('mouseup', (e) => { + if (isDraggingNode) { + const node = nodes[isDraggingNode]; + let snapped = false; + + // Check all other nodes for compatible slots + Object.values(nodes).forEach(target => { + if (target.slots && !snapped && target.id !== node.id) { + for(let slotKey in target.slots) { + let slotDef = PC_PARTS[target.type].slots[slotKey]; + if(slotDef.accepts === node.type && target.slots[slotKey] === null) { + let tX = target.x + slotDef.x; let tY = target.y + slotDef.y; + if (Math.hypot(node.x - tX, node.y - tY) < 80) { + moveNodeRecursive(node.id, tX - node.x, tY - node.y); + node.snappedTo = { id: target.id, key: slotKey }; + target.slots[slotKey] = node.id; + node.el.style.zIndex = PC_PARTS[target.type].z + 5; // Layer above parent + snapped = true; break; + } + } + } + } + }); + isDraggingNode = null; updateNodePositions(); evaluateBuild(); + } + + if (wiringStart) { + const port = e.target.closest('.pb-port'); + if (port) { + const targetNodeId = port.closest('.pb-node').dataset.id; + const targetPortId = port.dataset.port; + if (targetNodeId !== wiringStart.node) { connections.push({ id: `conn_${nextWireId++}`, fromNode: wiringStart.node, fromPort: wiringStart.port, toNode: targetNodeId, toPort: targetPortId }); } + } + wiringStart = null; tempWirePath = null; evaluateBuild(); renderWires(); + } + isPanning = false; + }); + + /* --- Deletion (Recursive) --- */ + function deleteNodeRecursive(id) { + const n = nodes[id]; if(!n) return; + if(n.slots) { Object.keys(n.slots).forEach(k => { if(typeof n.slots[k] === 'string') deleteNodeRecursive(n.slots[k]); }); } + if(n.snappedTo) { const p = nodes[n.snappedTo.id]; if(p) p.slots[n.snappedTo.key] = null; } + connections = connections.filter(c => c.fromNode !== id && c.toNode !== id); + viewport.removeChild(n.el); delete nodes[id]; + } + + window.addEventListener('keydown', (e) => { + if ((e.key === 'Delete' || e.key === 'Backspace') && selectedNodeId) { + deleteNodeRecursive(selectedNodeId); clearSelection(); evaluateBuild(); renderWires(); + } + if ((e.key === 'Delete' || e.key === 'Backspace') && selectedWireId) { + connections = connections.filter(c => c.id !== selectedWireId); clearSelection(); evaluateBuild(); renderWires(); + } + }); + + workspace.addEventListener('dragover', (e) => { e.preventDefault(); }); + workspace.addEventListener('drop', (e) => { + e.preventDefault(); + const type = e.dataTransfer.getData('spawnType'); + if (type) { + const r = workspace.getBoundingClientRect(); + spawnNode(type, (e.clientX - r.left - panX) / zoom - (PC_PARTS[type].w / 2), (e.clientY - r.top - panY) / zoom - (PC_PARTS[type].h / 2)); + } + }); + + btnClearBoard?.addEventListener('click', () => { + viewport.querySelectorAll('.pb-node').forEach(el => el.remove()); + nodes = {}; connections = []; evaluateBuild(); renderWires(); + }); + + toolboxToggle?.addEventListener("click", () => { + const c = pcPage?.classList.contains("toolboxCollapsed"); + pcPage.classList.toggle("toolboxCollapsed", !c); + toolboxToggle?.setAttribute("aria-expanded", c ? "true" : "false"); + }); + + initToolbox(); evaluateBuild(); +})(); \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css index 0a6b418..ec816f0 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -47,39 +47,9 @@ body { margin: 0; background: var(--bg); color: var(--text); font-family: var(-- .siteFooter { border-top: 1px solid var(--line); background: rgba(0,0,0,.08); } .footerInner { max-width: 1400px; margin: 0 auto; padding: 18px 20px; color: var(--muted); font-size: 12px; letter-spacing: .08em; display: flex; flex-direction: column; gap: 6px; } -/* --- APP LAYOUT --- */ -.binaryPage { - --toolbox-w: 360px; - --toolbox-gap: 22px; - --toolbox-toggle-top: calc(var(--nav-h) + 16px); - --toolbox-top: calc(var(--toolbox-toggle-top) + 60px); - position: relative; padding-top: 16px; flex: 1; display: flex; flex-direction: column; -} -.binaryPage:not(.toolboxCollapsed) { padding-right: calc(var(--toolbox-w) + var(--toolbox-gap)); } -.binaryPage.toolboxCollapsed { padding-right: 0; } -.topGrid { display: flex; align-items: stretch; gap: 28px; flex: 1; } -.leftCol { flex: 1 1 auto; min-width: 0; container-type: inline-size; display: flex; flex-direction: column; } - -/* --- READOUT FORMATTING --- */ -.readoutContainer { display: flex; align-items: center; justify-content: center; gap: 64px; width: 100%; } -.readout { display: flex; flex-direction: column; align-items: center; gap: 16px; padding-top: 4px; } -.readoutBlock { display: flex; flex-direction: column; align-items: center; gap: 8px; } -.label { font-family: var(--bit-font); letter-spacing: .14em; text-transform: uppercase; font-size: 18px; opacity: .75; margin: 0; } -.num { font-family: var(--num-font); color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); letter-spacing: 2px; } - -.denaryValue, .hexValue, .binaryValue { display: flex; gap: 16px; justify-content: center; white-space: pre-wrap; text-align: center; margin: 0; line-height: 1; } -.denaryValue { font-size: 56px; } -.hexValue { font-size: 48px; } -.binaryValue { font-size: 40px; } +/* --- SHARED UI COMPONENTS (Used by ALL Simulators) --- */ .divider { height: 1px; background: rgba(255,255,255,.08); margin: 16px 0 16px; } -/* --- GRIDS & BITS --- */ -.bitsWrap { width: 100%; } -.bitsGrid { --cols: 8; display: grid; grid-template-columns: repeat(var(--cols), minmax(92px, 1fr)); gap: 26px 22px; align-items: start; justify-items: center; } -.bitsGrid.bitsFew { justify-content: center; } -.bit { width: 100%; max-width: 140px; display: flex; flex-direction: column; align-items: center; gap: 8px; container-type: inline-size; } -.bitVal { font-family: var(--bit-font); font-size: min(32px, calc(140cqw / var(--len, 1))); letter-spacing: 2px; color: rgba(232,232,238,.85); white-space: nowrap; line-height: 1; } - .bulb { width: 44px; height: 44px; color: rgba(255,255,255,.15); margin-bottom: 8px; flex-shrink: 0; transition: 0.2s ease; background: transparent; display: flex; align-items: center; justify-content: center; } .bulb svg { width: 100%; height: 100%; display: block; } .bulb.on { color: #ffd86b !important; filter: drop-shadow(0 0 14px rgba(255, 216, 107, 1)) !important; } @@ -92,99 +62,19 @@ body { margin: 0; background: var(--bg); color: var(--text); font-family: var(-- .switch input:checked + .slider { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.30); } .switch input:checked + .slider::before { transform: translateX(28px); } -/* --- HEXADECIMAL --- */ -.hexGrid { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), minmax(160px, 1fr)); gap: 32px 20px; align-items: start; justify-items: center; width: 100%; } -.hexGrid.bitsFew { justify-content: center; } -.hexCol { display: flex; flex-direction: column; align-items: center; width: 100%; } - -/* --- HEX COLOURS SPECIFIC --- */ -.colorGroupWrap { display: flex; flex-wrap: nowrap; gap: 16px; justify-content: center; width: 100%; } -.colorGroup { display: flex; gap: 12px; padding: 12px; background: rgba(255,255,255,.02); border-radius: 20px; border: 1px solid rgba(255,255,255,.05); flex: 0 1 auto; min-width: 0; } -.colorPreviewSide { display: flex; gap: 24px; align-items: center; justify-content: center; } -.colorBoxWrap { display: flex; flex-direction: column; align-items: center; gap: 8px; } -.colorBox { width: 60px; height: 60px; border-radius: 12px; border: 2px solid rgba(255,255,255,.15); box-shadow: 0 4px 16px rgba(0,0,0,.4); background-color: #000000; transition: background-color 0.2s ease; } -.colorBoxLabel { font-family: var(--ui-font); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); } - -.text-red { color: #ff5555 !important; text-shadow: 0 0 18px rgba(255,85,85,.35) !important; } -.text-green { color: #28f07a !important; text-shadow: 0 0 18px rgba(40,240,122,.35) !important; } -.text-blue { color: #55aaff !important; text-shadow: 0 0 18px rgba(85,170,255,.35) !important; } - -/* HEX CARD */ -.hexCard { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 190px; flex: 0 1 auto; min-width: 0; box-shadow: 0 4px 24px rgba(0,0,0,.2); backdrop-filter: blur(10px); } -.hexCardButtons { display: flex; gap: 10px; } -.hexCardBtn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; color: rgba(232,232,238,.92); transition: all 0.2s ease; } -.hexCardBtn.inc { background: rgba(40,240,122,.15); border-color: rgba(40,240,122,.25); } -.hexCardBtn.inc:hover { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.4); } -.hexCardBtn.dec { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.25); } -.hexCardBtn.dec:hover { background: rgba(255,80,80,.28); border-color: rgba(255,80,80,.4); } -.hexDigitDisplay { font-family: var(--num-font); font-size: 48px; color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); line-height: 1; } -.hexNibbleRow { display: flex; gap: 10px; justify-content: center; width: 100%; } -.hexNibbleBit { display: flex; flex-direction: column; align-items: center; gap: 6px; } -.hexNibbleBulb { width: 32px !important; height: 32px !important; margin-bottom: 2px !important; } -.hexNibbleLabel { font-family: var(--bit-font); font-size: 24px; color: rgba(232,232,238,.6); } -.hexColWeight { font-family: var(--bit-font); font-size: 40px; color: rgba(232,232,238,.6); margin-top: 14px; } - - -/* --- TOOLBOX --- */ -.toolboxToggle { position: fixed; top: var(--toolbox-toggle-top); right: 22px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); font-weight: 800; font-size: 16px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; } -.toolboxIcon { font-size: 20px; filter: drop-shadow(0 0 8px rgba(255,105,180,.35)); } -.toolboxToggle:hover { border-color: rgba(255,255,255,.22); } -.panelCol { position: fixed; top: var(--toolbox-top); right: 22px; width: var(--toolbox-w); z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); opacity: 1; transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; } -.binaryPage.toolboxCollapsed .panelCol { transform: translateX(calc(var(--toolbox-w) + 32px)); opacity: 0; pointer-events: none; } .card { background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.10); border-radius: 16px; padding: 16px; backdrop-filter: blur(10px); } .cardTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 18px; color: rgba(232,232,238,.9); margin-bottom: 12px; } .hint { font-family: var(--bit-font); font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: rgba(232,232,238,.55); margin-top: 10px; line-height: 1.35; } -.toggleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.toggleLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; color: var(--muted); transition: color 0.2s, text-shadow 0.2s; } -.toggleLabel.activeMode { color: #28f07a; text-shadow: 0 0 12px rgba(40,240,122,.45); } -.subCard { margin-top: 12px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.10); background: rgba(0,0,0,.12); } -.subTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 16px; margin-bottom: 10px; opacity: .85; } -.bitWidthRow { display: flex; align-items: center; gap: 10px; } -.miniBtn { width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.9); font-family: var(--bit-font); font-weight: 900; font-size: 22px; cursor: pointer; } -.miniBtn:hover { border-color: rgba(255,255,255,.22); } -.bitInputWrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.04); padding: 10px 12px; } -.bitInputLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; } -.bitInput { width: 70px; text-align: right; font-family: var(--num-font); font-size: 28px; letter-spacing: 2px; color: #28f07a; background: transparent; border: none; outline: none; } + .btn { border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); border-radius: 12px; padding: 10px 12px; font-family: var(--bit-font); font-size: 14px; letter-spacing: .12em; text-transform: uppercase; font-weight: 900; cursor: pointer; } .btn:hover { border-color: rgba(255,255,255,.22); } .btnAccent { background: rgba(40,240,122,.12); border-color: rgba(40,240,122,.22); } .btnAccent:hover { border-color: rgba(40,240,122,.35); } .btnHalf { width: calc(50% - 6px); } .btnWide { width: 100%; } -.controlsRow { display: flex; gap: 12px; margin-bottom: 12px; } -.toolRowCentered { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; } -.toolBtn { width: 46px; height: 46px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; } -.toolDec { background: rgba(255,80,80,.20); border-color: rgba(255,80,80,.25); } -.toolInc { background: rgba(40,240,122,.18); border-color: rgba(40,240,122,.25); } .btnReset { color: rgba(232,232,238,.95); } .btnReset:hover { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.35); } -/* === CONTAINER QUERIES === */ -@container (max-width: 1050px) { - .readoutContainer { gap: 40px; } - .colorGroupWrap { gap: 10px; } - .colorGroup { padding: 10px; gap: 8px; border-radius: 16px; } - .hexCard { padding: 12px 8px; width: 140px; gap: 12px; } - .hexDigitDisplay { font-size: 40px; } - .hexNibbleBulb { width: 24px !important; height: 24px !important; } - .hexNibbleLabel { font-size: 20px; } - .hexColWeight { font-size: 26px; margin-top: 10px; } - .hexCardBtn { width: 34px; height: 34px; font-size: 14px; } -} - -@container (max-width: 800px) { - .readoutContainer { flex-direction: column; gap: 24px; } - .colorPreviewSide { padding-top: 0; } - .colorGroupWrap { gap: 6px; } - .colorGroup { padding: 6px; gap: 6px; border-radius: 12px; } - .hexCard { padding: 8px 4px; width: 90px; gap: 8px; border-radius: 10px; } - .hexDigitDisplay { font-size: 32px; } - .hexNibbleBulb { width: 16px !important; height: 16px !important; } - .hexNibbleLabel { font-size: 16px; } - .hexColWeight { font-size: 20px; margin-top: 6px; } - .hexCardBtn { width: 28px; height: 28px; font-size: 12px; } - .denaryValue, .hexValue, .binaryValue { font-size: 32px; gap: 10px; } -} - -@media (max-width: 1100px) { .binaryPage { --toolbox-w: 330px; } .denaryValue { font-size: 48px; } .hexValue { font-size: 40px; } .binaryValue { font-size: 32px; } } -@media (max-width: 900px) { .binaryPage { --toolbox-w: 320px; } .bitsGrid { grid-template-columns: repeat(var(--cols), minmax(84px, 1fr)); } .hexGrid { grid-template-columns: repeat(var(--cols), minmax(130px, 1fr)); } } \ No newline at end of file +.toolboxToggle { position: fixed; top: var(--toolbox-toggle-top, calc(var(--nav-h) + 16px)); right: 22px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); font-weight: 800; font-size: 16px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; } +.toolboxIcon { font-size: 20px; filter: drop-shadow(0 0 8px rgba(255,105,180,.35)); } +.toolboxToggle:hover { border-color: rgba(255,255,255,.22); } \ No newline at end of file diff --git a/src/styles/number-simulators.css b/src/styles/number-simulators.css new file mode 100644 index 0000000..68dc7d5 --- /dev/null +++ b/src/styles/number-simulators.css @@ -0,0 +1,114 @@ +/* --- APP LAYOUT --- */ +.binaryPage { + --toolbox-w: 360px; + --toolbox-gap: 22px; + --toolbox-toggle-top: calc(var(--nav-h) + 16px); + --toolbox-top: calc(var(--toolbox-toggle-top) + 60px); + position: relative; padding-top: 16px; flex: 1; display: flex; flex-direction: column; +} +.binaryPage:not(.toolboxCollapsed) { padding-right: calc(var(--toolbox-w) + var(--toolbox-gap)); } +.binaryPage.toolboxCollapsed { padding-right: 0; } +.topGrid { display: flex; align-items: stretch; gap: 28px; flex: 1; } +.leftCol { flex: 1 1 auto; min-width: 0; container-type: inline-size; display: flex; flex-direction: column; } + +/* --- READOUT FORMATTING --- */ +.readoutContainer { display: flex; align-items: center; justify-content: center; gap: 64px; width: 100%; } +.readout { display: flex; flex-direction: column; align-items: center; gap: 16px; padding-top: 4px; } +.readoutBlock { display: flex; flex-direction: column; align-items: center; gap: 8px; } +.label { font-family: var(--bit-font); letter-spacing: .14em; text-transform: uppercase; font-size: 18px; opacity: .75; margin: 0; } +.num { font-family: var(--num-font); color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); letter-spacing: 2px; } + +.denaryValue, .hexValue, .binaryValue { display: flex; gap: 16px; justify-content: center; white-space: pre-wrap; text-align: center; margin: 0; line-height: 1; } +.denaryValue { font-size: 56px; } +.hexValue { font-size: 48px; } +.binaryValue { font-size: 40px; } + +/* --- GRIDS & BITS --- */ +.bitsWrap { width: 100%; } +.bitsGrid { --cols: 8; display: grid; grid-template-columns: repeat(var(--cols), minmax(92px, 1fr)); gap: 26px 22px; align-items: start; justify-items: center; } +.bitsGrid.bitsFew { justify-content: center; } +.bit { width: 100%; max-width: 140px; display: flex; flex-direction: column; align-items: center; gap: 8px; container-type: inline-size; } +.bitVal { font-family: var(--bit-font); font-size: min(32px, calc(140cqw / var(--len, 1))); letter-spacing: 2px; color: rgba(232,232,238,.85); white-space: nowrap; line-height: 1; } + +/* --- HEXADECIMAL --- */ +.hexGrid { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), minmax(160px, 1fr)); gap: 32px 20px; align-items: start; justify-items: center; width: 100%; } +.hexGrid.bitsFew { justify-content: center; } +.hexCol { display: flex; flex-direction: column; align-items: center; width: 100%; } + +/* --- HEX COLOURS SPECIFIC --- */ +.colorGroupWrap { display: flex; flex-wrap: nowrap; gap: 16px; justify-content: center; width: 100%; } +.colorGroup { display: flex; gap: 12px; padding: 12px; background: rgba(255,255,255,.02); border-radius: 20px; border: 1px solid rgba(255,255,255,.05); flex: 0 1 auto; min-width: 0; } +.colorPreviewSide { display: flex; gap: 24px; align-items: center; justify-content: center; } +.colorBoxWrap { display: flex; flex-direction: column; align-items: center; gap: 8px; } +.colorBox { width: 60px; height: 60px; border-radius: 12px; border: 2px solid rgba(255,255,255,.15); box-shadow: 0 4px 16px rgba(0,0,0,.4); background-color: #000000; transition: background-color 0.2s ease; } +.colorBoxLabel { font-family: var(--ui-font); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); } + +.text-red { color: #ff5555 !important; text-shadow: 0 0 18px rgba(255,85,85,.35) !important; } +.text-green { color: #28f07a !important; text-shadow: 0 0 18px rgba(40,240,122,.35) !important; } +.text-blue { color: #55aaff !important; text-shadow: 0 0 18px rgba(85,170,255,.35) !important; } + +/* HEX CARD */ +.hexCard { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 190px; flex: 0 1 auto; min-width: 0; box-shadow: 0 4px 24px rgba(0,0,0,.2); backdrop-filter: blur(10px); } +.hexCardButtons { display: flex; gap: 10px; } +.hexCardBtn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; color: rgba(232,232,238,.92); transition: all 0.2s ease; } +.hexCardBtn.inc { background: rgba(40,240,122,.15); border-color: rgba(40,240,122,.25); } +.hexCardBtn.inc:hover { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.4); } +.hexCardBtn.dec { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.25); } +.hexCardBtn.dec:hover { background: rgba(255,80,80,.28); border-color: rgba(255,80,80,.4); } +.hexDigitDisplay { font-family: var(--num-font); font-size: 48px; color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); line-height: 1; } +.hexNibbleRow { display: flex; gap: 10px; justify-content: center; width: 100%; } +.hexNibbleBit { display: flex; flex-direction: column; align-items: center; gap: 6px; } +.hexNibbleBulb { width: 32px !important; height: 32px !important; margin-bottom: 2px !important; } +.hexNibbleLabel { font-family: var(--bit-font); font-size: 24px; color: rgba(232,232,238,.6); } +.hexColWeight { font-family: var(--bit-font); font-size: 40px; color: rgba(232,232,238,.6); margin-top: 14px; } + +/* --- TOOLBOX COMPONENTS FOR NUMBERS --- */ +.panelCol { position: fixed; top: var(--toolbox-top); right: 22px; width: var(--toolbox-w); z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); opacity: 1; transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; } +.binaryPage.toolboxCollapsed .panelCol { transform: translateX(calc(var(--toolbox-w) + 32px)); opacity: 0; pointer-events: none; } + +.toggleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.toggleLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; color: var(--muted); transition: color 0.2s, text-shadow 0.2s; } +.toggleLabel.activeMode { color: #28f07a; text-shadow: 0 0 12px rgba(40,240,122,.45); } +.subCard { margin-top: 12px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.10); background: rgba(0,0,0,.12); } +.subTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 16px; margin-bottom: 10px; opacity: .85; } +.bitWidthRow { display: flex; align-items: center; gap: 10px; } +.miniBtn { width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.9); font-family: var(--bit-font); font-weight: 900; font-size: 22px; cursor: pointer; } +.miniBtn:hover { border-color: rgba(255,255,255,.22); } +.bitInputWrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.04); padding: 10px 12px; } +.bitInputLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; } +.bitInput { width: 70px; text-align: right; font-family: var(--num-font); font-size: 28px; letter-spacing: 2px; color: #28f07a; background: transparent; border: none; outline: none; } +.controlsRow { display: flex; gap: 12px; margin-bottom: 12px; } +.toolRowCentered { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; } +.toolBtn { width: 46px; height: 46px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; } +.toolDec { background: rgba(255,80,80,.20); border-color: rgba(255,80,80,.25); } +.toolInc { background: rgba(40,240,122,.18); border-color: rgba(40,240,122,.25); } + +/* === CONTAINER QUERIES === */ +@container (max-width: 1050px) { + .readoutContainer { gap: 40px; } + .colorGroupWrap { gap: 10px; } + .colorGroup { padding: 10px; gap: 8px; border-radius: 16px; } + .hexCard { padding: 12px 8px; width: 140px; gap: 12px; } + .hexDigitDisplay { font-size: 40px; } + .hexNibbleBulb { width: 24px !important; height: 24px !important; } + .hexNibbleLabel { font-size: 20px; } + .hexColWeight { font-size: 26px; margin-top: 10px; } + .hexCardBtn { width: 34px; height: 34px; font-size: 14px; } +} + +@container (max-width: 800px) { + .readoutContainer { flex-direction: column; gap: 24px; } + .colorPreviewSide { padding-top: 0; } + .colorGroupWrap { gap: 6px; } + .colorGroup { padding: 6px; gap: 6px; border-radius: 12px; } + .hexCard { padding: 8px 4px; width: 90px; gap: 8px; border-radius: 10px; } + .hexDigitDisplay { font-size: 32px; } + .hexNibbleBulb { width: 16px !important; height: 16px !important; } + .hexNibbleLabel { font-size: 16px; } + .hexColWeight { font-size: 20px; margin-top: 6px; } + .hexCardBtn { width: 28px; height: 28px; font-size: 12px; } + .denaryValue, .hexValue, .binaryValue { font-size: 32px; gap: 10px; } +} + +@media (max-width: 1100px) { .binaryPage { --toolbox-w: 330px; } .denaryValue { font-size: 48px; } .hexValue { font-size: 40px; } .binaryValue { font-size: 32px; } } +@media (max-width: 900px) { .binaryPage { --toolbox-w: 320px; } .bitsGrid { grid-template-columns: repeat(var(--cols), minmax(84px, 1fr)); } .hexGrid { grid-template-columns: repeat(var(--cols), minmax(130px, 1fr)); } } \ No newline at end of file diff --git a/src/styles/pc-builder.css b/src/styles/pc-builder.css new file mode 100644 index 0000000..df4e5ff --- /dev/null +++ b/src/styles/pc-builder.css @@ -0,0 +1,120 @@ +/* === FULL PAGE OVERRIDES FOR PC BUILDER === */ +body:has(#pcPage) { overflow: hidden; } +body:has(#pcPage) .pageWrap { + max-width: 100% !important; padding: 0 !important; margin: 0 !important; + height: calc(100vh - var(--nav-h)); display: flex; flex-direction: column; +} +#pcPage { padding: 0 !important; margin: 0 !important; } + +/* === MAIN CONTAINER === */ +.pb-container { flex: 1; display: flex; flex-direction: column; position: relative; width: 100%; height: 100%; overflow: hidden; } + +/* === FIXED HEADER === */ +.pb-top-header { + width: 100%; text-align: center; padding: 8px 20px 8px; + background: var(--bg); z-index: 10; flex-shrink: 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; + border-bottom: 1px solid rgba(255,255,255,0.08); +} +.pb-title { font-family: var(--bit-font); font-size: 32px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text); margin: 0 0 2px 0; line-height: 1; } +.pb-subtitle { color: var(--muted); font-size: 14px; font-family: var(--ui-font); font-weight: 500; margin: 0; line-height: 1.2; } +.pb-subtitle kbd { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px; font-family: var(--ui-font); color: #e8e8ee; } + +/* === DYNAMIC CANVAS === */ +.pb-workspace { + flex: 1; position: relative; width: 100%; background-color: transparent; + background-image: radial-gradient(rgba(255,255,255,0.08) 2px, transparent 2px); + background-size: 32px 32px; overflow: hidden; cursor: grab; +} +.pb-workspace:active { cursor: grabbing; } +.pb-viewport { position: absolute; inset: 0; width: 100%; height: 100%; transform-origin: 0 0; pointer-events: none; } + +/* Zoom UI Controls */ +.pb-zoom-controls { position: absolute; bottom: 20px; left: 20px; z-index: 100; display: flex; gap: 8px; } +.pb-zoom-btn { + width: 40px; height: 40px; border-radius: 8px; font-family: var(--ui-font); font-size: 22px; font-weight: 800; + display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); + border: 1px solid rgba(255,255,255,0.15); color: #e8e8ee; cursor: pointer; backdrop-filter: blur(8px); transition: all 0.2s; +} +.pb-zoom-btn:hover { background: rgba(255,255,255,0.1); border-color: #55aaff; color: #55aaff; } + +/* Wires sit at the VERY FRONT so they are never hidden in the case */ +.pb-svg-layer { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 100; pointer-events: none; } + +/* Cables */ +.pb-wire { + stroke: rgba(255,255,255,0.25); stroke-width: 6; fill: none; stroke-linecap: round; + transition: stroke 0.1s ease, filter 0.1s ease, stroke-width 0.1s ease; pointer-events: stroke; cursor: pointer; +} +.pb-wire:hover { stroke: rgba(255,255,255,0.6); stroke-width: 10; } +.pb-wire.active { stroke: #55aaff; filter: drop-shadow(0 0 6px rgba(85,170,255,0.6)); } +.pb-wire.active:hover { stroke: #88ccff; } +.pb-wire.selected { stroke: #ff5555 !important; stroke-width: 8 !important; stroke-dasharray: 8 8; filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important; animation: wireDash 1s linear infinite; } +@keyframes wireDash { to { stroke-dashoffset: -16; } } +.pb-wire-temp { stroke: rgba(255,255,255,0.4); stroke-dasharray: 8 8; pointer-events: none; } + +/* PC Parts */ +.pb-node { + position: absolute; background: transparent; border: none; border-radius: 0; padding: 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: grab; + user-select: none; transition: filter 0.2s; pointer-events: auto; +} +.pb-node:active { cursor: grabbing; } +.pb-node.selected { filter: drop-shadow(0 0 10px rgba(255,85,85,0.8)); } +.pb-part-svg { width: 100%; height: 100%; display: block; pointer-events: none; filter: drop-shadow(0 10px 15px rgba(0,0,0,0.5)); } + +/* Connection Ports */ +.pb-port { + width: 14px; height: 14px; background: #222; border-radius: 50%; cursor: crosshair; + border: 2px solid #55aaff; box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s; + position: absolute; z-index: 200; transform: translate(-50%, -50%); pointer-events: auto; +} +.pb-port:hover { transform: translate(-50%, -50%) scale(1.4); background: #fff; } +.pb-port.active { background: #55aaff; box-shadow: 0 0 12px rgba(85,170,255,0.8); } + +/* === ANIMATIONS (Triggered on Boot) === */ +@keyframes spin { 100% { transform: rotate(360deg); } } +.system-running .fan-blades { animation: spin 0.4s linear infinite; } +.system-running .monitor-screen { opacity: 1 !important; } + +/* === FLOATING TOOLBOX === */ +.pb-toolbox { + position: absolute; top: 60px; right: 20px; bottom: 20px; width: var(--toolbox-w, 360px); + z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); + transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; overflow-y: auto; pointer-events: auto; padding-right: 6px; +} +.pb-toolbox::-webkit-scrollbar { width: 6px; } +.pb-toolbox::-webkit-scrollbar-track { background: transparent; } +.pb-toolbox::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.05); border-radius: 10px; transition: background 0.3s; } +.pb-toolbox:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); } +.pb-container.toolboxCollapsed .pb-toolbox { transform: translateX(calc(100% + 40px)); opacity: 0; pointer-events: none; } + +.tb-icon-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } +.tb-icon-box { + background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; width: 100%; padding: 8px 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; cursor: grab; transition: background 0.2s, border-color 0.2s; +} +.tb-icon-box:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); } +.tb-icon-label { font-family: var(--ui-font); font-size: 10px; font-weight: 800; color: var(--text); letter-spacing: 0px; text-transform: uppercase; text-align: center;} + +/* Diagnostics Panel */ +.specs-panel { width: 100%; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.4); padding: 12px; } +.diag-cat { font-family: var(--ui-font); font-size: 12px; font-weight: 800; color: #55aaff; letter-spacing: 1px; text-transform: uppercase; margin: 12px 0 4px 0; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 2px;} +.diag-cat:first-child { margin-top: 0; } +.diag-row { + display: flex; justify-content: space-between; font-family: var(--bit-font); font-size: 16px; + letter-spacing: 1px; text-transform: uppercase; margin-bottom: 4px; +} + +/* === 3D INSPECT MODAL === */ +.inspect-modal { + position: fixed; inset: 0; background: rgba(10,11,15,0.95); z-index: 1000; + display: flex; align-items: center; justify-content: center; flex-direction: column; + opacity: 0; pointer-events: none; transition: opacity 0.3s ease; backdrop-filter: blur(10px); +} +.inspect-modal.active { opacity: 1; pointer-events: auto; } +.inspect-close { position: absolute; top: 30px; right: 40px; font-size: 40px; color: var(--muted); cursor: pointer; transition: color 0.2s; } +.inspect-close:hover { color: #ff5555; } +.inspect-stage { width: 600px; height: 600px; perspective: 1200px; display: flex; align-items: center; justify-content: center; margin-top: 20px;} +.inspect-object { width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.1s cubic-bezier(0.2, 0.8, 0.2, 1); display: flex; align-items: center; justify-content: center; } +.inspect-object svg { width: 100%; height: 100%; filter: drop-shadow(0 30px 40px rgba(0,0,0,0.8)); } \ No newline at end of file From bb8e0c1969af9aaaca3f052f6e4e16efeea2d388 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Sun, 1 Mar 2026 17:52:36 +0000 Subject: [PATCH 22/23] Broken code Signed-off-by: Alexander Lyall --- public/images/BitBoxLogo.png | Bin 0 -> 103604 bytes public/images/Educational_Impact.webp | Bin 0 -> 434812 bytes .../computingbox-concept-illustration.webp | Bin 0 -> 74606 bytes src/pages/about.astro | 60 ++++++--- src/scripts/pcBuilder.js | 124 +++++------------- src/styles/global.css | 103 +++++++++------ 6 files changed, 132 insertions(+), 155 deletions(-) create mode 100644 public/images/BitBoxLogo.png create mode 100644 public/images/Educational_Impact.webp create mode 100644 public/images/computingbox-concept-illustration.webp diff --git a/public/images/BitBoxLogo.png b/public/images/BitBoxLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..53ba6106a9c4b8968d6f888ff65a425c64a393af GIT binary patch literal 103604 zcmce;1yo$ywk?{F;K_yr3kkv9-Ccsadyql{g%?s#K!5~DAb4oRgiClZ~C1jf0DYgHMo~SCE67{2zam!0TKr ztpqis-~8icz&8;}8#gy6K{hrB1i}j8Vg_`hiW_s{?G5U{q&%Ky*( zJ2?FP3RgE74`3SqSdf1^8dohZClH$k$QA7FVgZuz0OmtQNp5K&Xa#n0FasX8buhCA zu{k+f-%rpyx&X~4t5rHel1RRK`sG74gn^14ncPIznUt8Ep4s5{#{dUZb1&7e`yMclBJoO z+5cSv3j7Z20=xnk2iKqPQ&tv~cXV|#bF=`-ON&qf(_^)@wG_1AHRs^wH|J*I zx8mSn;pgJ!W-$kGakKDoa+#TPv72%5@LB%zd1uz&5@x%XcHlnwUsr#%W&4*t{uDVuIa^m?+r9oxpX7f^nV^}) z{pk^*bT#t;0n78(P1}F7y#G@Z`N#7R8xYXtze>%&h{9j5aRpnsLCjo064t<0{8KE| z{&~L0Ik{MQ$mw-$9WB8SSB8I%&BDga(HaD(Gd4=LzZd9#bu#~Rvhvp`|9_m!pWFJ6 z&HT^%_vdf_LE-&#?*T1(fBCom2EP1l0YHwxI=KM)UHk>zqX!Qo*yN=pv^-OGW+5rl zC%0k;k(OSwUMae)LY80J6#T^{yGV(!o~jx2FlNUR_H1OVqT^pkDl(`UC}4wPIeSb% zxsU6E-c`~CMST%rJM>B$FdW{;ArnofR;*;X-s#=GTsS0Ozt;QhyAnA!ESo-XE#;Mz zjG;x`2!}kNUMSF_B8jsL%GUVoq*e4RLV>4Uz3>e`;~0i)?#sphwI9uUDuxzVwl$J( zS0`KuI6s|F+&NX&PUlp=##wW)`|V_3Bl9C^^yrBPH`(*7&Gd48!|vJqYzN`IH5mMC z2x%iko$~}%$vY%^F~)pb6BNs<^PQ}_3vh^bq1G{Ap3MNJbByJxAVEe^5Ob1p1be1Aa-Axs;6k3 z|LRrL+A+En?8LbKD>jWQ_QuP81Q8tw_U>-rY@=5QbQU8*a%|e7l=mQg!C;-49Hco{ zOSwmg*R*8o*ds!{gtFJb<%)wz(Q$lOGDGG7iR>1k4#FKz*0fFFxTbR8TOoZeDR&$9 zgL(Y(mrFI{Y+xKpi72sAtoD(n!HBn^oh*|uz8l|^dU2O#w(I>Fv9FvSI~4F%<4XU- zK9kpX3J_lM@nTKEyW6!&w`~Cu=vV zzxb+`b58OHIAN`Ru_DKfi*69vfXV(KI%-&g>q0)$un#3C>O}n4yD$(ZY9)LTY8G8q zr=RDKq?Y|*sIq4E~fEgU2wzKUwlie8_*_i&W>2=#}0M7>?tZLieks#3Pv9ck@cyf;%~{kwLST@xcvdIjbR2 zlPE)c5NuwVMB+}Sp)BkJOjg(2gmQ6n`EX|iu=7KNVHt+j9M=nYo$-zN9*fCF?dcNA zt}3_}DRj(kM1HQKc@(N5fl9rP-fTTEfE0qxTQ8vmB_xb*nc=&inhDyw9ZILVq_Y> z8Sjm8HQjWP!=Q*Ys?Qb4gSHMU)TFfC=)#g5D2?q_rm3FSn-LJHfRHR3RteV4mlu6v zOJuKYuD6L2Ve7x&oh?tq>*<$6#BY~zJTk3mid7wwhL95UWL%91iMBdLLB;Tgs#>cyCb{yOuX&8rm1IX zNUrxuHKNM&PKfQF`2G~p362O;5{sKckt#Vtb^B~3xw`L66@)7}jlVV!*OH!8aKUfk zoZvK$f-Fw3&i-f_aB=hA0moreV_{jp;n#a?nkuPBoJL^fZ5Lr_Wi{v77!wkpbPfw*c{mxwscAU?;exDdfD>?nNc3PzmY16uoZ-$pyFf*I6 zi`GW*3|4mkV8Re^>_TEvCt*IxNtd3#E@ayRHtE^n0u4RW>f&2Cj;U4TLPZpHWTMl> z``2w*eHl8EADz9^5wrZI7BeMxMXN2?t-JGm0$(Nrx-##kCj{5=4rL(`FZDboe01hm zwVuWu7U+SmJ$58CxhQf@XTWz0VEYbXYjNv`X+cP^MT_!fxzSI$`i|&$D5{1biV;7J zfEHD_$ct4;1`pFY(d89Jw)D51uGE$7=Z>@_=^_x0_bi-%W^qYt!&~V0I z7!m8<+b^x3+0lA_{Kq)cA{H1F)zkF!Uv{}X2DCkwiYKf~7WpmOI@mb|nj)G(hIou( z)oP$7)6kn`reg>?uH+{??^P3>P$A@FyeVjc!YA@>mM?nL&(4lbit@bL)i_ZBgLXBf zl8DmM8*f#1^%(V{Hui)r{>kmzmr`PSk=kWkUw!tT^w4uWDnGpmj@BP|e7O~70pIPI zx&BoG%|;aKOin4J2|WR{fJncx!CcRZpOwK?Z6_ePs;-)wbn#&f*Tl>XJ;|4m;Qe=$ z4X(K$&!lAGer31vz+a43V%$RCB3Jrs%ltt5T!bgtmIGRi6gnQ(hG*^$*?9F%Ev$BvW&FuGH=jCT2yXia>Yq%jaAQ2tl53A7 z%f0PMQuKcGj@Uc>71`&inp=MS^|y&&OYcA~(`F@t3%_cu6mQC-u7qGedJx~>(~y}8 z%SpzF<{F`+a);FYT%juzK|}GR$d>BtJy|JCin8(iD^`QPCpA`_kWEC&DPV6}SVP`cfu++RAs`;mU~OhQQTC=cjT>fj7& zowM^^ZhBvkbOXd!T_`p%vs$YKAj}%Uw<0Aqs?fX8yN*SEesJ&c}q2C+6yAY{)0nNXPFlaC|CiKju_e2pv{ zN}2t5N0)9DBGoldzoNY7L7n#6P6`7P>8<=@UWG2G-XxuoHRb&GiqJ7vYk$-5s)jPv ztSj0vFHhZ_HQc@+5- zVPoGl*?tg{9_!ymC!Y@K4Q}!@$x`) zH1|9Yz&P>=C<2uqs7(WQC=UIsTj~lE?|6HeKn&xJGJwYX+5I^Y7PL1bo^$ljgV