You've already forked computing-box
@@ -1,7 +1,4 @@
|
||||
---
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
|
||||
const { title = "Computing:Box" } = Astro.props;
|
||||
---
|
||||
|
||||
@@ -11,25 +8,34 @@ const { title = "Computing:Box" } = Astro.props;
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<link rel="stylesheet" href="/src/styles/site.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Header />
|
||||
<main class="page">
|
||||
<header class="site-header">
|
||||
<div class="site-header__inner">
|
||||
<div class="brand">Computing:Box</div>
|
||||
|
||||
<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>
|
||||
|
||||
<main class="site-main">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="site-footer__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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
:global(html, body) {
|
||||
height: 100%;
|
||||
}
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
}
|
||||
.page {
|
||||
min-height: calc(100vh - 64px - 120px); /* header + footer-ish */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,43 +1,30 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import "../styles/binary.css";
|
||||
|
||||
// keeps JS in src/ and lets Vite/Astro bundle it properly
|
||||
const scriptUrl = Astro.resolve("../scripts/binary.js");
|
||||
// ✅ Correct Astro v5 way: bundle script from src/ and get its final URL
|
||||
import binaryScriptUrl from "../scripts/binary.js?url";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Binary | Computing:Box</title>
|
||||
|
||||
<script type="module" src={scriptUrl}></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Your site header/footer are already present in your layout/screenshots.
|
||||
If this page is wrapped by a global Layout, leave it there.
|
||||
If not, keep your existing header/footer includes here. -->
|
||||
|
||||
<main class="wrap">
|
||||
<section class="topGrid">
|
||||
<!-- LEFT: readout + buttons -->
|
||||
<BaseLayout title="Binary | Computing:Box">
|
||||
<section class="binary-wrap">
|
||||
<div class="topGrid">
|
||||
<!-- LEFT: readout + main buttons -->
|
||||
<div>
|
||||
<div class="readout">
|
||||
<div class="label">Denary</div>
|
||||
<div id="denaryNumber" class="num denaryValue">0</div>
|
||||
<div id="denaryNumber" class="num denary">0</div>
|
||||
|
||||
<div class="label">Binary</div>
|
||||
<div id="binaryNumber" class="num binaryValue">0</div>
|
||||
<pre id="binaryNumber" class="num binary" aria-label="Binary value">0</pre>
|
||||
|
||||
<!-- Orange box: custom on one row, shift on next row -->
|
||||
<div class="controls">
|
||||
<div class="controlRow">
|
||||
<button class="btn btnPrimary" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||
<button class="btn btnPrimary" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||
<!-- ORANGE: custom + shifts on separate lines -->
|
||||
<div class="controls controls--twoRows">
|
||||
<div class="controlsRow">
|
||||
<button class="btn btn--green" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||
<button class="btn btn--green" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||
</div>
|
||||
<div class="controlRow">
|
||||
<div class="controlsRow">
|
||||
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
|
||||
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
|
||||
</div>
|
||||
@@ -50,26 +37,8 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
|
||||
<section class="bits" id="bitsGrid" aria-label="Bit switches"></section>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: actions + mode + bit width -->
|
||||
<!-- RIGHT: red buttons go above/below this panel (not in the middle) -->
|
||||
<aside class="panelCol">
|
||||
<!-- Red box: move these buttons to the panel -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Actions</div>
|
||||
|
||||
<div class="actionGrid">
|
||||
<button class="btn" id="btnClear" type="button">Clear</button>
|
||||
|
||||
<button class="btn btnSpin" id="btnDec1" type="button" aria-label="Decrease by 1">−1</button>
|
||||
<button class="btn btnSpin" id="btnInc1" type="button" aria-label="Increase by 1">+1</button>
|
||||
|
||||
<button class="btn" id="btnAutoRandom" type="button">Auto Random</button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
−1/+1 steps the current value by one. Auto Random runs briefly then stops automatically.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="cardTitle">Mode</div>
|
||||
|
||||
@@ -81,7 +50,7 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
|
||||
<div class="toggleLabel" id="lblTwos">Two’s complement</div>
|
||||
<div class="toggleLabel" id="lblTwos">Two's complement</div>
|
||||
</div>
|
||||
|
||||
<div class="hint" id="modeHint">
|
||||
@@ -89,6 +58,20 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RED: extra buttons ABOVE bit width -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Tools</div>
|
||||
<div class="toolsGrid">
|
||||
<button class="btn" id="btnClear" type="button">Clear</button>
|
||||
<button class="btn btn--spin" id="btnMinus1" type="button">−1</button>
|
||||
<button class="btn btn--spin" id="btnPlus1" type="button">+1</button>
|
||||
<button class="btn" id="btnAutoRandom" type="button">Auto Random</button>
|
||||
</div>
|
||||
<div class="hint">
|
||||
Auto Random runs briefly then stops automatically.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="cardTitle">Bit width</div>
|
||||
|
||||
@@ -116,7 +99,8 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
|
||||
<div class="hint">Minimum 1 bit, maximum 64 bits.</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script type="module" src={binaryScriptUrl}></script>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
/* Binary simulator (Unsigned + Two's complement)
|
||||
- bits 1..64
|
||||
- wrap bits every 8 visually (CSS), no scrollbars
|
||||
- bulbs always update in BOTH modes
|
||||
- MSB label shows negative weight in two's complement (e.g. -128)
|
||||
*/
|
||||
|
||||
const bitsGrid = document.getElementById("bitsGrid");
|
||||
const denaryEl = document.getElementById("denaryNumber");
|
||||
const binaryEl = document.getElementById("binaryNumber");
|
||||
@@ -22,122 +15,57 @@ const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||
|
||||
const btnClear = document.getElementById("btnClear");
|
||||
const btnDec1 = document.getElementById("btnDec1");
|
||||
const btnInc1 = document.getElementById("btnInc1");
|
||||
const btnMinus1 = document.getElementById("btnMinus1");
|
||||
const btnPlus1 = document.getElementById("btnPlus1");
|
||||
const btnAutoRandom = document.getElementById("btnAutoRandom");
|
||||
|
||||
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
||||
let isTwos = Boolean(modeToggle?.checked);
|
||||
|
||||
// state is MSB -> LSB
|
||||
let bits = new Array(bitCount).fill(false);
|
||||
|
||||
let bits = new Array(bitCount).fill(false); // MSB at index 0
|
||||
let autoTimer = null;
|
||||
|
||||
function clampInt(n, min, max){
|
||||
n = Number(n);
|
||||
if (!Number.isFinite(n)) return min;
|
||||
n = Math.floor(n);
|
||||
if (n < min) return min;
|
||||
if (n > max) return max;
|
||||
return n;
|
||||
n = Math.trunc(n);
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
function pow2Big(n){
|
||||
// n is number (0..63)
|
||||
return 1n << BigInt(n);
|
||||
}
|
||||
|
||||
function isTwos(){
|
||||
return !!modeToggle?.checked;
|
||||
}
|
||||
|
||||
function getRange(){
|
||||
// returns { min: BigInt, max: BigInt, mod: BigInt }
|
||||
const n = bitCount;
|
||||
const mod = pow2Big(n);
|
||||
if (!isTwos()){
|
||||
return { min: 0n, max: mod - 1n, mod };
|
||||
}
|
||||
// two's: [-2^(n-1), 2^(n-1)-1]
|
||||
if (n === 1){
|
||||
return { min: -1n, max: 0n, mod };
|
||||
}
|
||||
const half = pow2Big(n - 1);
|
||||
return { min: -half, max: half - 1n, mod };
|
||||
}
|
||||
|
||||
function currentValueBig(){
|
||||
// interpret current bits as unsigned or two's complement signed
|
||||
let unsigned = 0n;
|
||||
/* ----------------------------
|
||||
Label values (MSB..LSB)
|
||||
Unsigned: [2^(n-1) ... 1]
|
||||
Two's: [-2^(n-1), 2^(n-2) ... 1]
|
||||
----------------------------- */
|
||||
function getLabelValues(){
|
||||
const vals = [];
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
if (!bits[i]) continue;
|
||||
const shift = BigInt(bitCount - 1 - i);
|
||||
unsigned += 1n << shift;
|
||||
const pow = bitCount - 1 - i;
|
||||
let v = 2 ** pow;
|
||||
if (isTwos && i === 0) v = -v; // ✅ MSB label becomes negative
|
||||
vals.push(v);
|
||||
}
|
||||
|
||||
if (!isTwos()){
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
// signed decode
|
||||
const signBit = bits[0];
|
||||
if (!signBit) return unsigned;
|
||||
|
||||
const { mod } = getRange();
|
||||
return unsigned - mod; // two's complement negative
|
||||
}
|
||||
|
||||
function setFromUnsignedBig(u){
|
||||
// u in [0, 2^n-1]
|
||||
const n = bitCount;
|
||||
for (let i = 0; i < n; i++){
|
||||
const shift = BigInt(n - 1 - i);
|
||||
bits[i] = ((u >> shift) & 1n) === 1n;
|
||||
}
|
||||
}
|
||||
|
||||
function setFromValueBig(v){
|
||||
// v is signed depending on mode. We convert to bit pattern.
|
||||
const { min, max, mod } = getRange();
|
||||
|
||||
if (v < min) v = min;
|
||||
if (v > max) v = max;
|
||||
|
||||
if (!isTwos()){
|
||||
setFromUnsignedBig(v);
|
||||
return;
|
||||
}
|
||||
|
||||
// two's: if negative, add 2^n
|
||||
let u = v;
|
||||
if (u < 0n) u = u + mod;
|
||||
setFromUnsignedBig(u);
|
||||
return vals;
|
||||
}
|
||||
|
||||
function buildBits(){
|
||||
// wrap every 8 bits
|
||||
bitsGrid.style.setProperty("--cols", String(Math.min(8, bitCount)));
|
||||
|
||||
bitsGrid.innerHTML = "";
|
||||
const labelValues = getLabelValues();
|
||||
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
const placePow = bitCount - 1 - i;
|
||||
const unsignedWeight = pow2Big(placePow);
|
||||
|
||||
// label weight depends on mode (MSB negative in two's)
|
||||
let label = unsignedWeight.toString();
|
||||
if (isTwos() && i === 0){
|
||||
label = "-" + unsignedWeight.toString();
|
||||
}
|
||||
|
||||
const bit = document.createElement("div");
|
||||
bit.className = "bit";
|
||||
bit.innerHTML = `
|
||||
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
||||
<div class="bitVal">${label}</div>
|
||||
<label class="switch" aria-label="Toggle bit ${label}">
|
||||
<div class="bitVal num" id="label-${i}">${labelValues[i]}</div>
|
||||
<label class="switch" aria-label="Toggle bit">
|
||||
<input type="checkbox" data-index="${i}">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
`;
|
||||
|
||||
bitsGrid.appendChild(bit);
|
||||
}
|
||||
|
||||
@@ -146,134 +74,175 @@ function buildBits(){
|
||||
input.addEventListener("change", () => {
|
||||
const idx = Number(input.dataset.index);
|
||||
bits[idx] = input.checked;
|
||||
updateReadout();
|
||||
updateUI();
|
||||
});
|
||||
});
|
||||
|
||||
syncUI();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function binaryStringGrouped(){
|
||||
const raw = bits.map(b => (b ? "1" : "0")).join("");
|
||||
// group every 8 from the RIGHT (LSB side) so long widths look sane
|
||||
// Example: 11 bits -> 00000000 000 (as in your screenshot)
|
||||
const groups = [];
|
||||
for (let end = raw.length; end > 0; end -= 8){
|
||||
const start = Math.max(0, end - 8);
|
||||
groups.unshift(raw.slice(start, end));
|
||||
function setLabels(){
|
||||
const labelValues = getLabelValues();
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
const el = document.getElementById(`label-${i}`);
|
||||
if (el) el.textContent = String(labelValues[i]);
|
||||
}
|
||||
return groups.join(" ");
|
||||
}
|
||||
|
||||
function updateModeHint(){
|
||||
if (!modeHint) return;
|
||||
if (isTwos()){
|
||||
function bitsToUnsigned(){
|
||||
let n = 0;
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
if (!bits[i]) continue;
|
||||
const pow = bitCount - 1 - i;
|
||||
n += 2 ** pow;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function bitsToTwos(){
|
||||
// Two's complement interpretation
|
||||
// value = -MSB*2^(n-1) + sum(other set bits)
|
||||
let n = 0;
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
if (!bits[i]) continue;
|
||||
const pow = bitCount - 1 - i;
|
||||
const v = 2 ** pow;
|
||||
if (i === 0) n -= v;
|
||||
else n += v;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function getCurrentValue(){
|
||||
return isTwos ? bitsToTwos() : bitsToUnsigned();
|
||||
}
|
||||
|
||||
function setFromUnsignedValue(n){
|
||||
// clamp to range of bitCount
|
||||
const max = (2 ** bitCount) - 1;
|
||||
n = clampInt(n, 0, max);
|
||||
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
const pow = bitCount - 1 - i;
|
||||
const v = 2 ** pow;
|
||||
if (n >= v){
|
||||
bits[i] = true;
|
||||
n -= v;
|
||||
} else {
|
||||
bits[i] = false;
|
||||
}
|
||||
}
|
||||
syncSwitchesAndBulbs();
|
||||
updateUI(false);
|
||||
}
|
||||
|
||||
function setFromTwosValue(n){
|
||||
// represent in two's complement with bitCount bits:
|
||||
// allowed range: [-2^(n-1), 2^(n-1)-1]
|
||||
const min = -(2 ** (bitCount - 1));
|
||||
const max = (2 ** (bitCount - 1)) - 1;
|
||||
n = clampInt(n, min, max);
|
||||
|
||||
// Convert to unsigned representation modulo 2^bitCount
|
||||
const mod = 2 ** bitCount;
|
||||
let u = ((n % mod) + mod) % mod;
|
||||
|
||||
// then set bits from unsigned u
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
const pow = bitCount - 1 - i;
|
||||
const v = 2 ** pow;
|
||||
if (u >= v){
|
||||
bits[i] = true;
|
||||
u -= v;
|
||||
} else {
|
||||
bits[i] = false;
|
||||
}
|
||||
}
|
||||
syncSwitchesAndBulbs();
|
||||
updateUI(false);
|
||||
}
|
||||
|
||||
function formatBinary(groupsOf = 4){
|
||||
const raw = bits.map(b => (b ? "1" : "0")).join("");
|
||||
// group for readability (keeps your “wrap every 8 bits” layout for switches;
|
||||
// this just formats the readout)
|
||||
let out = "";
|
||||
for (let i = 0; i < raw.length; i++){
|
||||
out += raw[i];
|
||||
const isLast = i === raw.length - 1;
|
||||
if (!isLast && (i + 1) % groupsOf === 0) out += " ";
|
||||
}
|
||||
return out.trim();
|
||||
}
|
||||
|
||||
function syncSwitchesAndBulbs(){
|
||||
// ✅ Bulbs always update (unsigned OR two's)
|
||||
bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => {
|
||||
const idx = Number(input.dataset.index);
|
||||
input.checked = Boolean(bits[idx]);
|
||||
});
|
||||
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
const bulb = document.getElementById(`bulb-${i}`);
|
||||
if (bulb) bulb.classList.toggle("on", Boolean(bits[i]));
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(sync = true){
|
||||
if (sync) syncSwitchesAndBulbs();
|
||||
|
||||
// labels update when mode changes
|
||||
setLabels();
|
||||
|
||||
// readouts
|
||||
const value = getCurrentValue();
|
||||
denaryEl.textContent = String(value);
|
||||
binaryEl.textContent = formatBinary(4);
|
||||
|
||||
// hint
|
||||
if (isTwos){
|
||||
modeHint.textContent = "Tip: In two’s complement, the left-most bit (MSB) represents a negative value.";
|
||||
} else {
|
||||
modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values.";
|
||||
}
|
||||
}
|
||||
|
||||
function updateReadout(){
|
||||
const v = currentValueBig();
|
||||
|
||||
// display
|
||||
denaryEl.textContent = v.toString();
|
||||
binaryEl.textContent = binaryStringGrouped();
|
||||
|
||||
// bulbs update ALWAYS (mode should not affect bulb on/off)
|
||||
for (let i = 0; i < bitCount; i++){
|
||||
const bulb = document.getElementById(`bulb-${i}`);
|
||||
if (bulb) bulb.classList.toggle("on", bits[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function syncUI(){
|
||||
// sync switch positions
|
||||
bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => {
|
||||
const idx = Number(input.dataset.index);
|
||||
input.checked = !!bits[idx];
|
||||
});
|
||||
|
||||
updateReadout();
|
||||
}
|
||||
|
||||
function clearBits(){
|
||||
bits = new Array(bitCount).fill(false);
|
||||
syncUI();
|
||||
}
|
||||
|
||||
function shiftLeft(){
|
||||
// logical left shift: drop MSB, add 0 at LSB
|
||||
/* ----------------------------
|
||||
Controls
|
||||
----------------------------- */
|
||||
btnShiftLeft?.addEventListener("click", () => {
|
||||
// shift left: drop MSB, append 0 to LSB
|
||||
bits.shift();
|
||||
bits.push(false);
|
||||
syncUI();
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
function shiftRight(){
|
||||
// logical right shift: drop LSB, add 0 at MSB
|
||||
btnShiftRight?.addEventListener("click", () => {
|
||||
// shift right: drop LSB, insert 0 at MSB
|
||||
bits.pop();
|
||||
bits.unshift(false);
|
||||
syncUI();
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
function setFromBinaryPrompt(){
|
||||
const v = prompt(`Enter binary (${bitCount} bits). Spaces allowed:`);
|
||||
if (v === null) return;
|
||||
btnClear?.addEventListener("click", () => {
|
||||
bits = new Array(bitCount).fill(false);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
const clean = String(v).replace(/\s+/g, "");
|
||||
if (!/^[01]+$/.test(clean)){
|
||||
alert("Invalid input. Use only 0 and 1 (spaces allowed).");
|
||||
return;
|
||||
}
|
||||
btnMinus1?.addEventListener("click", () => {
|
||||
const v = getCurrentValue();
|
||||
if (isTwos) setFromTwosValue(v - 1);
|
||||
else setFromUnsignedValue(v - 1);
|
||||
});
|
||||
|
||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
||||
bits = [...padded].map(ch => ch === "1");
|
||||
syncUI();
|
||||
}
|
||||
btnPlus1?.addEventListener("click", () => {
|
||||
const v = getCurrentValue();
|
||||
if (isTwos) setFromTwosValue(v + 1);
|
||||
else setFromUnsignedValue(v + 1);
|
||||
});
|
||||
|
||||
function setFromDenaryPrompt(){
|
||||
const v = prompt(`Enter denary (${isTwos() ? "signed" : "unsigned"}).`);
|
||||
if (v === null) return;
|
||||
|
||||
// BigInt parse (handles negatives)
|
||||
let n;
|
||||
try {
|
||||
n = BigInt(String(v).trim());
|
||||
} catch {
|
||||
alert("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
setFromValueBig(n);
|
||||
syncUI();
|
||||
}
|
||||
|
||||
function stepBy(delta){
|
||||
const v = currentValueBig();
|
||||
const next = v + BigInt(delta);
|
||||
|
||||
// wrap within valid range
|
||||
const { min, max, mod } = getRange();
|
||||
|
||||
let wrapped = next;
|
||||
if (!isTwos()){
|
||||
// unsigned wrap: modulo 2^n
|
||||
wrapped = ((next % mod) + mod) % mod;
|
||||
} else {
|
||||
// signed wrap across [min..max]
|
||||
const span = max - min + 1n; // equals 2^n
|
||||
wrapped = next;
|
||||
while (wrapped > max) wrapped -= span;
|
||||
while (wrapped < min) wrapped += span;
|
||||
}
|
||||
|
||||
setFromValueBig(wrapped);
|
||||
syncUI();
|
||||
}
|
||||
|
||||
function autoRandomOnce(){
|
||||
// runs briefly then stops automatically
|
||||
btnAutoRandom?.addEventListener("click", () => {
|
||||
// stop if already running
|
||||
if (autoTimer){
|
||||
clearInterval(autoTimer);
|
||||
autoTimer = null;
|
||||
@@ -281,110 +250,98 @@ function autoRandomOnce(){
|
||||
return;
|
||||
}
|
||||
|
||||
btnAutoRandom.textContent = "Auto Random (Running…)";
|
||||
const { min, max, mod } = getRange();
|
||||
btnAutoRandom.textContent = "Stop Random";
|
||||
|
||||
// run briefly then stop automatically
|
||||
const start = Date.now();
|
||||
const durationMs = 1800; // short burst
|
||||
const tickMs = 90;
|
||||
const durationMs = 2200; // auto stop
|
||||
|
||||
autoTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
if (now - start >= durationMs){
|
||||
if (now - start > durationMs){
|
||||
clearInterval(autoTimer);
|
||||
autoTimer = null;
|
||||
btnAutoRandom.textContent = "Auto Random";
|
||||
return;
|
||||
}
|
||||
|
||||
// pick a random unsigned pattern 0..2^n-1 then interpret via mode
|
||||
// (this keeps distribution consistent even for signed mode)
|
||||
const r = randomBigIntBelow(mod);
|
||||
setFromUnsignedBig(r);
|
||||
syncUI();
|
||||
}, tickMs);
|
||||
}
|
||||
// random within correct range for current mode
|
||||
if (isTwos){
|
||||
const min = -(2 ** (bitCount - 1));
|
||||
const max = (2 ** (bitCount - 1)) - 1;
|
||||
const n = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
setFromTwosValue(n);
|
||||
} else {
|
||||
const max = (2 ** bitCount) - 1;
|
||||
const n = Math.floor(Math.random() * (max + 1));
|
||||
setFromUnsignedValue(n);
|
||||
}
|
||||
}, 90);
|
||||
});
|
||||
|
||||
function randomBigIntBelow(maxExclusive){
|
||||
// maxExclusive up to 2^64
|
||||
// Use crypto if available, otherwise fallback (still fine for teaching tool)
|
||||
const n = bitCount;
|
||||
btnCustomBinary?.addEventListener("click", () => {
|
||||
const v = prompt(`Enter a ${bitCount}-bit binary number (0/1):`);
|
||||
if (v === null) return;
|
||||
|
||||
if (globalThis.crypto && crypto.getRandomValues){
|
||||
const bytes = Math.ceil(n / 8);
|
||||
const buf = new Uint8Array(bytes);
|
||||
|
||||
while (true){
|
||||
crypto.getRandomValues(buf);
|
||||
let val = 0n;
|
||||
for (const b of buf){
|
||||
val = (val << 8n) + BigInt(b);
|
||||
const clean = v.replace(/\s+/g, "");
|
||||
if (!/^[01]+$/.test(clean)){
|
||||
alert("Invalid binary. Use only 0 and 1.");
|
||||
return;
|
||||
}
|
||||
|
||||
// mask extra bits
|
||||
const extra = BigInt(bytes * 8 - n);
|
||||
if (extra > 0n) val = val & ((1n << BigInt(n)) - 1n);
|
||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
||||
bits = [...padded].map(ch => ch === "1");
|
||||
updateUI();
|
||||
});
|
||||
|
||||
if (val < maxExclusive) return val;
|
||||
}
|
||||
btnCustomDenary?.addEventListener("click", () => {
|
||||
const v = prompt(isTwos
|
||||
? `Enter a denary number (${-(2 ** (bitCount - 1))} to ${(2 ** (bitCount - 1)) - 1}):`
|
||||
: `Enter a denary number (0 to ${(2 ** bitCount) - 1}):`
|
||||
);
|
||||
if (v === null) return;
|
||||
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || !Number.isInteger(n)){
|
||||
alert("Invalid denary. Enter a whole number.");
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback
|
||||
const maxNum = Number.MAX_SAFE_INTEGER;
|
||||
let val = BigInt(Math.floor(Math.random() * maxNum));
|
||||
return val % maxExclusive;
|
||||
}
|
||||
if (isTwos) setFromTwosValue(n);
|
||||
else setFromUnsignedValue(n);
|
||||
});
|
||||
|
||||
function setBitCount(nextCount){
|
||||
nextCount = clampInt(nextCount, 1, 64);
|
||||
bitCount = nextCount;
|
||||
/* ----------------------------
|
||||
Mode + Bit width
|
||||
----------------------------- */
|
||||
modeToggle?.addEventListener("change", () => {
|
||||
isTwos = Boolean(modeToggle.checked);
|
||||
// keep the same underlying bit pattern; just reinterpret and relabel
|
||||
updateUI(false);
|
||||
});
|
||||
|
||||
btnBitsUp?.addEventListener("click", () => {
|
||||
bitCount = clampInt(bitCount + 1, 1, 64);
|
||||
bitsInput.value = String(bitCount);
|
||||
|
||||
// preserve current value if possible by re-encoding it into new width
|
||||
const v = currentValueBig();
|
||||
bits = new Array(bitCount).fill(false);
|
||||
setFromValueBig(v);
|
||||
|
||||
buildBits();
|
||||
updateModeHint();
|
||||
}
|
||||
|
||||
function onModeChange(){
|
||||
updateModeHint();
|
||||
|
||||
// rebuild labels so MSB shows negative weight in two's mode
|
||||
// preserve current *bit pattern* (not numeric), because students are toggling interpretation
|
||||
const currentPattern = bits.slice();
|
||||
});
|
||||
|
||||
btnBitsDown?.addEventListener("click", () => {
|
||||
bitCount = clampInt(bitCount - 1, 1, 64);
|
||||
bitsInput.value = String(bitCount);
|
||||
bits = new Array(bitCount).fill(false);
|
||||
buildBits();
|
||||
bits = currentPattern.slice(0, bitCount);
|
||||
});
|
||||
|
||||
// if length changed (shouldn't), pad
|
||||
if (bits.length < bitCount){
|
||||
bits = bits.concat(new Array(bitCount - bits.length).fill(false));
|
||||
}
|
||||
bitsInput?.addEventListener("change", () => {
|
||||
bitCount = clampInt(Number(bitsInput.value), 1, 64);
|
||||
bitsInput.value = String(bitCount);
|
||||
bits = new Array(bitCount).fill(false);
|
||||
buildBits();
|
||||
});
|
||||
|
||||
// rebuild labels again (already done), then resync
|
||||
syncUI();
|
||||
}
|
||||
|
||||
/* ----------------- Hooks ----------------- */
|
||||
btnShiftLeft?.addEventListener("click", shiftLeft);
|
||||
btnShiftRight?.addEventListener("click", shiftRight);
|
||||
btnCustomBinary?.addEventListener("click", setFromBinaryPrompt);
|
||||
btnCustomDenary?.addEventListener("click", setFromDenaryPrompt);
|
||||
|
||||
btnClear?.addEventListener("click", clearBits);
|
||||
btnDec1?.addEventListener("click", () => stepBy(-1));
|
||||
btnInc1?.addEventListener("click", () => stepBy(+1));
|
||||
btnAutoRandom?.addEventListener("click", autoRandomOnce);
|
||||
|
||||
btnBitsUp?.addEventListener("click", () => setBitCount(bitCount + 1));
|
||||
btnBitsDown?.addEventListener("click", () => setBitCount(bitCount - 1));
|
||||
bitsInput?.addEventListener("change", () => setBitCount(Number(bitsInput.value)));
|
||||
|
||||
modeToggle?.addEventListener("change", onModeChange);
|
||||
|
||||
/* ----------------- Init ----------------- */
|
||||
updateModeHint();
|
||||
/* ----------------------------
|
||||
Init
|
||||
----------------------------- */
|
||||
buildBits();
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
:root{
|
||||
--bg: #1f2027;
|
||||
--panel: #22242d;
|
||||
--panel2: rgba(255,255,255,.04);
|
||||
--text: #e8e8ee;
|
||||
--muted: #a9acb8;
|
||||
|
||||
--accent: #33ff7a;
|
||||
--accent-dim: rgba(51,255,122,.15);
|
||||
|
||||
--line: rgba(255,255,255,.12);
|
||||
}
|
||||
|
||||
/* DSEG7ClassicRegular font */
|
||||
@font-face{
|
||||
font-family: "DSEG7ClassicRegular";
|
||||
src:
|
||||
url("/fonts/DSEG7Classic-Regular.woff2") format("woff2"),
|
||||
url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
|
||||
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
|
||||
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"),
|
||||
url("/fonts/DSEG7Classic-Regular.woff") format("woff");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body{
|
||||
margin:0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.wrap{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 20px 60px;
|
||||
.binary-wrap{
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.topGrid{
|
||||
@@ -64,32 +42,41 @@ body{
|
||||
text-shadow: 0 0 18px var(--accent-dim);
|
||||
}
|
||||
|
||||
.denaryValue{
|
||||
font-size: 72px;
|
||||
.denary{
|
||||
font-size: 72px; /* smaller */
|
||||
line-height: 1.0;
|
||||
margin: 6px 0 10px;
|
||||
}
|
||||
|
||||
.binaryValue{
|
||||
font-size: 52px;
|
||||
letter-spacing: .10em;
|
||||
line-height: 1.0;
|
||||
.binary{
|
||||
font-size: 46px; /* smaller */
|
||||
letter-spacing: .12em;
|
||||
line-height: 1.15;
|
||||
margin: 6px 0 14px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
display:inline-block;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.controls{
|
||||
margin-top: 10px;
|
||||
margin-top: 8px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap: 10px;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.controlRow{
|
||||
.controls--twoRows{
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.controlsRow{
|
||||
display:flex;
|
||||
gap: 12px;
|
||||
justify-content:center;
|
||||
flex-wrap:nowrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn{
|
||||
@@ -102,34 +89,37 @@ body{
|
||||
cursor: pointer;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.btn:active{ transform: translateY(1px); }
|
||||
|
||||
.btnPrimary{
|
||||
background: rgba(51,255,122,.18);
|
||||
.btn--green{
|
||||
background: rgba(51,255,122,.16);
|
||||
border-color: rgba(51,255,122,.45);
|
||||
box-shadow: 0 0 0 1px rgba(51,255,122,.10) inset;
|
||||
}
|
||||
|
||||
.btnSpin{
|
||||
.btn--green:hover{
|
||||
background: rgba(51,255,122,.22);
|
||||
}
|
||||
|
||||
.btn--spin{
|
||||
min-width: 120px;
|
||||
font-size: 18px;
|
||||
padding: 12px 10px;
|
||||
font-size: 18px; /* bigger */
|
||||
}
|
||||
|
||||
.divider{
|
||||
margin-top: 26px;
|
||||
margin-top: 22px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.panelCol{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:14px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.card{
|
||||
background: var(--panel2);
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
@@ -154,7 +144,7 @@ body{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toggleLabel{
|
||||
@@ -164,11 +154,11 @@ body{
|
||||
}
|
||||
|
||||
.switch{
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 34px;
|
||||
position:relative;
|
||||
width:56px;
|
||||
height:34px;
|
||||
display:inline-block;
|
||||
flex: 0 0 auto;
|
||||
flex:0 0 auto;
|
||||
}
|
||||
.switch input{
|
||||
opacity:0;
|
||||
@@ -180,19 +170,19 @@ body{
|
||||
inset:0;
|
||||
background: rgba(255,255,255,.10);
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
border-radius: 999px;
|
||||
transition: .18s ease;
|
||||
border-radius:999px;
|
||||
transition:.18s ease;
|
||||
}
|
||||
.slider::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
left: 3px;
|
||||
top: 2px;
|
||||
height:28px;
|
||||
width:28px;
|
||||
left:3px;
|
||||
top:2px;
|
||||
background: rgba(255,255,255,.92);
|
||||
border-radius: 50%;
|
||||
transition: .18s ease;
|
||||
border-radius:50%;
|
||||
transition:.18s ease;
|
||||
}
|
||||
.switch input:checked + .slider{
|
||||
background: rgba(51,255,122,.20);
|
||||
@@ -205,8 +195,8 @@ body{
|
||||
|
||||
.bitWidthRow{
|
||||
display:grid;
|
||||
grid-template-columns: 44px 1fr 44px;
|
||||
gap: 10px;
|
||||
grid-template-columns:44px 1fr 44px;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
@@ -218,69 +208,56 @@ body{
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
color:#fff;
|
||||
cursor:pointer;
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
font-weight:900;
|
||||
font-size:18px;
|
||||
}
|
||||
|
||||
.bitInputWrap{
|
||||
background: rgba(255,255,255,.06);
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius:12px;
|
||||
padding:10px 12px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap: 12px;
|
||||
gap:12px;
|
||||
}
|
||||
|
||||
.bitInputLabel{
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: .18em;
|
||||
text-transform: uppercase;
|
||||
font-size:12px;
|
||||
font-weight:900;
|
||||
letter-spacing:.18em;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
|
||||
.bitInput{
|
||||
width: 86px;
|
||||
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;
|
||||
font-family:"DSEG7ClassicRegular", ui-monospace, monospace;
|
||||
font-size:28px;
|
||||
}
|
||||
.bitInput::-webkit-outer-spin-button,
|
||||
.bitInput::-webkit-inner-spin-button{
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
-webkit-appearance:none;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.actionGrid{
|
||||
display:grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.actionGrid .btn{
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* -------- Bits grid: wrap every 8, centered when <8, no scrollbar -------- */
|
||||
/* Bits: wrap every 8 */
|
||||
.bits{
|
||||
margin-top: 22px;
|
||||
--cols: 8;
|
||||
margin-top: 26px;
|
||||
padding-top: 18px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 92px);
|
||||
display:grid;
|
||||
grid-template-columns: repeat(var(--cols), 90px);
|
||||
justify-content: center; /* centres when < 8 too */
|
||||
gap: 18px;
|
||||
justify-content: center;
|
||||
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
align-items:end;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.bit{
|
||||
@@ -291,45 +268,30 @@ body{
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
/* Bulb: emoji 💡 larger, grey when off, glowing when on */
|
||||
/* Bulb emoji bigger */
|
||||
.bulb{
|
||||
font-size: 28px; /* bigger bulb */
|
||||
font-size: 26px; /* bigger */
|
||||
line-height: 1;
|
||||
filter: grayscale(100%) brightness(.85);
|
||||
opacity: .65;
|
||||
filter: grayscale(1);
|
||||
opacity: .35;
|
||||
transform: translateY(2px);
|
||||
user-select: none;
|
||||
}
|
||||
.bulb.on{
|
||||
filter: none;
|
||||
filter: grayscale(0);
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 18px rgba(255, 216, 107, .55);
|
||||
text-shadow: 0 0 16px rgba(255,216,107,.55);
|
||||
}
|
||||
|
||||
/* Bit place value */
|
||||
.bitVal{
|
||||
font-family: "DSEG7ClassicRegular", ui-monospace, monospace;
|
||||
font-size: 30px;
|
||||
font-size: 28px;
|
||||
color: var(--text);
|
||||
opacity: .95;
|
||||
line-height: 1;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
/* Per-bit switch */
|
||||
.bit .switch{
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 980px){
|
||||
.topGrid{ grid-template-columns: 1fr; }
|
||||
.denaryValue{ font-size: 64px; }
|
||||
.binaryValue{ font-size: 46px; }
|
||||
.btn{ min-width: 140px; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px){
|
||||
.bits{
|
||||
grid-template-columns: repeat(4, 92px);
|
||||
}
|
||||
.denary{ font-size: 62px; }
|
||||
.binary{ font-size: 40px; }
|
||||
}
|
||||
|
||||
75
src/styles/site.css
Normal file
75
src/styles/site.css
Normal file
@@ -0,0 +1,75 @@
|
||||
:root{
|
||||
--bg: #1f2027;
|
||||
--panel: rgba(255,255,255,.04);
|
||||
--panel-border: rgba(255,255,255,.10);
|
||||
--text: #e8e8ee;
|
||||
--muted: #a9acb8;
|
||||
--accent: #33ff7a;
|
||||
--accent-dim: rgba(51,255,122,.15);
|
||||
--line: rgba(255,255,255,.12);
|
||||
}
|
||||
|
||||
*{ box-sizing: border-box; }
|
||||
|
||||
body{
|
||||
margin:0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.site-header{
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
background: rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
.site-header__inner{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 14px 20px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.brand{
|
||||
font-weight: 800;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.nav{
|
||||
display:flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
|
||||
.nav__link{
|
||||
color: var(--muted);
|
||||
text-decoration:none;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.nav__link:hover{ color: var(--text); }
|
||||
|
||||
.site-main{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 20px 40px;
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.site-footer{
|
||||
border-top: 1px solid rgba(255,255,255,.08);
|
||||
background: rgba(0,0,0,.10);
|
||||
}
|
||||
|
||||
.site-footer__inner{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 20px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
Reference in New Issue
Block a user