You've already forked computing-box
Snapshot as lots broken. Taken snapshot before reverting commits
This commit is contained in:
1017
public/images/favicon.svg
Normal file
1017
public/images/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 732 KiB |
8
src/components/site-footer.astro
Normal file
8
src/components/site-footer.astro
Normal file
@@ -0,0 +1,8 @@
|
||||
<footer class="site-footer">
|
||||
<div class="site-footer__inner">
|
||||
<div class="site-footer__text">
|
||||
COMPUTER SCIENCE CONCEPT SIMULATORS<br />
|
||||
© 2025 COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
25
src/components/site-header.astro
Normal file
25
src/components/site-header.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
const nav = [
|
||||
{ href: "/about", label: "About" },
|
||||
{ href: "/binary", label: "Binary" },
|
||||
{ href: "/hexadecimal", label: "Hexadecimal" },
|
||||
{ href: "/hex-colours", label: "Hex Colours" },
|
||||
{ href: "/logic-gates", label: "Logic Gates" },
|
||||
];
|
||||
---
|
||||
<header class="site-header">
|
||||
<div class="site-header__inner">
|
||||
<a class="site-header__brand" href="/" aria-label="Computing:Box home">
|
||||
<img class="site-header__logo" src="/img/logo.png" alt="" width="26" height="26" />
|
||||
<span class="site-header__name">COMPUTING:BOX</span>
|
||||
</a>
|
||||
|
||||
<nav class="site-header__nav" aria-label="Primary">
|
||||
{nav.map((i) => (
|
||||
<a class="site-header__link" href={i.href}>
|
||||
{i.label.toUpperCase()}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -4,23 +4,21 @@ import "../styles/binary.css";
|
||||
---
|
||||
|
||||
<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">
|
||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||
<span class="toolboxText">TOOLBOX</span>
|
||||
</button>
|
||||
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||
<span class="toolboxLabel">TOOLBOX</span>
|
||||
</button>
|
||||
|
||||
<main class="wrap">
|
||||
<section class="topGrid">
|
||||
<!-- LEFT -->
|
||||
<div class="mainCol">
|
||||
<div>
|
||||
<div class="readout">
|
||||
<div class="label">Denary</div>
|
||||
<div id="denaryNumber" class="num denaryValue">0</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">0000 0000</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
@@ -30,22 +28,19 @@ import "../styles/binary.css";
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT (Toolbox panel) -->
|
||||
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
||||
<!-- Settings -->
|
||||
<!-- RIGHT TOOLBOX -->
|
||||
<aside id="toolbox" class="panelCol" aria-label="Toolbox">
|
||||
<!-- SETTINGS -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Settings</div>
|
||||
|
||||
<div class="toggleRow">
|
||||
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
|
||||
|
||||
<label class="switch" aria-label="Toggle mode">
|
||||
<input id="modeToggle" type="checkbox" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
|
||||
<!-- keep this on ONE line -->
|
||||
<div class="toggleLabel" id="lblTwos">Two's complement</div>
|
||||
<div class="toggleLabel" id="lblTwos">Two’s complement</div>
|
||||
</div>
|
||||
|
||||
<div class="hint" id="modeHint">
|
||||
@@ -54,7 +49,6 @@ import "../styles/binary.css";
|
||||
|
||||
<div class="subCard">
|
||||
<div class="subTitle">Bit width</div>
|
||||
|
||||
<div class="bitWidthRow">
|
||||
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
||||
|
||||
@@ -78,34 +72,37 @@ import "../styles/binary.css";
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Number -->
|
||||
<!-- CUSTOM -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Custom Number</div>
|
||||
<div class="cardTitle">Custom</div>
|
||||
|
||||
<div class="controlsRow">
|
||||
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||
<div class="twoBtnRow">
|
||||
<button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||
<button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
|
||||
<button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
|
||||
Random
|
||||
</button>
|
||||
|
||||
<div class="hint">Random runs briefly then stops automatically.</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools -->
|
||||
<!-- TOOLS -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Tools</div>
|
||||
|
||||
<div class="toolRowCentered">
|
||||
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||
<div class="toolsTopRow">
|
||||
<button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||
<button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||
</div>
|
||||
|
||||
<div class="toolRow2">
|
||||
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
|
||||
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
|
||||
<div class="twoBtnRow">
|
||||
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
|
||||
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
||||
<button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
@@ -34,11 +34,8 @@
|
||||
----------------------------- */
|
||||
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
||||
let bits = new Array(bitCount).fill(false);
|
||||
let randomTimer = null;
|
||||
|
||||
// For responsive wrapping of the top binary display
|
||||
let nibblesPerLine = null;
|
||||
let wrapMeasureSpan = null;
|
||||
let randomTimer = null;
|
||||
|
||||
/* -----------------------------
|
||||
HELPERS
|
||||
@@ -83,7 +80,6 @@
|
||||
function unsignedBigIntToBits(vUnsigned) {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const v = ((vUnsigned % span) + span) % span;
|
||||
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
|
||||
}
|
||||
@@ -102,6 +98,16 @@
|
||||
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() {
|
||||
if (!modeHint) return;
|
||||
modeHint.textContent = isTwosMode()
|
||||
@@ -109,72 +115,6 @@
|
||||
: "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)
|
||||
----------------------------- */
|
||||
@@ -188,17 +128,9 @@
|
||||
|
||||
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--) {
|
||||
const bitEl = document.createElement("div");
|
||||
bitEl.className = "bit";
|
||||
|
||||
bitEl.innerHTML = `
|
||||
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
||||
<div class="bitVal" id="bitLabel-${i}"></div>
|
||||
@@ -207,7 +139,6 @@
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
`;
|
||||
|
||||
bitsGrid.appendChild(bitEl);
|
||||
}
|
||||
|
||||
@@ -219,28 +150,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -252,8 +161,10 @@
|
||||
const label = document.getElementById(`bitLabel-${i}`);
|
||||
if (!label) continue;
|
||||
|
||||
// Keep label on ONE LINE (no wrapping)
|
||||
label.style.whiteSpace = "nowrap";
|
||||
|
||||
if (isTwosMode() && i === bitCount - 1) {
|
||||
// Keep on one line (CSS: white-space:nowrap)
|
||||
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
||||
} else {
|
||||
label.textContent = pow2Big(i).toString();
|
||||
@@ -272,25 +183,14 @@
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const bulb = document.getElementById(`bulb-${i}`);
|
||||
if (!bulb) continue;
|
||||
|
||||
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";
|
||||
}
|
||||
bulb.classList.toggle("on", bits[i] === true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateReadout() {
|
||||
if (!denaryEl || !binaryEl) return;
|
||||
|
||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
||||
binaryEl.textContent = formatBinaryWrapped();
|
||||
binaryEl.textContent = formatBinaryGrouped();
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
@@ -302,18 +202,16 @@
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
SET FROM INPUT
|
||||
INPUT SETTERS
|
||||
----------------------------- */
|
||||
function setFromBinaryString(binStr) {
|
||||
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
||||
if (!/^[01]+$/.test(clean)) return false;
|
||||
|
||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const charFromRight = padded[padded.length - 1 - i];
|
||||
bits[i] = charFromRight === "1";
|
||||
}
|
||||
|
||||
updateUI();
|
||||
return true;
|
||||
}
|
||||
@@ -355,15 +253,13 @@
|
||||
}
|
||||
|
||||
function shiftRight() {
|
||||
if (isTwosMode()) {
|
||||
// arithmetic right shift: keep MSB
|
||||
const msb = bits[bitCount - 1];
|
||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||
bits[bitCount - 1] = msb;
|
||||
} else {
|
||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||
bits[bitCount - 1] = false;
|
||||
}
|
||||
// Unsigned: logical right shift (MSB becomes 0)
|
||||
// Two's complement: arithmetic right shift (MSB preserved)
|
||||
const msb = bits[bitCount - 1];
|
||||
|
||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||
|
||||
bits[bitCount - 1] = isTwosMode() ? msb : false;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@@ -384,7 +280,8 @@
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
|
||||
const v = (bitsToUnsignedBigInt() + 1n) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
@@ -398,16 +295,18 @@
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
|
||||
const v = (bitsToUnsignedBigInt() - 1n + span) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
RANDOM
|
||||
RANDOM (with running pulse + longer run)
|
||||
----------------------------- */
|
||||
function cryptoRandomBigInt(maxExclusive) {
|
||||
if (maxExclusive <= 0n) return 0n;
|
||||
|
||||
const bitLen = maxExclusive.toString(2).length;
|
||||
const byteLen = Math.ceil(bitLen / 8);
|
||||
|
||||
@@ -420,12 +319,13 @@
|
||||
|
||||
const extraBits = BigInt(byteLen * 8 - bitLen);
|
||||
if (extraBits > 0n) x = x >> extraBits;
|
||||
|
||||
if (x < maxExclusive) return x;
|
||||
}
|
||||
}
|
||||
|
||||
function setRandomOnce() {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const span = unsignedMaxExclusive(bitCount); // 2^n
|
||||
const u = cryptoRandomBigInt(span);
|
||||
unsignedBigIntToBits(u);
|
||||
updateUI();
|
||||
@@ -437,8 +337,11 @@
|
||||
randomTimer = null;
|
||||
}
|
||||
|
||||
// pulse while running
|
||||
btnRandom?.classList.add("is-running");
|
||||
|
||||
const start = Date.now();
|
||||
const durationMs = 1125; // (your “~25% longer” vs 900ms)
|
||||
const durationMs = 1125; // 25% longer than 900ms
|
||||
const tickMs = 80;
|
||||
|
||||
randomTimer = setInterval(() => {
|
||||
@@ -446,30 +349,31 @@
|
||||
if (Date.now() - start >= durationMs) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
btnRandom?.classList.remove("is-running");
|
||||
}
|
||||
}, tickMs);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
BIT WIDTH CONTROLS
|
||||
BIT WIDTH
|
||||
----------------------------- */
|
||||
function setBitWidth(n) {
|
||||
buildBits(clampInt(n, 1, 64));
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
TOOLBOX TOGGLE (simple open/close state)
|
||||
TOOLBOX VISIBILITY
|
||||
----------------------------- */
|
||||
function setToolboxOpen(open) {
|
||||
document.body.classList.toggle("toolboxClosed", !open);
|
||||
toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
refreshBinaryWrap(); // width changes when toolbox closes/opens
|
||||
function setToolboxVisible(isVisible) {
|
||||
if (!toolboxPanel) return;
|
||||
toolboxPanel.style.display = isVisible ? "flex" : "none";
|
||||
toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
EVENTS
|
||||
----------------------------- */
|
||||
modeToggle?.addEventListener("change", () => updateUI());
|
||||
modeToggle?.addEventListener("change", updateUI);
|
||||
|
||||
btnCustomBinary?.addEventListener("click", () => {
|
||||
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
|
||||
@@ -480,8 +384,8 @@
|
||||
btnCustomDenary?.addEventListener("click", () => {
|
||||
const v = prompt(
|
||||
isTwosMode()
|
||||
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
|
||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
|
||||
? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):`
|
||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
|
||||
);
|
||||
if (v === null) return;
|
||||
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
||||
@@ -502,15 +406,8 @@
|
||||
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const isOpen = !document.body.classList.contains("toolboxClosed");
|
||||
setToolboxOpen(!isOpen);
|
||||
});
|
||||
|
||||
// Recompute wrapping live when the window size changes
|
||||
let resizeT = null;
|
||||
window.addEventListener("resize", () => {
|
||||
if (resizeT) clearTimeout(resizeT);
|
||||
resizeT = setTimeout(() => refreshBinaryWrap(), 60);
|
||||
const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false";
|
||||
setToolboxVisible(!isOpen);
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
@@ -518,5 +415,5 @@
|
||||
----------------------------- */
|
||||
updateModeHint();
|
||||
buildBits(bitCount);
|
||||
setToolboxOpen(true);
|
||||
setToolboxVisible(true);
|
||||
})();
|
||||
|
||||
1
src/src/assets/astro.svg
Normal file
1
src/src/assets/astro.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
src/src/assets/background.svg
Normal file
1
src/src/assets/background.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
210
src/src/components/Welcome.astro
Normal file
210
src/src/components/Welcome.astro
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
import astroLogo from '../assets/astro.svg';
|
||||
import background from '../assets/background.svg';
|
||||
---
|
||||
|
||||
<div id="container">
|
||||
<img id="background" src={background.src} alt="" fetchpriority="high" />
|
||||
<main>
|
||||
<section id="hero">
|
||||
<a href="https://astro.build"
|
||||
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
|
||||
>
|
||||
<h1>
|
||||
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
|
||||
</h1>
|
||||
<section id="links">
|
||||
<a class="button" href="https://docs.astro.build">Read our docs</a>
|
||||
<a href="https://astro.build/chat"
|
||||
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
|
||||
fill="#111827"></path></svg
|
||||
>
|
||||
<h2>What's New in Astro 5.0?</h2>
|
||||
<p>
|
||||
From content layers to server islands, click to learn more about the new features and
|
||||
improvements in Astro 5.0
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
filter: blur(100px);
|
||||
}
|
||||
|
||||
#container {
|
||||
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
#links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
color: #111827;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
#links a:hover {
|
||||
color: rgb(78, 80, 86);
|
||||
}
|
||||
|
||||
#links a svg {
|
||||
height: 1em;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
color: white;
|
||||
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#links a.button:hover {
|
||||
color: rgb(230, 230, 230);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family:
|
||||
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
||||
monospace;
|
||||
font-weight: normal;
|
||||
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 1em;
|
||||
font-weight: normal;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4b5563;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.006em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
background:
|
||||
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
|
||||
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
border-radius: 16px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
#news {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
max-width: 300px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
backdrop-filter: blur(50px);
|
||||
}
|
||||
|
||||
#news:hover {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
@media screen and (max-height: 368px) {
|
||||
#news {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: block;
|
||||
padding-top: 10%;
|
||||
}
|
||||
|
||||
#links {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
#news {
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
bottom: 2.5rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
src/src/components/simulators/HexSimulator.astro
Normal file
104
src/src/components/simulators/HexSimulator.astro
Normal 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>
|
||||
346
src/src/components/simulators/hex/hex-simulator.css
Normal file
346
src/src/components/simulators/hex/hex-simulator.css
Normal 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; }
|
||||
}
|
||||
232
src/src/components/simulators/hex/hex-simulator.ts
Normal file
232
src/src/components/simulators/hex/hex-simulator.ts
Normal 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) (0–9, A–F).`;
|
||||
dialogInput.placeholder = "A1";
|
||||
dialogInput.inputMode = "text";
|
||||
} else if (mode === "den") {
|
||||
dialogTitle.textContent = "Custom Denary";
|
||||
dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`;
|
||||
dialogInput.placeholder = "42";
|
||||
dialogInput.inputMode = "numeric";
|
||||
} else {
|
||||
dialogTitle.textContent = "Custom Binary";
|
||||
dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`;
|
||||
dialogInput.placeholder = "00101010";
|
||||
dialogInput.inputMode = "text";
|
||||
}
|
||||
|
||||
dialog.showModal();
|
||||
window.setTimeout(() => dialogInput.focus(), 0);
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
dialogMode = null;
|
||||
dialogError.textContent = "";
|
||||
if (dialog.open) dialog.close();
|
||||
}
|
||||
|
||||
function applyDialog(): void {
|
||||
const raw = (dialogInput.value || "").trim();
|
||||
if (!dialogMode) return closeDialog();
|
||||
if (raw.length === 0) return closeDialog();
|
||||
|
||||
const max = maxForDigits(digits);
|
||||
const bits = digits * 4;
|
||||
|
||||
if (dialogMode === "hex") {
|
||||
const v = raw.toUpperCase();
|
||||
if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; }
|
||||
if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; }
|
||||
value = clamp(parseInt(v, 16), 0, max);
|
||||
render();
|
||||
return closeDialog();
|
||||
}
|
||||
|
||||
if (dialogMode === "den") {
|
||||
if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; }
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; }
|
||||
value = clamp(n, 0, max);
|
||||
render();
|
||||
return closeDialog();
|
||||
}
|
||||
|
||||
// bin
|
||||
if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; }
|
||||
if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; }
|
||||
value = clamp(parseInt(raw, 2), 0, max);
|
||||
render();
|
||||
return closeDialog();
|
||||
}
|
||||
|
||||
function applyDigitDelta(i: number, delta: number): void {
|
||||
stopRandom();
|
||||
const hexArr = padHex(value, digits).split("");
|
||||
let v = parseInt(hexArr[i], 16);
|
||||
v = (v + delta) % 16;
|
||||
if (v < 0) v += 16;
|
||||
hexArr[i] = v.toString(16).toUpperCase();
|
||||
value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits));
|
||||
render();
|
||||
}
|
||||
|
||||
// dialog cancel / backdrop
|
||||
dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); });
|
||||
dialog.addEventListener("click", (e) => {
|
||||
const card = dialog.querySelector(".hex-dialog-card");
|
||||
if (card && !card.contains(e.target as Node)) closeDialog();
|
||||
});
|
||||
dialogInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") applyDialog();
|
||||
if (e.key === "Escape") closeDialog();
|
||||
});
|
||||
|
||||
// main click handler
|
||||
root.addEventListener("click", (e) => {
|
||||
const btn = (e.target as HTMLElement).closest<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();
|
||||
118
src/src/layouts/BaseLayout.astro
Normal file
118
src/src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
const { title = "Computing:Box" } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--nav-h: 108px; /* 3x-ish height */
|
||||
--bg: #1f2027;
|
||||
--text: #e8e8ee;
|
||||
--muted: #a9acb8;
|
||||
--line: rgba(255,255,255,.10);
|
||||
}
|
||||
|
||||
body{
|
||||
margin:0;
|
||||
background:var(--bg);
|
||||
color:var(--text);
|
||||
}
|
||||
|
||||
.siteNav{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
height: var(--nav-h);
|
||||
background: rgba(0,0,0,.10);
|
||||
border-bottom: 1px solid var(--line);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.navInner{
|
||||
height: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.brand{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
text-decoration:none;
|
||||
color:var(--text);
|
||||
}
|
||||
|
||||
.brandLogo{
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.brandName{
|
||||
letter-spacing: .12em;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.navLinks{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:18px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
|
||||
.navLinks a{
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
font-weight: 800;
|
||||
letter-spacing: .12em;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.navLinks a:hover{
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.pageWrap{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</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>
|
||||
</html>
|
||||
22
src/src/layouts/Layout.astro
Normal file
22
src/src/layouts/Layout.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro Basics</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
115
src/src/pages/binary.astro
Normal file
115
src/src/pages/binary.astro
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import "../styles/binary.css";
|
||||
---
|
||||
|
||||
<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">
|
||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||
<span class="toolboxText">TOOLBOX</span>
|
||||
</button>
|
||||
|
||||
<section class="topGrid">
|
||||
<!-- LEFT -->
|
||||
<div class="mainCol">
|
||||
<div class="readout">
|
||||
<div class="label">Denary</div>
|
||||
<div id="denaryNumber" class="num denaryValue">0</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>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<section class="bitsWrap" aria-label="Bit switches">
|
||||
<div class="bitsGrid" id="bitsGrid"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT (Toolbox panel) -->
|
||||
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
||||
<!-- Settings -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Settings</div>
|
||||
|
||||
<div class="toggleRow">
|
||||
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
|
||||
|
||||
<label class="switch" aria-label="Toggle mode">
|
||||
<input id="modeToggle" type="checkbox" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
|
||||
<!-- keep this on ONE line -->
|
||||
<div class="toggleLabel" id="lblTwos">Two's complement</div>
|
||||
</div>
|
||||
|
||||
<div class="hint" id="modeHint">
|
||||
Tip: In unsigned binary, all bits represent positive values.
|
||||
</div>
|
||||
|
||||
<div class="subCard">
|
||||
<div class="subTitle">Bit width</div>
|
||||
|
||||
<div class="bitWidthRow">
|
||||
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
||||
|
||||
<div class="bitInputWrap">
|
||||
<div class="bitInputLabel">Bits</div>
|
||||
<input
|
||||
id="bitsInput"
|
||||
class="bitInput"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
min="1"
|
||||
max="64"
|
||||
step="1"
|
||||
value="8"
|
||||
aria-label="Number of bits"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Number -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Custom Number</div>
|
||||
|
||||
<div class="controlsRow">
|
||||
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
|
||||
<div class="hint">Random runs briefly then stops automatically.</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Tools</div>
|
||||
|
||||
<div class="toolRowCentered">
|
||||
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||
</div>
|
||||
|
||||
<div class="toolRow2">
|
||||
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
|
||||
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/src/scripts/binary.js"></script>
|
||||
</BaseLayout>
|
||||
8
src/src/pages/hexadecimal.astro
Normal file
8
src/src/pages/hexadecimal.astro
Normal 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>
|
||||
11
src/src/pages/index.astro
Normal file
11
src/src/pages/index.astro
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Welcome from '../components/Welcome.astro';
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
|
||||
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Welcome />
|
||||
</Layout>
|
||||
522
src/src/scripts/binary.js
Normal file
522
src/src/scripts/binary.js
Normal file
@@ -0,0 +1,522 @@
|
||||
// src/scripts/binary.js
|
||||
// Computing:Box — Binary page logic (Unsigned + Two's Complement)
|
||||
|
||||
(() => {
|
||||
/* -----------------------------
|
||||
DOM
|
||||
----------------------------- */
|
||||
const bitsGrid = document.getElementById("bitsGrid");
|
||||
const denaryEl = document.getElementById("denaryNumber");
|
||||
const binaryEl = document.getElementById("binaryNumber");
|
||||
const bitsInput = document.getElementById("bitsInput");
|
||||
|
||||
const modeToggle = document.getElementById("modeToggle");
|
||||
const modeHint = document.getElementById("modeHint");
|
||||
|
||||
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||
const btnShiftLeft = document.getElementById("btnShiftLeft");
|
||||
const btnShiftRight = document.getElementById("btnShiftRight");
|
||||
|
||||
const btnDec = document.getElementById("btnDec");
|
||||
const btnInc = document.getElementById("btnInc");
|
||||
const btnClear = document.getElementById("btnClear");
|
||||
const btnRandom = document.getElementById("btnRandom");
|
||||
|
||||
const btnBitsUp = document.getElementById("btnBitsUp");
|
||||
const btnBitsDown = document.getElementById("btnBitsDown");
|
||||
|
||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||
const toolboxPanel = document.getElementById("toolboxPanel");
|
||||
|
||||
/* -----------------------------
|
||||
STATE
|
||||
----------------------------- */
|
||||
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
||||
let bits = new Array(bitCount).fill(false);
|
||||
let randomTimer = null;
|
||||
|
||||
// For responsive wrapping of the top binary display
|
||||
let nibblesPerLine = null;
|
||||
let wrapMeasureSpan = null;
|
||||
|
||||
/* -----------------------------
|
||||
HELPERS
|
||||
----------------------------- */
|
||||
function clampInt(n, min, max) {
|
||||
if (!Number.isFinite(n)) return min;
|
||||
return Math.max(min, Math.min(max, Math.trunc(n)));
|
||||
}
|
||||
|
||||
function isTwosMode() {
|
||||
return !!modeToggle?.checked;
|
||||
}
|
||||
|
||||
function pow2Big(n) {
|
||||
return 1n << BigInt(n);
|
||||
}
|
||||
|
||||
function unsignedMaxExclusive(nBits) {
|
||||
return pow2Big(nBits);
|
||||
}
|
||||
|
||||
function unsignedMaxValue(nBits) {
|
||||
return pow2Big(nBits) - 1n;
|
||||
}
|
||||
|
||||
function twosMin(nBits) {
|
||||
return -pow2Big(nBits - 1);
|
||||
}
|
||||
|
||||
function twosMax(nBits) {
|
||||
return pow2Big(nBits - 1) - 1n;
|
||||
}
|
||||
|
||||
function bitsToUnsignedBigInt() {
|
||||
let v = 0n;
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
if (bits[i]) v += pow2Big(i);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function unsignedBigIntToBits(vUnsigned) {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const v = ((vUnsigned % span) + span) % span;
|
||||
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
|
||||
}
|
||||
}
|
||||
|
||||
function bitsToSignedBigIntTwos() {
|
||||
const u = bitsToUnsignedBigInt();
|
||||
const signBit = bits[bitCount - 1] === true;
|
||||
if (!signBit) return u;
|
||||
return u - pow2Big(bitCount);
|
||||
}
|
||||
|
||||
function signedBigIntToBitsTwos(vSigned) {
|
||||
const span = pow2Big(bitCount);
|
||||
let v = ((vSigned % span) + span) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
}
|
||||
|
||||
function updateModeHint() {
|
||||
if (!modeHint) return;
|
||||
modeHint.textContent = isTwosMode()
|
||||
? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."
|
||||
: "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)
|
||||
----------------------------- */
|
||||
function buildBits(count) {
|
||||
bitCount = clampInt(count, 1, 64);
|
||||
if (bitsInput) bitsInput.value = String(bitCount);
|
||||
|
||||
const oldBits = bits.slice();
|
||||
bits = new Array(bitCount).fill(false);
|
||||
for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
|
||||
|
||||
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--) {
|
||||
const bitEl = document.createElement("div");
|
||||
bitEl.className = "bit";
|
||||
|
||||
bitEl.innerHTML = `
|
||||
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
||||
<div class="bitVal" id="bitLabel-${i}"></div>
|
||||
<label class="switch" aria-label="Toggle bit ${i}">
|
||||
<input type="checkbox" data-index="${i}">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
`;
|
||||
|
||||
bitsGrid.appendChild(bitEl);
|
||||
}
|
||||
|
||||
bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
const i = Number(input.dataset.index);
|
||||
bits[i] = input.checked;
|
||||
updateUI();
|
||||
});
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
UI UPDATE
|
||||
----------------------------- */
|
||||
function updateBitLabels() {
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const label = document.getElementById(`bitLabel-${i}`);
|
||||
if (!label) continue;
|
||||
|
||||
if (isTwosMode() && i === bitCount - 1) {
|
||||
// Keep on one line (CSS: white-space:nowrap)
|
||||
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
||||
} else {
|
||||
label.textContent = pow2Big(i).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncSwitchesToBits() {
|
||||
bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
|
||||
const i = Number(input.dataset.index);
|
||||
input.checked = !!bits[i];
|
||||
});
|
||||
}
|
||||
|
||||
function updateBulbs() {
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const bulb = document.getElementById(`bulb-${i}`);
|
||||
if (!bulb) continue;
|
||||
|
||||
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() {
|
||||
if (!denaryEl || !binaryEl) return;
|
||||
|
||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
||||
binaryEl.textContent = formatBinaryWrapped();
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
updateModeHint();
|
||||
updateBitLabels();
|
||||
syncSwitchesToBits();
|
||||
updateBulbs();
|
||||
updateReadout();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
SET FROM INPUT
|
||||
----------------------------- */
|
||||
function setFromBinaryString(binStr) {
|
||||
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
||||
if (!/^[01]+$/.test(clean)) return false;
|
||||
|
||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const charFromRight = padded[padded.length - 1 - i];
|
||||
bits[i] = charFromRight === "1";
|
||||
}
|
||||
|
||||
updateUI();
|
||||
return true;
|
||||
}
|
||||
|
||||
function setFromDenaryInput(vStr) {
|
||||
const raw = String(vStr ?? "").trim();
|
||||
if (!raw) return false;
|
||||
|
||||
let v;
|
||||
try {
|
||||
if (!/^-?\d+$/.test(raw)) return false;
|
||||
v = BigInt(raw);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isTwosMode()) {
|
||||
const min = twosMin(bitCount);
|
||||
const max = twosMax(bitCount);
|
||||
if (v < min || v > max) return false;
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
if (v < 0n) return false;
|
||||
if (v > unsignedMaxValue(bitCount)) return false;
|
||||
unsignedBigIntToBits(v);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
SHIFTS
|
||||
----------------------------- */
|
||||
function shiftLeft() {
|
||||
for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1];
|
||||
bits[0] = false;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function shiftRight() {
|
||||
if (isTwosMode()) {
|
||||
// arithmetic right shift: keep MSB
|
||||
const msb = bits[bitCount - 1];
|
||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||
bits[bitCount - 1] = msb;
|
||||
} else {
|
||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||
bits[bitCount - 1] = false;
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
CLEAR / INC / DEC
|
||||
----------------------------- */
|
||||
function clearAll() {
|
||||
bits.fill(false);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function increment() {
|
||||
if (isTwosMode()) {
|
||||
const min = twosMin(bitCount);
|
||||
const max = twosMax(bitCount);
|
||||
let v = bitsToSignedBigIntTwos() + 1n;
|
||||
if (v > max) v = min;
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
if (isTwosMode()) {
|
||||
const min = twosMin(bitCount);
|
||||
const max = twosMax(bitCount);
|
||||
let v = bitsToSignedBigIntTwos() - 1n;
|
||||
if (v < min) v = max;
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
RANDOM
|
||||
----------------------------- */
|
||||
function cryptoRandomBigInt(maxExclusive) {
|
||||
if (maxExclusive <= 0n) return 0n;
|
||||
const bitLen = maxExclusive.toString(2).length;
|
||||
const byteLen = Math.ceil(bitLen / 8);
|
||||
|
||||
while (true) {
|
||||
const bytes = new Uint8Array(byteLen);
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
let x = 0n;
|
||||
for (const b of bytes) x = (x << 8n) | BigInt(b);
|
||||
|
||||
const extraBits = BigInt(byteLen * 8 - bitLen);
|
||||
if (extraBits > 0n) x = x >> extraBits;
|
||||
if (x < maxExclusive) return x;
|
||||
}
|
||||
}
|
||||
|
||||
function setRandomOnce() {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const u = cryptoRandomBigInt(span);
|
||||
unsignedBigIntToBits(u);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function runRandomBriefly() {
|
||||
if (randomTimer) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const durationMs = 1125; // (your “~25% longer” vs 900ms)
|
||||
const tickMs = 80;
|
||||
|
||||
randomTimer = setInterval(() => {
|
||||
setRandomOnce();
|
||||
if (Date.now() - start >= durationMs) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
}
|
||||
}, tickMs);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
BIT WIDTH CONTROLS
|
||||
----------------------------- */
|
||||
function setBitWidth(n) {
|
||||
buildBits(clampInt(n, 1, 64));
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
TOOLBOX TOGGLE (simple open/close state)
|
||||
----------------------------- */
|
||||
function setToolboxOpen(open) {
|
||||
document.body.classList.toggle("toolboxClosed", !open);
|
||||
toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
refreshBinaryWrap(); // width changes when toolbox closes/opens
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
EVENTS
|
||||
----------------------------- */
|
||||
modeToggle?.addEventListener("change", () => updateUI());
|
||||
|
||||
btnCustomBinary?.addEventListener("click", () => {
|
||||
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
|
||||
if (v === null) return;
|
||||
if (!setFromBinaryString(v)) alert("Invalid binary");
|
||||
});
|
||||
|
||||
btnCustomDenary?.addEventListener("click", () => {
|
||||
const v = prompt(
|
||||
isTwosMode()
|
||||
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
|
||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
|
||||
);
|
||||
if (v === null) return;
|
||||
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
||||
});
|
||||
|
||||
btnShiftLeft?.addEventListener("click", shiftLeft);
|
||||
btnShiftRight?.addEventListener("click", shiftRight);
|
||||
|
||||
btnInc?.addEventListener("click", increment);
|
||||
btnDec?.addEventListener("click", decrement);
|
||||
|
||||
btnClear?.addEventListener("click", clearAll);
|
||||
btnRandom?.addEventListener("click", runRandomBriefly);
|
||||
|
||||
btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1));
|
||||
btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1));
|
||||
|
||||
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const isOpen = !document.body.classList.contains("toolboxClosed");
|
||||
setToolboxOpen(!isOpen);
|
||||
});
|
||||
|
||||
// Recompute wrapping live when the window size changes
|
||||
let resizeT = null;
|
||||
window.addEventListener("resize", () => {
|
||||
if (resizeT) clearTimeout(resizeT);
|
||||
resizeT = setTimeout(() => refreshBinaryWrap(), 60);
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
INIT
|
||||
----------------------------- */
|
||||
updateModeHint();
|
||||
buildBits(bitCount);
|
||||
setToolboxOpen(true);
|
||||
})();
|
||||
342
src/src/styles/binary.css
Normal file
342
src/src/styles/binary.css
Normal file
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
Binary page styles (keeps the last-working simulator markup + binary.js).
|
||||
|
||||
Goals:
|
||||
- Do NOT change any IDs/classes expected by src/scripts/binary.js
|
||||
- Toolbox button toggles the ENTIRE right-hand column via body.toolboxClosed
|
||||
- Fix toolbox button positioning (no overlap, consistent with header container)
|
||||
- Fix spacing/consistency of cards + buttons
|
||||
- Keep binary readout wrapping/bit-width behaviour from JS (\n in output)
|
||||
*/
|
||||
|
||||
:root{
|
||||
--panel-w: 360px;
|
||||
--gap: 22px;
|
||||
}
|
||||
|
||||
/* Page wrapper (inside BaseLayout .pageWrap) */
|
||||
.wrap{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 22px 20px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* Toolbox toggle button (sits below navbar, aligned right, never overlaps) */
|
||||
.toolboxToggle{
|
||||
align-self: flex-end;
|
||||
position: sticky;
|
||||
top: calc(var(--nav-h, 108px) + 14px);
|
||||
z-index: 30;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 40px;
|
||||
padding: 0 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;
|
||||
}
|
||||
.toolboxToggle:hover{ background: rgba(255,255,255,.08); }
|
||||
.toolboxText{
|
||||
letter-spacing: .12em;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* Main layout grid */
|
||||
.topGrid{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr var(--panel-w);
|
||||
gap: var(--gap);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Hide ENTIRE toolbox column when toggled closed */
|
||||
body.toolboxClosed .topGrid{ grid-template-columns: 1fr; }
|
||||
body.toolboxClosed #toolboxPanel{ display: none; }
|
||||
|
||||
.mainCol{ min-width: 0; }
|
||||
|
||||
/* Readout */
|
||||
.readout{
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.label{
|
||||
opacity: .8;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
|
||||
.num{
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
white-space: pre-line; /* allows JS \n wraps */
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.denaryValue{
|
||||
font-size: 54px;
|
||||
margin: 6px 0 10px;
|
||||
}
|
||||
|
||||
.binaryValue{
|
||||
font-size: 56px;
|
||||
margin: 4px 0 18px;
|
||||
}
|
||||
|
||||
.divider{
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,.10);
|
||||
margin: 14px auto 24px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
/* Bits area */
|
||||
.bitsWrap{ padding-top: 6px; }
|
||||
|
||||
.bitsGrid{
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.bitsGrid.bitsFew{ justify-content: center; }
|
||||
|
||||
.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 column */
|
||||
.panelCol{
|
||||
position: sticky;
|
||||
top: calc(var(--nav-h, 108px) + 72px); /* leaves space for sticky toolbox button */
|
||||
align-self: start;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card{
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,.05);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.cardTitle{
|
||||
opacity: .8;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
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{
|
||||
margin-top: 12px;
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
border-radius: 14px;
|
||||
background: rgba(0,0,0,.12);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.subTitle{
|
||||
opacity: .8;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bitWidthRow{
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr 44px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bitInputWrap{
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,.04);
|
||||
}
|
||||
|
||||
.bitInputLabel{
|
||||
opacity: .75;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bitInput{
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
font-size: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.miniBtn{
|
||||
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;
|
||||
}
|
||||
.miniBtn:hover{ background: rgba(255,255,255,.08); }
|
||||
|
||||
/* Buttons */
|
||||
.controlsRow{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn{
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.06);
|
||||
color: rgba(255,255,255,.92);
|
||||
padding: 12px 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .10em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover{ background: rgba(255,255,255,.08); }
|
||||
|
||||
.btnWide{ width: 100%; }
|
||||
|
||||
.btnAccent{
|
||||
background: rgba(0,255,140,.12);
|
||||
border-color: rgba(0,255,140,.22);
|
||||
}
|
||||
.btnAccent:hover{ background: rgba(0,255,140,.16); }
|
||||
|
||||
.toolRowCentered{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 10px 0 12px;
|
||||
}
|
||||
|
||||
.toolBtn{
|
||||
width: 56px;
|
||||
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); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 980px){
|
||||
.topGrid{ grid-template-columns: 1fr; }
|
||||
.panelCol{ position: static; }
|
||||
.toolboxToggle{ position: static; align-self: flex-start; }
|
||||
}
|
||||
85
src/src/styles/global.css
Normal file
85
src/src/styles/global.css
Normal file
@@ -0,0 +1,85 @@
|
||||
: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);
|
||||
}
|
||||
|
||||
*{ 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);
|
||||
}
|
||||
|
||||
.siteHeader{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: rgba(0,0,0,.15);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||
}
|
||||
|
||||
.siteHeaderInner{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 14px 20px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.brand{
|
||||
color: var(--text);
|
||||
text-decoration:none;
|
||||
font-weight: 900;
|
||||
letter-spacing:.02em;
|
||||
}
|
||||
|
||||
.nav{
|
||||
display:flex;
|
||||
gap: 14px;
|
||||
flex-wrap:wrap;
|
||||
justify-content:flex-end;
|
||||
}
|
||||
.nav a{
|
||||
color: var(--muted);
|
||||
text-decoration:none;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
.nav a:hover{ color: var(--text); }
|
||||
|
||||
.siteMain{
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.siteFooter{
|
||||
border-top: 1px solid rgba(255,255,255,.08);
|
||||
margin-top: 32px;
|
||||
background: rgba(0,0,0,.10);
|
||||
}
|
||||
|
||||
.siteFooterInner{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 18px 20px 26px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footerTitle{
|
||||
color: var(--text);
|
||||
opacity:.9;
|
||||
font-weight: 800;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
75
src/src/styles/site.css
Normal file
75
src/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