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 <alex@adcm.uk>
This commit is contained in:
2025-12-16 22:46:29 +00:00
parent 8b3c58c8d9
commit ce6c2298a1
8 changed files with 1200 additions and 505 deletions

View File

@@ -0,0 +1,104 @@
---
import "./hex/hex-simulator.css";
---
<section class="hex-sim" data-hex-sim>
<div class="hex-main">
<div class="hex-readout">
<div class="hex-label">DENARY</div>
<div class="hex-number" data-out="denary">0</div>
<div class="hex-label hex-mt">HEXADECIMAL</div>
<div class="hex-number hex-number--small" data-out="hex">00</div>
<div class="hex-label hex-mt">BINARY</div>
<div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div>
</div>
<div class="hex-divider"></div>
<div class="hex-digits" data-out="digitsRow"></div>
</div>
<!-- Toolbox button -->
<button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true">
<span class="hex-toolbox-icon" aria-hidden="true">
<!-- toolbox icon -->
<svg viewBox="0 0 24 24" width="18" height="18" fill="none">
<path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
TOOLBOX
</button>
<!-- Toolbox panel -->
<aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox">
<div class="hex-panel">
<div class="hex-panel-title">SETTINGS</div>
<div class="hex-setting-title">HEX DIGIT WIDTH</div>
<div class="hex-width">
<button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus"></button>
<div class="hex-width-readout">
<div class="hex-width-label">DIGITS</div>
<div class="hex-width-number" data-out="digitsCount">2</div>
</div>
<button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button>
</div>
<div class="hex-hint" data-out="bitsHint">= 8 bits</div>
</div>
<div class="hex-panel">
<div class="hex-panel-title">CUSTOM NUMBER</div>
<div class="hex-grid-2">
<button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button>
<button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button>
</div>
<!-- Custom Binary + Random on SAME row, same size -->
<div class="hex-grid-2 hex-mt-sm">
<button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button>
<button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button>
</div>
<div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div>
</div>
<div class="hex-panel">
<div class="hex-panel-title">TOOLS</div>
<div class="hex-tools-top">
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement">▼</button>
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment">▲</button>
</div>
<button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button>
</div>
</aside>
<!-- Custom number dialog -->
<dialog class="hex-dialog" data-out="dialog">
<div class="hex-dialog-card">
<div class="hex-dialog-title" data-out="dialogTitle">Custom</div>
<input class="hex-dialog-input hex-font-mono" data-out="dialogInput" />
<div class="hex-dialog-hint" data-out="dialogHint"></div>
<div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div>
<div class="hex-dialog-actions">
<button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button>
<button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button>
</div>
</div>
</dialog>
<script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script>
</section>

View File

@@ -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; }
}

View File

