Files
computing-box/src/components/tools/BinarySimulator.astro
Alexander Lyall 50829688e3 feat(binary): add full binary simulator with unsigned and two’s complement modes
- Introduce new Binary Simulator page with adjustable bit width (4–16 bits)
- Support unsigned and two’s complement representations with live conversion
- Add left/right shift operations and custom binary/denary input
- Implement accessible bulb-and-switch UI with MD3-inspired styling
- Add seven-segment display font assets for realistic number output
- Establish shared base layout, styles, and tooling for future simulators

Signed-off-by: Alexander Lyall <alex@adcm.uk>
2025-12-14 16:57:31 +00:00

382 lines
9.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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);
---
<section class="binary-sim" data-mode={mode} data-bits={initialBits}>
<div class="top">
<div class="left-brand">
<!-- Replace with your logo/img -->
<div class="brand-box" aria-hidden="true">Computing:Box</div>
</div>
<div class="display" aria-live="polite">
<div class="label">DENARY</div>
<div id="denaryNumber" class="value">0</div>
<div class="label">BINARY</div>
<div id="binaryNumber" class="value mono">00000000</div>
</div>
<div class="actions">
<button class="btn" type="button" data-action="custom-binary">Custom Binary</button>
<button class="btn" type="button" data-action="custom-denary">Custom Denary</button>
<button class="btn" type="button" data-action="shift-left">Left Shift</button>
<button class="btn" type="button" data-action="shift-right">Right Shift</button>
<div class="bits-control">
<div class="bits-title">Bits</div>
<div class="bits-buttons">
<button class="btn small" type="button" data-action="bits-minus" aria-label="Decrease bits"></button>
<span id="bitsCount" class="bits-count">{initialBits}</span>
<button class="btn small" type="button" data-action="bits-plus" aria-label="Increase bits">+</button>
</div>
</div>
</div>
</div>
<div id="bitsRow" class="bits-row" aria-label="Binary bit switches"></div>
</section>
<script is:inline>
(() => {
const root = document.currentScript.closest(".binary-sim");
const bitsRow = root.querySelector("#bitsRow");
const binaryEl = root.querySelector("#binaryNumber");
const denaryEl = root.querySelector("#denaryNumber");
const bitsCountEl = root.querySelector("#bitsCount");
let bitCount = parseInt(root.dataset.bits || "8", 10);
const minBits = 4;
const maxBits = 16;
// state: array of 0/1, MSB at index 0
let bits = new Array(bitCount).fill(0);
function placeValues(n) {
// unsigned place values: [2^(n-1), ..., 2^0]
return Array.from({ length: n }, (_, i) => 2 ** (n - 1 - i));
}
function bitsToDenary(bitsArr) {
const pv = placeValues(bitsArr.length);
return bitsArr.reduce((acc, b, i) => acc + (b ? pv[i] : 0), 0);
}
function render() {
const binaryStr = bits.join("");
binaryEl.textContent = binaryStr;
denaryEl.textContent = String(bitsToDenary(bits));
bitsCountEl.textContent = String(bitCount);
const pv = placeValues(bitCount);
bitsRow.innerHTML = pv.map((value, i) => {
const id = `bit_${bitCount}_${i}`;
const checked = bits[i] === 1 ? "checked" : "";
return `
<div class="bit-col">
<div class="bulb ${bits[i] ? "on" : "off"}" aria-hidden="true"></div>
<div class="place">${value}</div>
<label class="switch" for="${id}">
<input id="${id}" type="checkbox" ${checked} data-index="${i}">
<span class="slider" aria-hidden="true"></span>
<span class="sr-only">Toggle bit value ${value}</span>
</label>
</div>
`;
}).join("");
}
function setBitsFromBinaryString(str) {
// allow shorter input; pad left
const clean = (str || "").trim();
if (!/^[01]+$/.test(clean) || clean.length > bitCount) return false;
const padded = clean.padStart(bitCount, "0");
bits = padded.split("").map(c => c === "1" ? 1 : 0);
return true;
}
function setBitsFromDenary(num) {
if (!Number.isInteger(num)) return false;
const max = (2 ** bitCount) - 1;
if (num < 0 || num > max) return false;
const bin = num.toString(2).padStart(bitCount, "0");
bits = bin.split("").map(c => c === "1" ? 1 : 0);
return true;
}
function shiftLeft() {
bits = bits.slice(1).concat(0);
}
function shiftRight() {
bits = [0].concat(bits.slice(0, -1));
}
function resizeBits(newCount) {
const clamped = Math.max(minBits, Math.min(maxBits, newCount));
if (clamped === bitCount) return;
// keep value as best as possible: preserve LSBs when resizing
const currentBinary = bits.join("");
bitCount = clamped;
bits = new Array(bitCount).fill(0);
const take = currentBinary.slice(-bitCount);
setBitsFromBinaryString(take);
}
// Switch input handler
bitsRow.addEventListener("change", (e) => {
const input = e.target;
if (!(input instanceof HTMLInputElement)) return;
const idx = parseInt(input.dataset.index || "-1", 10);
if (idx < 0) return;
bits[idx] = input.checked ? 1 : 0;
render();
});
// Button actions
root.querySelector(".actions").addEventListener("click", (e) => {
const btn = e.target.closest("button[data-action]");
if (!btn) return;
const action = btn.dataset.action;
if (action === "custom-binary") {
const entered = prompt(`Enter a ${bitCount}-bit binary value (0s and 1s):`, bits.join(""));
if (entered === null) return;
if (!setBitsFromBinaryString(entered)) {
alert(`Invalid binary. Use only 0 and 1, up to ${bitCount} digits.`);
return;
}
render();
}
if (action === "custom-denary") {
const max = (2 ** bitCount) - 1;
const entered = prompt(`Enter a denary value (0 to ${max}):`, String(bitsToDenary(bits)));
if (entered === null) return;
const num = Number.parseInt(entered, 10);
if (!setBitsFromDenary(num)) {
alert(`Invalid denary. Enter a whole number from 0 to ${max}.`);
return;
}
render();
}
if (action === "shift-left") { shiftLeft(); render(); }
if (action === "shift-right") { shiftRight(); render(); }
if (action === "bits-minus") { resizeBits(bitCount - 1); render(); }
if (action === "bits-plus") { resizeBits(bitCount + 1); render(); }
});
// initial
render();
})();
</script>
<style>
/* Layout */
.binary-sim {
padding: 2rem 1rem 3rem;
}
.top {
display: grid;
grid-template-columns: 260px 1fr 220px;
gap: 1.5rem;
align-items: start;
}
@media (max-width: 980px) {
.top {
grid-template-columns: 1fr;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
/* Brand placeholder */
.brand-box {
width: 180px;
height: 180px;
border-radius: 12px;
display: grid;
place-items: center;
font-weight: 700;
opacity: 0.9;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.04);
}
/* Display */
.display {
text-align: center;
padding: 1rem;
}
.label {
letter-spacing: 0.12em;
opacity: 0.85;
margin-top: 0.5rem;
}
.value {
font-size: 3rem;
line-height: 1.1;
margin: 0.25rem 0 0.75rem;
font-weight: 700;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* Buttons */
.actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.btn {
border: 0;
border-radius: 10px;
padding: 0.75rem 1rem;
font-weight: 600;
cursor: pointer;
background: rgba(255,255,255,0.08);
color: inherit;
}
.btn:hover { background: rgba(255,255,255,0.12); }
.btn.small {
padding: 0.45rem 0.75rem;
border-radius: 10px;
}
.bits-control {
margin-top: 0.25rem;
padding-top: 0.5rem;
border-top: 1px solid rgba(255,255,255,0.08);
}
.bits-title { opacity: 0.8; margin-bottom: 0.35rem; }
.bits-buttons {
display: flex;
align-items: center;
gap: 0.5rem;
}
.bits-count {
min-width: 2ch;
text-align: center;
font-weight: 700;
}
/* Bits row */
.bits-row {
margin-top: 2rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
gap: 1.25rem;
align-items: end;
}
.bit-col {
text-align: center;
padding: 0.5rem 0.25rem;
}
.place {
margin: 0.5rem 0 0.5rem;
font-size: 2rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* Bulb */
.bulb {
width: 26px;
height: 26px;
margin: 0 auto 0.25rem;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.15);
}
.bulb.off { opacity: 0.15; }
.bulb.on { opacity: 1; }
/* Light switch (accessible checkbox) */
.switch {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 36px;
position: relative;
}
.switch input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.slider {
width: 64px;
height: 36px;
border-radius: 999px;
background: rgba(255,255,255,0.10);
border: 1px solid rgba(255,255,255,0.14);
position: relative;
transition: transform 120ms ease, background 120ms ease;
}
.slider::after {
content: "";
position: absolute;
top: 4px;
left: 4px;
width: 28px;
height: 28px;
border-radius: 999px;
background: rgba(255,255,255,0.85);
transition: transform 120ms ease;
}
.switch input:checked + .slider {
background: rgba(255,255,255,0.18);
}
.switch input:checked + .slider::after {
transform: translateX(28px);
}
/* focus */
.switch input:focus-visible + .slider {
outline: 3px solid rgba(255,255,255,0.35);
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,0,0);
white-space: nowrap; border: 0;
}
</style>