Tweaks to Binary UI

Signed-off-by: Alexander Lyall <alex@adcm.uk>
This commit is contained in:
2025-12-14 20:00:01 +00:00
parent d4765b3788
commit e6da9c8c98
18 changed files with 304 additions and 1150 deletions

View File

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

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

View File

@@ -1,321 +0,0 @@
// Binary simulator: unsigned + two's complement, 464 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 twos 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();

View File

@@ -1,11 +1,4 @@
/* Binary simulator for Computing:Box
- Wrap bits every 8 (CSS handles layout)
- Bit width 4..64
- Unsigned + Twos 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 = `
<div class="bulb" id="bulb-${i}" aria-hidden="true"></div>
<div class="bitVal">${value}</div>
<label class="switch" aria-label="Toggle bit ${value}">
<input type="checkbox" data-index="${i}">
<span class="slider"></span>
</label>
`;
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 = `
<span class="bulb" id="bulb-${i}" aria-hidden="true">💡</span>
<div class="bitVal">${labelVal}</div>
<label class="switch" aria-label="Toggle bit ${labelVal}">
<input type="checkbox" data-index="${i}">
<span class="slider"></span>
</label>
`;
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 <-> Twos)
----------------------------- */
function setModeTwos(on) {
isTwos = !!on;
modeHint.textContent = isTwos
? "Tip: In twos 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();

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
<footer class="siteFooter">
<div class="siteFooter__inner">
<div>Computer Science Concept Simulators</div>
<div>© 2025 Computing:Box · Created with ❤️ by Mr Lyall</div>
<div>Powered by ADCM Networks</div>
</div>
</footer>

View File

@@ -0,0 +1,13 @@
<header class="siteHeader">
<div class="siteHeader__inner">
<a class="brand" href="/">Computing:Box</a>
<nav class="nav">
<a class="nav__link" href="/binary">Binary</a>
<a class="nav__link" href="/hexadecimal">Hexadecimal</a>
<a class="nav__link" href="/hex-colours">Hex Colours</a>
<a class="nav__link" href="/logic-gates">Logic Gates</a>
<a class="nav__link" href="/about">About</a>
</nav>
</div>
</header>

View File

@@ -1,6 +0,0 @@
<footer class="site-footer">
<div class="site-footer__inner">
<div class="muted">© {new Date().getFullYear()} Computing:Box</div>
<div class="muted">Powered by ADCM Networks</div>
</div>
</footer>

View File

@@ -1,15 +0,0 @@
<header class="site-header">
<div class="site-header__inner">
<a class="brand" href="/">
<span class="brand__name">Computing:Box</span>
</a>
<nav class="nav">
<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>

View File

@@ -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;
---
<!doctype html>
<html lang="en">
<head>
@@ -11,16 +12,18 @@ const { title = "Computing:Box" } = Astro.props;
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{title}</title>
<!-- Global site styles -->
<link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
<SiteHeader />
<!-- Global site styling -->
<link rel="stylesheet" href="/styles/site.css" />
<main class="site-main">
<!-- Page specific styles can be added by each page -->
<slot name="head" />
</head>
<body>
<Header />
<main class="page">
<slot />
</main>
<SiteFooter />
<Footer />
</body>
</html>

View File

@@ -3,12 +3,11 @@ import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Binary | Computing:Box">
<!-- Page-specific CSS -->
<link rel="stylesheet" href="/styles/binary.css" />
<main class="wrap">
<section class="topGrid">
<!-- LEFT: readout + buttons + bits -->
<section class="binaryWrap">
<div class="topGrid">
<!-- LEFT -->
<div>
<div class="readout">
<div class="label">Denary</div>
@@ -27,11 +26,11 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<div class="divider"></div>
<!-- Bits render here (JS builds bulbs + switches) -->
<section class="bits" id="bitsGrid" aria-label="Bit switches"></section>
<!-- Bit rows injected by JS -->
<section id="bitsRows" class="bitsRows" aria-label="Bit switches"></section>
</div>
<!-- RIGHT: mode + bit width -->
<!-- RIGHT -->
<aside class="panelCol">
<div class="card">
<div class="cardTitle">Mode</div>
@@ -39,13 +38,12 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<div class="toggleRow">
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
<!-- SAME SWITCH STYLE as bit switches -->
<label class="switch" aria-label="Toggle mode">
<input id="modeToggle" type="checkbox" />
<span class="slider"></span>
</label>
<div class="toggleLabel" id="lblTwos">Twos complement</div>
<div class="toggleLabel" id="lblTwos">Two's complement</div>
</div>
<div class="hint" id="modeHint">
@@ -61,28 +59,17 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<div class="bitInputWrap">
<div class="bitInputLabel">Bits</div>
<input
id="bitsInput"
class="bitInput"
type="number"
inputmode="numeric"
min="4"
max="64"
step="1"
value="8"
aria-label="Number of bits"
/>
<input id="bitsInput" class="bitInput" type="number" inputmode="numeric" min="4" max="64" step="1" value="8" />
</div>
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
</div>
<div class="hint">Minimum 4 bits, maximum 64 bits. Display wraps every 8 bits.</div>
<div class="hint">Minimum 4 bits, maximum 64 bits.</div>
</div>
</aside>
</section>
</main>
</div>
</section>
<!-- Page-specific JS -->
<script type="module" src="/scripts/binary.js"></script>
</BaseLayout>

View File

@@ -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 (0255)"),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();

View File

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

View File

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

View File

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

View File

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