From ce6c2298a1d0086067fd03a3f579992ee7a961c5 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 22:46:29 +0000 Subject: [PATCH] Binary simulator is feature complete. Known issues: - Formatting of the navbar across the site is broken - Formatting of the Binary simulator is broken Signed-off-by: Alexander Lyall --- src/components/simulators/HexSimulator.astro | 104 +++ .../simulators/hex/hex-simulator.css | 346 ++++++++++ .../simulators/hex/hex-simulator.ts | 232 +++++++ src/layouts/BaseLayout.astro | 149 ++--- src/pages/binary.astro | 59 +- src/pages/hexadecimal.astro | 8 + src/scripts/binary.js | 201 ++++-- src/styles/binary.css | 606 ++++++++---------- 8 files changed, 1200 insertions(+), 505 deletions(-) create mode 100644 src/components/simulators/HexSimulator.astro create mode 100644 src/components/simulators/hex/hex-simulator.css create mode 100644 src/components/simulators/hex/hex-simulator.ts create mode 100644 src/pages/hexadecimal.astro diff --git a/src/components/simulators/HexSimulator.astro b/src/components/simulators/HexSimulator.astro new file mode 100644 index 0000000..4f212bb --- /dev/null +++ b/src/components/simulators/HexSimulator.astro @@ -0,0 +1,104 @@ +--- +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 new file mode 100644 index 0000000..8a15d61 --- /dev/null +++ b/src/components/simulators/hex/hex-simulator.css @@ -0,0 +1,346 @@ +/* ================= 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 new file mode 100644 index 0000000..08bc8aa --- /dev/null +++ b/src/components/simulators/hex/hex-simulator.ts @@ -0,0 +1,232 @@ +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/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 95ef8ae..022fd45 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,7 +1,5 @@ --- -const { - title = "Computing:Box", -} = Astro.props; +const { title = "Computing:Box" } = Astro.props; --- @@ -10,41 +8,10 @@ const { {title} - - - - - - - + + + + + +
+ +
diff --git a/src/pages/binary.astro b/src/pages/binary.astro index e585a28..fc195e5 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -4,21 +4,23 @@ import "../styles/binary.css"; --- - -
+ + +
-
+
Denary
0
Binary
-
0000 0000
+ +
00000000
@@ -28,19 +30,22 @@ import "../styles/binary.css";
- -