@@ -0,0 +1,232 @@
type DialogMode = "hex" | "den" | "bin";
const root = document.querySelector<HTMLElement>("[data-hex-sim]");
if (!root) throw new Error("Hex simulator root not found");
const outDen = root.querySelector<HTMLElement>('[data-out="denary"]')!;
const outHex = root.querySelector<HTMLElement>('[data-out="hex"]')!;
const outBin = root.querySelector<HTMLElement>('[data-out="bin"]')!;
const outDigitsRow = root.querySelector<HTMLElement>('[data-out="digitsRow"]')!;
const toolbox = root.querySelector<HTMLElement>('[data-out="toolbox"]')!;
const toolboxBtn = root.querySelector<HTMLButtonElement>('[data-action="toggleToolbox"]')!;
const digitsCount = root.querySelector<HTMLElement>('[data-out="digitsCount"]')!;
const bitsHint = root.querySelector<HTMLElement>('[data-out="bitsHint"]')!;
const randomBtn = root.querySelector<HTMLButtonElement>("[data-random]")!;
const dialog = root.querySelector<HTMLDialogElement>('[data-out="dialog"]')!;
const dialogTitle = root.querySelector<HTMLElement>('[data-out="dialogTitle"]')!;
const dialogInput = root.querySelector<HTMLInputElement>('[data-out="dialogInput"]')!;
const dialogHint = root.querySelector<HTMLElement>('[data-out="dialogHint"]')!;
const dialogError = root.querySelector<HTMLElement>('[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 = `
<div class="hex-digit-controls">
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="digitUp" data-i="${i}" title="Increase">▲</button>
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="digitDown" data-i="${i}" title="Decrease">▼</button>
</div>
<div class="hex-digit-char hex-font-number">${digitChar}</div>
<!-- bulbs: brightness changes based on nibble bits -->
<div class="hex-bulbs" aria-label="Nibble bits">
${[8,4,2,1].map((w, idx) => {
const on = nibbleBits[idx] === 1;
return `
<div class="hex-bulb ${on ? "is-on" : ""}">
<div class="hex-bulb-cap"></div>
<div class="hex-bulb-glow"></div>
<div class="hex-bulb-label">${w}</div>
</div>
`;
}).join("")}
</div>
<div class="hex-digit-place">${placeValue}</div>
`;
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) (09, AF).`;
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 09 and AF 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<HTMLElement>("[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();

View File

@@ -1,7 +1,5 @@
--- ---
const { const { title = "Computing:Box" } = Astro.props;
title = "Computing:Box",
} = Astro.props;
--- ---
<!doctype html> <!doctype html>
@@ -10,41 +8,10 @@ const {
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{title}</title> <title>{title}</title>
<meta name="color-scheme" content="dark" />
</head>
<body>
<header class="siteHeader">
<div class="siteHeaderInner">
<a class="brand" href="/">
<!-- Put your logo file here -->
<img
class="brandLogo"
src="../images/computing-box-logo.svg"
alt="Computing:Box logo"
width="28"
height="28"
/>
<span class="brandName">Computing:Box</span>
</a>
<nav class="siteNav" aria-label="Main navigation">
<a href="/about">About</a>
<a href="/binary">Binary</a>
<a href="/hexadecimal">Hexadecimal</a>
<a href="/hex-colours">Hex Colours</a>
<a href="/logic-gates">Logic Gates</a>
</nav>
</div>
</header>
<slot />
<style> <style>
:root{ :root{
/* 3× navbar height */ --nav-h: 108px; /* 3x-ish height */
--nav-height: 96px;
--nav-pad-x: 22px;
--bg: #1f2027; --bg: #1f2027;
--text: #e8e8ee; --text: #e8e8ee;
--muted: #a9acb8; --muted: #a9acb8;
@@ -57,77 +24,95 @@ const {
color:var(--text); color:var(--text);
} }
.siteHeader{ .siteNav{
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1000; z-index: 50;
height: var(--nav-height); height: var(--nav-h);
display:flex; background: rgba(0,0,0,.10);
align-items:center;
background: rgba(0,0,0,.15);
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
backdrop-filter: blur(10px); backdrop-filter: blur(8px);
} }
.siteHeaderInner{ .navInner{
width: 100%; height: 100%;
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 0 var(--nav-pad-x); padding: 0 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 18px; gap: 24px;
} }
.brand{ .brand{
display:flex; display:flex;
align-items:center; align-items:center;
gap: 10px; gap:12px;
text-decoration:none; text-decoration:none;
color:var(--text); color:var(--text);
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
} }
.brandLogo{ .brandLogo{
display:block; width: 2em;
width: 28px; height: 2em;
height: 28px; image-rendering: pixelated;
} }
.brandName{ .brandName{
font-size: 14px; letter-spacing: .12em;
opacity: .95; font-weight: 900;
text-transform: uppercase;
font-size: 18px;
} }
.siteNav{ .navLinks{
display:flex; display:flex;
gap: 18px;
align-items:center; align-items:center;
gap:18px;
flex-wrap:wrap;
} }
.siteNav a{ .navLinks a{
color: var(--muted); color: var(--muted);
text-decoration: none; text-decoration: none;
font-weight: 700; font-weight: 800;
letter-spacing: .08em; letter-spacing: .12em;
font-size: 16px;
text-transform: uppercase; text-transform: uppercase;
font-size: 11px;
padding: 10px 8px;
border-radius: 10px;
} }
.siteNav a:hover{ .navLinks a:hover{
color: var(--text); color: var(--text);
background: rgba(255,255,255,.06);
} }
@media (max-width: 860px){ .pageWrap{
.siteNav{ gap: 10px; } max-width: 1400px;
.siteNav a{ font-size: 10px; padding: 8px 6px; } margin: 0 auto;
} }
</style> </style>
</head>
<body>
<header class="siteNav">
<div class="navInner">
<a class="brand" href="/">
<img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" />
<span class="brandName">COMPUTING:BOX</span>
</a>
<nav class="navLinks" aria-label="Site navigation">
<a href="/about">ABOUT</a>
<a href="/binary">BINARY</a>
<a href="/hexadecimal">HEXADECIMAL</a>
<a href="/hex-colours">HEX COLOURS</a>
<a href="/logic-gates">LOGIC GATES</a>
</nav>
</div>
</header>
<main class="pageWrap">
<slot />
</main>
</body> </body>
</html> </html>

View File

@@ -4,20 +4,22 @@ import "../styles/binary.css";
--- ---
<BaseLayout title="Binary Simulator"> <BaseLayout title="Binary Simulator">
<main class="wrap">
<!-- Toolbox toggle sits below navbar (navbar is in BaseLayout) -->
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true"> <button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
<span class="toolboxIcon" aria-hidden="true">🧰</span> <span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxLabel">TOOLBOX</span> <span class="toolboxText">TOOLBOX</span>
</button> </button>
<main class="wrap">
<section class="topGrid"> <section class="topGrid">
<!-- LEFT --> <!-- LEFT -->
<div> <div class="mainCol">
<div class="readout"> <div class="readout">
<div class="label">Denary</div> <div class="label">Denary</div>
<div id="denaryNumber" class="num denaryValue">0</div> <div id="denaryNumber" class="num denaryValue">0</div>
<div class="label">Binary</div> <div class="label">Binary</div>
<!-- NOTE: JS writes exact bit-width here, so initial value doesn't matter much -->
<div id="binaryNumber" class="num binaryValue">00000000</div> <div id="binaryNumber" class="num binaryValue">00000000</div>
</div> </div>
@@ -28,19 +30,22 @@ import "../styles/binary.css";
</section> </section>
</div> </div>
<!-- RIGHT TOOLBOX --> <!-- RIGHT (Toolbox panel) -->
<aside id="toolbox" class="panelCol" aria-label="Toolbox"> <aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<!-- SETTINGS --> <!-- Settings -->
<div class="card"> <div class="card">
<div class="cardTitle">Settings</div> <div class="cardTitle">Settings</div>
<div class="toggleRow"> <div class="toggleRow">
<div class="toggleLabel" id="lblUnsigned">Unsigned</div> <div class="toggleLabel" id="lblUnsigned">Unsigned</div>
<label class="switch" aria-label="Toggle mode"> <label class="switch" aria-label="Toggle mode">
<input id="modeToggle" type="checkbox" /> <input id="modeToggle" type="checkbox" />
<span class="slider"></span> <span class="slider"></span>
</label> </label>
<div class="toggleLabel" id="lblTwos">Two&rsquo;s complement</div>
<!-- keep this on ONE line -->
<div class="toggleLabel" id="lblTwos">Two&apos;s&nbsp;complement</div>
</div> </div>
<div class="hint" id="modeHint"> <div class="hint" id="modeHint">
@@ -49,6 +54,7 @@ import "../styles/binary.css";
<div class="subCard"> <div class="subCard">
<div class="subTitle">Bit width</div> <div class="subTitle">Bit width</div>
<div class="bitWidthRow"> <div class="bitWidthRow">
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits"></button> <button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits"></button>
@@ -72,37 +78,34 @@ import "../styles/binary.css";
</div> </div>
</div> </div>
<!-- CUSTOM --> <!-- Custom Number -->
<div class="card"> <div class="card">
<div class="cardTitle">Custom</div> <div class="cardTitle">Custom Number</div>
<div class="twoBtnRow"> <div class="controlsRow">
<button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button> <button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
<button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button> <button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
</div> </div>
<button class="toolBtn toolWide toolRandom" id="btnRandom" type="button"> <button class="btn btnWide" id="btnRandom" type="button">Random</button>
Random
</button>
<div class="hint">Random runs briefly then stops automatically.</div> <div class="hint">Random runs briefly then stops automatically.</div>
</div> </div>
<!-- TOOLS --> <!-- Tools -->
<div class="card"> <div class="card">
<div class="cardTitle">Tools</div> <div class="cardTitle">Tools</div>
<div class="toolsTopRow"> <div class="toolRowCentered">
<button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement">▼</button> <button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
<button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button> <button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
</div> </div>
<div class="twoBtnRow"> <div class="toolRow2">
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button> <button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
<button class="btn" id="btnShiftRight" type="button">Right Shift</button> <button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
</div> </div>
<button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button> <button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div> </div>
</aside> </aside>
</section> </section>

View File

@@ -0,0 +1,8 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import HexSimulator from "../components/simulators/HexSimulator.astro";
---
<BaseLayout title="Hexadecimal | Computing:Box">
<HexSimulator />
</BaseLayout>

View File

@@ -34,9 +34,12 @@
----------------------------- */ ----------------------------- */
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
let bits = new Array(bitCount).fill(false); let bits = new Array(bitCount).fill(false);
let randomTimer = null; let randomTimer = null;
// For responsive wrapping of the top binary display
let nibblesPerLine = null;
let wrapMeasureSpan = null;
/* ----------------------------- /* -----------------------------
HELPERS HELPERS
----------------------------- */ ----------------------------- */
@@ -80,6 +83,7 @@
function unsignedBigIntToBits(vUnsigned) { function unsignedBigIntToBits(vUnsigned) {
const span = unsignedMaxExclusive(bitCount); const span = unsignedMaxExclusive(bitCount);
const v = ((vUnsigned % span) + span) % span; const v = ((vUnsigned % span) + span) % span;
for (let i = 0; i < bitCount; i++) { for (let i = 0; i < bitCount; i++) {
bits[i] = ((v >> BigInt(i)) & 1n) === 1n; bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
} }
@@ -98,16 +102,6 @@
unsignedBigIntToBits(v); unsignedBigIntToBits(v);
} }
function formatBinaryGrouped() {
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() { function updateModeHint() {
if (!modeHint) return; if (!modeHint) return;
modeHint.textContent = isTwosMode() modeHint.textContent = isTwosMode()
@@ -115,6 +109,72 @@
: "Tip: In unsigned binary, all bits represent positive values."; : "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) BUILD UI (BITS)
----------------------------- */ ----------------------------- */
@@ -128,9 +188,17 @@
bitsGrid.innerHTML = ""; 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--) { for (let i = bitCount - 1; i >= 0; i--) {
const bitEl = document.createElement("div"); const bitEl = document.createElement("div");
bitEl.className = "bit"; bitEl.className = "bit";
bitEl.innerHTML = ` bitEl.innerHTML = `
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div> <div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
<div class="bitVal" id="bitLabel-${i}"></div> <div class="bitVal" id="bitLabel-${i}"></div>
@@ -139,6 +207,7 @@
<span class="slider"></span> <span class="slider"></span>
</label> </label>
`; `;
bitsGrid.appendChild(bitEl); bitsGrid.appendChild(bitEl);
} }
@@ -150,6 +219,28 @@
}); });
}); });
// 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(); updateUI();
} }
@@ -161,10 +252,8 @@
const label = document.getElementById(`bitLabel-${i}`); const label = document.getElementById(`bitLabel-${i}`);
if (!label) continue; if (!label) continue;
// Keep label on ONE LINE (no wrapping)
label.style.whiteSpace = "nowrap";
if (isTwosMode() && i === bitCount - 1) { if (isTwosMode() && i === bitCount - 1) {
// Keep on one line (CSS: white-space:nowrap)
label.textContent = `-${pow2Big(bitCount - 1).toString()}`; label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
} else { } else {
label.textContent = pow2Big(i).toString(); label.textContent = pow2Big(i).toString();
@@ -183,14 +272,25 @@
for (let i = 0; i < bitCount; i++) { for (let i = 0; i < bitCount; i++) {
const bulb = document.getElementById(`bulb-${i}`); const bulb = document.getElementById(`bulb-${i}`);
if (!bulb) continue; if (!bulb) continue;
bulb.classList.toggle("on", bits[i] === true);
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() { function updateReadout() {
if (!denaryEl || !binaryEl) return; if (!denaryEl || !binaryEl) return;
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString(); denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
binaryEl.textContent = formatBinaryGrouped(); binaryEl.textContent = formatBinaryWrapped();
} }
function updateUI() { function updateUI() {
@@ -202,16 +302,18 @@
} }
/* ----------------------------- /* -----------------------------
INPUT SETTERS SET FROM INPUT
----------------------------- */ ----------------------------- */
function setFromBinaryString(binStr) { function setFromBinaryString(binStr) {
const clean = String(binStr ?? "").replace(/\s+/g, ""); const clean = String(binStr ?? "").replace(/\s+/g, "");
if (!/^[01]+$/.test(clean)) return false; if (!/^[01]+$/.test(clean)) return false;
const padded = clean.slice(-bitCount).padStart(bitCount, "0"); const padded = clean.slice(-bitCount).padStart(bitCount, "0");
for (let i = 0; i < bitCount; i++) { for (let i = 0; i < bitCount; i++) {
const charFromRight = padded[padded.length - 1 - i]; const charFromRight = padded[padded.length - 1 - i];
bits[i] = charFromRight === "1"; bits[i] = charFromRight === "1";
} }
updateUI(); updateUI();
return true; return true;
} }
@@ -253,13 +355,15 @@
} }
function shiftRight() { function shiftRight() {
// Unsigned: logical right shift (MSB becomes 0) if (isTwosMode()) {
// Two's complement: arithmetic right shift (MSB preserved) // arithmetic right shift: keep MSB
const msb = bits[bitCount - 1]; 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] = msb;
bits[bitCount - 1] = isTwosMode() ? msb : false; } else {
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
bits[bitCount - 1] = false;
}
updateUI(); updateUI();
} }
@@ -280,8 +384,7 @@
signedBigIntToBitsTwos(v); signedBigIntToBitsTwos(v);
} else { } else {
const span = unsignedMaxExclusive(bitCount); const span = unsignedMaxExclusive(bitCount);
const v = (bitsToUnsignedBigInt() + 1n) % span; unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
unsignedBigIntToBits(v);
} }
updateUI(); updateUI();
} }
@@ -295,18 +398,16 @@
signedBigIntToBitsTwos(v); signedBigIntToBitsTwos(v);
} else { } else {
const span = unsignedMaxExclusive(bitCount); const span = unsignedMaxExclusive(bitCount);
const v = (bitsToUnsignedBigInt() - 1n + span) % span; unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
unsignedBigIntToBits(v);
} }
updateUI(); updateUI();
} }
/* ----------------------------- /* -----------------------------
RANDOM (with running pulse + longer run) RANDOM
----------------------------- */ ----------------------------- */
function cryptoRandomBigInt(maxExclusive) { function cryptoRandomBigInt(maxExclusive) {
if (maxExclusive <= 0n) return 0n; if (maxExclusive <= 0n) return 0n;
const bitLen = maxExclusive.toString(2).length; const bitLen = maxExclusive.toString(2).length;
const byteLen = Math.ceil(bitLen / 8); const byteLen = Math.ceil(bitLen / 8);
@@ -319,13 +420,12 @@
const extraBits = BigInt(byteLen * 8 - bitLen); const extraBits = BigInt(byteLen * 8 - bitLen);
if (extraBits > 0n) x = x >> extraBits; if (extraBits > 0n) x = x >> extraBits;
if (x < maxExclusive) return x; if (x < maxExclusive) return x;
} }
} }
function setRandomOnce() { function setRandomOnce() {
const span = unsignedMaxExclusive(bitCount); // 2^n const span = unsignedMaxExclusive(bitCount);
const u = cryptoRandomBigInt(span); const u = cryptoRandomBigInt(span);
unsignedBigIntToBits(u); unsignedBigIntToBits(u);
updateUI(); updateUI();
@@ -337,11 +437,8 @@
randomTimer = null; randomTimer = null;
} }
// pulse while running
btnRandom?.classList.add("is-running");
const start = Date.now(); const start = Date.now();
const durationMs = 1125; // 25% longer than 900ms const durationMs = 1125; // (your “~25% longer” vs 900ms)
const tickMs = 80; const tickMs = 80;
randomTimer = setInterval(() => { randomTimer = setInterval(() => {
@@ -349,31 +446,30 @@
if (Date.now() - start >= durationMs) { if (Date.now() - start >= durationMs) {
clearInterval(randomTimer); clearInterval(randomTimer);
randomTimer = null; randomTimer = null;
btnRandom?.classList.remove("is-running");
} }
}, tickMs); }, tickMs);
} }
/* ----------------------------- /* -----------------------------
BIT WIDTH BIT WIDTH CONTROLS
----------------------------- */ ----------------------------- */
function setBitWidth(n) { function setBitWidth(n) {
buildBits(clampInt(n, 1, 64)); buildBits(clampInt(n, 1, 64));
} }
/* ----------------------------- /* -----------------------------
TOOLBOX VISIBILITY TOOLBOX TOGGLE (simple open/close state)
----------------------------- */ ----------------------------- */
function setToolboxVisible(isVisible) { function setToolboxOpen(open) {
if (!toolboxPanel) return; document.body.classList.toggle("toolboxClosed", !open);
toolboxPanel.style.display = isVisible ? "flex" : "none"; toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
toolboxToggle?.setAttribute("aria-expanded", String(isVisible)); refreshBinaryWrap(); // width changes when toolbox closes/opens
} }
/* ----------------------------- /* -----------------------------
EVENTS EVENTS
----------------------------- */ ----------------------------- */
modeToggle?.addEventListener("change", updateUI); modeToggle?.addEventListener("change", () => updateUI());
btnCustomBinary?.addEventListener("click", () => { btnCustomBinary?.addEventListener("click", () => {
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`); const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
@@ -384,8 +480,8 @@
btnCustomDenary?.addEventListener("click", () => { btnCustomDenary?.addEventListener("click", () => {
const v = prompt( const v = prompt(
isTwosMode() isTwosMode()
? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):` ? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):` : `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
); );
if (v === null) return; if (v === null) return;
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width"); if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
@@ -406,8 +502,15 @@
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value))); bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
toolboxToggle?.addEventListener("click", () => { toolboxToggle?.addEventListener("click", () => {
const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false"; const isOpen = !document.body.classList.contains("toolboxClosed");
setToolboxVisible(!isOpen); setToolboxOpen(!isOpen);
});
// Recompute wrapping live when the window size changes
let resizeT = null;
window.addEventListener("resize", () => {
if (resizeT) clearTimeout(resizeT);
resizeT = setTimeout(() => refreshBinaryWrap(), 60);
}); });
/* ----------------------------- /* -----------------------------
@@ -415,5 +518,5 @@
----------------------------- */ ----------------------------- */
updateModeHint(); updateModeHint();
buildBits(bitCount); buildBits(bitCount);
setToolboxVisible(true); setToolboxOpen(true);
})(); })();

View File

@@ -1,314 +1,227 @@
/* Binary page styles (moved OUT of binary.astro) */
:root{ :root{
--bg: #1f2027; --panel-w: 360px;
--panel2: rgba(255,255,255,.04); --gap: 22px;
--text: #e8e8ee;
--muted: #a9acb8;
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
--line: rgba(255,255,255,.12);
}
/* NUMBERS font */
@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;
}
/* TEXT font */
@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;
}
body{
margin:0;
font-family: "SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
} }
.wrap{ .wrap{
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 26px 22px 60px; padding: 26px 20px 48px;
position: relative;
} }
/* MAIN GRID */
.topGrid{ .topGrid{
display: grid; display: grid;
grid-template-columns: 1fr 360px; grid-template-columns: 1fr var(--panel-w);
gap: 28px; gap: var(--gap);
align-items: start; align-items: start;
} }
/* Toolbox toggle BELOW navbar */ /* When toolbox is hidden, reclaim space + centre content */
.toolboxToggle{ body.toolboxClosed .topGrid{
position: sticky; grid-template-columns: 1fr;
top: calc(var(--nav-height) + 10px); }
z-index: 900; body.toolboxClosed #toolboxPanel{
float: right; display: none;
}
display:inline-flex;
align-items:center; .mainCol{
gap: 10px; min-width: 0;
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.14);
color: #fff;
padding: 10px 14px;
border-radius: 12px;
font-weight: 900;
cursor: pointer;
letter-spacing: .10em;
text-transform: uppercase;
font-size: 12px;
} }
.toolboxToggle:hover{ background: rgba(255,255,255,.09); }
/* Readout */
.readout{ .readout{
text-align: center; text-align: center;
padding: 10px 10px 0; margin-top: 8px;
} }
.label{ .label{
letter-spacing: .18em; opacity: .8;
font-weight: 900; letter-spacing: .12em;
color: var(--muted);
text-transform: uppercase; text-transform: uppercase;
font-size: 13px; font-size: 12px;
margin-top: 10px;
} }
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
.num{ .num{
font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; display: inline-block;
font-weight: 400; width: fit-content;
color: var(--accent); max-width: 100%;
text-shadow: 0 0 18px var(--accent-dim); white-space: pre-line; /* allows JS \n wraps */
letter-spacing: 2px;
} }
.denaryValue{ .denaryValue{
font-size: 52px; font-size: 54px;
line-height: 1.0;
margin: 6px 0 10px; margin: 6px 0 10px;
} }
.binaryValue{ .binaryValue{
font-size: 46px; font-size: 56px;
letter-spacing: .14em; margin: 4px 0 18px;
line-height: 1.05;
margin: 6px 0 14px;
white-space: pre-wrap;
word-break: break-word;
} }
.divider{ .divider{
margin-top: 26px; height: 1px;
border-top: 1px solid var(--line); background: rgba(255,255,255,.10);
margin: 14px auto 24px;
max-width: 900px;
} }
/* RIGHT COLUMN */ .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{ .panelCol{
display:flex; position: sticky;
flex-direction:column; top: calc(var(--nav-h, 72px) + 18px);
gap: 14px; align-self: start;
display: grid;
gap: 16px;
} }
/* Cards */
.card{ .card{
background: var(--panel2); border: 1px solid rgba(255,255,255,.12);
border: 1px solid rgba(255,255,255,.10); border-radius: 16px;
border-radius: 14px; background: rgba(255,255,255,.05);
padding: 14px; padding: 14px;
} }
.cardTitle{ .cardTitle{
letter-spacing: .18em; opacity: .8;
font-weight: 900; letter-spacing: .14em;
color: var(--muted);
text-transform: uppercase; text-transform: uppercase;
font-size: 12px; font-size: 12px;
margin: 0 0 10px; 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{ .subCard{
margin-top: 12px; margin-top: 12px;
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.10); border: 1px solid rgba(255,255,255,.10);
border-radius: 12px; border-radius: 14px;
background: rgba(0,0,0,.12);
padding: 12px; padding: 12px;
} }
.subTitle{ .subTitle{
letter-spacing: .18em; opacity: .8;
font-weight: 900; letter-spacing: .14em;
color: var(--muted);
text-transform: uppercase; text-transform: uppercase;
font-size: 11px; font-size: 11px;
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: 900;
font-size: 13px;
letter-spacing: .08em;
text-transform: uppercase;
}
/* Switch */
.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);
}
/* Buttons */
.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: 900;
cursor: pointer;
letter-spacing: .10em;
text-transform: uppercase;
font-size: 12px;
}
.btn:active{ transform: translateY(1px); }
.btnAccent{
background: rgba(51,255,122,.18);
border-color: rgba(51,255,122,.45);
}
.customRow{
display:grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.btnRandom{
width: 100%;
}
/* Random pulse ONLY while running */
@keyframes pulseGreen {
0% { box-shadow: 0 0 0 rgba(51,255,122,0); }
50% { box-shadow: 0 0 18px rgba(51,255,122,.55), 0 0 28px rgba(51,255,122,.25); }
100% { box-shadow: 0 0 0 rgba(51,255,122,0); }
}
.btnRandom.is-running{
animation: pulseGreen 0.9s ease-in-out infinite;
}
/* Tools */
.toolRowCenter{
display:flex;
justify-content:center;
gap: 10px;
margin-bottom: 10px;
}
.toolBtn{
height: 48px;
width: 66px;
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: 22px;
}
.toolDec{
background: rgba(255,0,0,.18);
border-color: rgba(255,0,0,.35);
}
.toolInc{
background: rgba(51,255,122,.18);
border-color: rgba(51,255,122,.35);
}
.shiftRow{
display:grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
/* Reset full-width + red hover pulse/background */
.btnReset{
width: 100%;
}
@keyframes pulseRedBg {
0% { box-shadow: 0 0 0 rgba(255,0,0,0); background: rgba(255,0,0,.14); }
50% { box-shadow: 0 0 18px rgba(255,0,0,.45), 0 0 28px rgba(255,0,0,.20); background: rgba(255,0,0,.28); }
100% { box-shadow: 0 0 0 rgba(255,0,0,0); background: rgba(255,0,0,.14); }
}
.btnReset:hover{
border-color: rgba(255,0,0,.45);
animation: pulseRedBg 0.9s ease-in-out infinite;
}
/* Bit width control */
.bitWidthRow{ .bitWidthRow{
display: grid; display: grid;
grid-template-columns: 44px 1fr 44px; grid-template-columns: 44px 1fr 44px;
@@ -316,97 +229,98 @@ body{
align-items: center; 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{ .bitInputWrap{
background:rgba(255,255,255,.06); display: grid;
border:1px solid rgba(255,255,255,.14); grid-template-columns: auto 1fr;
border-radius:12px; gap: 10px;
padding:10px 12px;
display:flex;
align-items: center; align-items: center;
justify-content:space-between; padding: 10px 12px;
gap:12px; border: 1px solid rgba(255,255,255,.10);
border-radius: 12px;
background: rgba(255,255,255,.04);
} }
.bitInputLabel{ .bitInputLabel{
color:var(--muted); opacity: .75;
font-size:12px; letter-spacing: .14em;
font-weight:900;
letter-spacing:.18em;
text-transform: uppercase; text-transform: uppercase;
font-size: 11px;
white-space: nowrap;
} }
.bitInput{ .bitInput{
width: 86px; width: 100%;
text-align:right; min-width: 0;
background: transparent; background: transparent;
border: none; border: none;
outline: none; outline: none;
color:var(--accent); color: inherit;
font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size: 20px;
font-size:28px; text-align: right;
}
.bitInput::-webkit-outer-spin-button,
.bitInput::-webkit-inner-spin-button{
-webkit-appearance:none;
margin:0;
} }
/* Bits grid */ .miniBtn{
.bitsWrap{ margin-top: 22px; } 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;
}
.bitsGrid{ /* Buttons */
.controlsRow{
display: grid; display: grid;
gap: 18px; grid-template-columns: 1fr 1fr;
justify-content:center;
grid-template-columns: repeat(8, minmax(92px, 1fr));
padding-top: 18px;
}
.bit{
display:flex;
flex-direction:column;
align-items:center;
gap: 10px; gap: 10px;
padding: 8px 4px; margin-bottom: 10px;
text-align:center;
} }
/* Bulb 25% bigger */ .btn{
.bulb{ border-radius: 12px;
font-size: 32px; /* was ~26 */ border: 1px solid rgba(255,255,255,.12);
line-height: 1; background: rgba(255,255,255,.06);
opacity: .45; color: rgba(255,255,255,.92);
filter: grayscale(1); padding: 12px 12px;
font-weight: 800;
letter-spacing: .10em;
text-transform: uppercase;
cursor: pointer;
} }
.bulb.on{ .btnWide{ width: 100%; }
opacity: 1;
filter: grayscale(0); .btnAccent{
text-shadow: 0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45); background: rgba(0,255,140,.12);
border-color: rgba(0,255,140,.22);
} }
.bitVal{ .toolRowCentered{
font-family:"DSEG7ClassicRegular", ui-monospace, monospace; display: flex;
font-size: 28px; justify-content: center;
color: var(--text); gap: 12px;
opacity: .95; margin: 10px 0 12px;
line-height: 1.05;
min-height: 32px;
/* prevent “run together” when huge values */
white-space: nowrap;
} }
@media (max-width: 980px){ .toolBtn{
.topGrid{ grid-template-columns: 1fr; } width: 56px;
.toolboxToggle{ float:none; } 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);
} }