You've already forked computing-box
Completed Wave 3 features:
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 27s
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 27s
- [X] New User Interface (Responsive) - [X] Two's Compliment Simulator - [X] Extended Binary Simulator (Custom bit sizes) - [X] Unified Binary Simulator (Unsigned & Two's Completment combined) - [X] Extended Hexadecimal Simulator - [X] Unified Hexadecimal Simulator (For GCSE & A Level Specification) - [X] Enhanced Gate Simulator (Truth Table Creator) - [X] Compound Gate Simulator - [ ] Computer Components Simulator Signed-off-by: Alexander Lyall <alex@adcm.uk>
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
// src/scripts/binary.js
|
||||
// Computing:Box — Binary page logic (Unsigned + Two's Complement)
|
||||
|
||||
(() => {
|
||||
/* -----------------------------
|
||||
DOM
|
||||
@@ -12,6 +9,10 @@
|
||||
|
||||
const modeToggle = document.getElementById("modeToggle");
|
||||
const modeHint = document.getElementById("modeHint");
|
||||
|
||||
// Connect the text labels to the JS
|
||||
const lblUnsigned = document.getElementById("lblUnsigned");
|
||||
const lblTwos = document.getElementById("lblTwos");
|
||||
|
||||
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||
@@ -27,14 +28,13 @@
|
||||
const btnBitsDown = document.getElementById("btnBitsDown");
|
||||
|
||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||
const toolboxPanel = document.getElementById("toolboxPanel");
|
||||
const binaryPage = document.getElementById("binaryPage");
|
||||
|
||||
/* -----------------------------
|
||||
STATE
|
||||
----------------------------- */
|
||||
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
||||
let bits = new Array(bitCount).fill(false);
|
||||
|
||||
let randomTimer = null;
|
||||
|
||||
/* -----------------------------
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
|
||||
function unsignedMaxExclusive(nBits) {
|
||||
return pow2Big(nBits);
|
||||
return pow2Big(nBits);
|
||||
}
|
||||
|
||||
function unsignedMaxValue(nBits) {
|
||||
@@ -94,7 +94,8 @@
|
||||
|
||||
function signedBigIntToBitsTwos(vSigned) {
|
||||
const span = pow2Big(bitCount);
|
||||
let v = ((vSigned % span) + span) % span;
|
||||
let v = vSigned;
|
||||
v = ((v % span) + span) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
}
|
||||
|
||||
@@ -102,17 +103,33 @@
|
||||
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 += " ";
|
||||
const posFromLeft = (bitCount - i);
|
||||
if (i !== 0 && posFromLeft % 4 === 0) s += " ";
|
||||
}
|
||||
return s;
|
||||
return s.trimEnd();
|
||||
}
|
||||
|
||||
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.";
|
||||
if (isTwosMode()) {
|
||||
modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value.";
|
||||
} else {
|
||||
modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values.";
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
RESPONSIVE GRID COLS
|
||||
----------------------------- */
|
||||
function computeColsForBitsGrid() {
|
||||
if (!bitsGrid) return;
|
||||
const wrap = bitsGrid.parentElement;
|
||||
if (!wrap) return;
|
||||
|
||||
const width = wrap.getBoundingClientRect().width;
|
||||
const minCell = 100;
|
||||
const cols = clampInt(Math.floor(width / minCell), 1, 12);
|
||||
bitsGrid.style.setProperty("--cols", String(Math.min(cols, bitCount)));
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
@@ -127,18 +144,25 @@
|
||||
for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
|
||||
|
||||
bitsGrid.innerHTML = "";
|
||||
bitsGrid.classList.toggle("bitsFew", bitCount < 8);
|
||||
|
||||
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="bulb" id="bulb-${i}" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
|
||||
</svg>
|
||||
</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);
|
||||
}
|
||||
|
||||
@@ -150,6 +174,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
computeColsForBitsGrid();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@@ -161,14 +186,14 @@
|
||||
const label = document.getElementById(`bitLabel-${i}`);
|
||||
if (!label) continue;
|
||||
|
||||
// Keep label on ONE LINE (no wrapping)
|
||||
label.style.whiteSpace = "nowrap";
|
||||
|
||||
let valStr;
|
||||
if (isTwosMode() && i === bitCount - 1) {
|
||||
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
||||
valStr = `-${pow2Big(bitCount - 1).toString()}`;
|
||||
} else {
|
||||
label.textContent = pow2Big(i).toString();
|
||||
valStr = pow2Big(i).toString();
|
||||
}
|
||||
label.textContent = valStr;
|
||||
label.style.setProperty('--len', valStr.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,12 +214,23 @@
|
||||
|
||||
function updateReadout() {
|
||||
if (!denaryEl || !binaryEl) return;
|
||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
||||
if (isTwosMode()) {
|
||||
denaryEl.textContent = bitsToSignedBigIntTwos().toString();
|
||||
} else {
|
||||
denaryEl.textContent = bitsToUnsignedBigInt().toString();
|
||||
}
|
||||
binaryEl.textContent = formatBinaryGrouped();
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
updateModeHint();
|
||||
|
||||
// Toggle the glowing CSS class on the active mode text
|
||||
if (lblUnsigned && lblTwos) {
|
||||
lblUnsigned.classList.toggle("activeMode", !isTwosMode());
|
||||
lblTwos.classList.toggle("activeMode", isTwosMode());
|
||||
}
|
||||
|
||||
updateBitLabels();
|
||||
syncSwitchesToBits();
|
||||
updateBulbs();
|
||||
@@ -202,20 +238,25 @@
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
INPUT SETTERS
|
||||
SET FROM BINARY STRING
|
||||
----------------------------- */
|
||||
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;
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
SET FROM DENARY INPUT
|
||||
----------------------------- */
|
||||
function setFromDenaryInput(vStr) {
|
||||
const raw = String(vStr ?? "").trim();
|
||||
if (!raw) return false;
|
||||
@@ -224,9 +265,7 @@
|
||||
try {
|
||||
if (!/^-?\d+$/.test(raw)) return false;
|
||||
v = BigInt(raw);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} catch { return false; }
|
||||
|
||||
if (isTwosMode()) {
|
||||
const min = twosMin(bitCount);
|
||||
@@ -234,8 +273,7 @@
|
||||
if (v < min || v > max) return false;
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
if (v < 0n) return false;
|
||||
if (v > unsignedMaxValue(bitCount)) return false;
|
||||
if (v < 0n || v > unsignedMaxValue(bitCount)) return false;
|
||||
unsignedBigIntToBits(v);
|
||||
}
|
||||
|
||||
@@ -247,18 +285,14 @@
|
||||
SHIFTS
|
||||
----------------------------- */
|
||||
function shiftLeft() {
|
||||
for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1];
|
||||
for (let i = bitCount - 1; i >= 1; i--) { bits[i] = bits[i - 1]; }
|
||||
bits[0] = false;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function shiftRight() {
|
||||
// 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];
|
||||
|
||||
for (let i = 0; i < bitCount - 1; i++) { bits[i] = bits[i + 1]; }
|
||||
bits[bitCount - 1] = isTwosMode() ? msb : false;
|
||||
updateUI();
|
||||
}
|
||||
@@ -267,8 +301,9 @@
|
||||
CLEAR / INC / DEC
|
||||
----------------------------- */
|
||||
function clearAll() {
|
||||
bits.fill(false);
|
||||
updateUI();
|
||||
bits = [];
|
||||
if (modeToggle) modeToggle.checked = false;
|
||||
buildBits(8);
|
||||
}
|
||||
|
||||
function increment() {
|
||||
@@ -280,8 +315,7 @@
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const v = (bitsToUnsignedBigInt() + 1n) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
@@ -295,25 +329,22 @@
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const v = (bitsToUnsignedBigInt() - 1n + span) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
RANDOM (with running pulse + longer run)
|
||||
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);
|
||||
|
||||
@@ -325,23 +356,26 @@
|
||||
}
|
||||
|
||||
function setRandomOnce() {
|
||||
const span = unsignedMaxExclusive(bitCount); // 2^n
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const u = cryptoRandomBigInt(span);
|
||||
unsignedBigIntToBits(u);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function setRandomRunning(isRunning) {
|
||||
if (!btnRandom) return;
|
||||
btnRandom.classList.toggle("btnRandomRunning", !!isRunning);
|
||||
}
|
||||
|
||||
function runRandomBriefly() {
|
||||
if (randomTimer) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
}
|
||||
|
||||
// pulse while running
|
||||
btnRandom?.classList.add("is-running");
|
||||
|
||||
setRandomRunning(true);
|
||||
const start = Date.now();
|
||||
const durationMs = 1125; // 25% longer than 900ms
|
||||
const durationMs = 1125;
|
||||
const tickMs = 80;
|
||||
|
||||
randomTimer = setInterval(() => {
|
||||
@@ -349,25 +383,27 @@
|
||||
if (Date.now() - start >= durationMs) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
btnRandom?.classList.remove("is-running");
|
||||
setRandomRunning(false);
|
||||
}
|
||||
}, tickMs);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
BIT WIDTH
|
||||
BIT WIDTH CONTROLS
|
||||
----------------------------- */
|
||||
function setBitWidth(n) {
|
||||
buildBits(clampInt(n, 1, 64));
|
||||
const v = clampInt(n, 1, 64);
|
||||
buildBits(v);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
TOOLBOX VISIBILITY
|
||||
TOOLBOX TOGGLE
|
||||
----------------------------- */
|
||||
function setToolboxVisible(isVisible) {
|
||||
if (!toolboxPanel) return;
|
||||
toolboxPanel.style.display = isVisible ? "flex" : "none";
|
||||
toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
|
||||
function setToolboxCollapsed(collapsed) {
|
||||
if (!binaryPage) return;
|
||||
binaryPage.classList.toggle("toolboxCollapsed", !!collapsed);
|
||||
const expanded = !collapsed;
|
||||
toolboxToggle?.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
@@ -384,8 +420,8 @@
|
||||
btnCustomDenary?.addEventListener("click", () => {
|
||||
const v = prompt(
|
||||
isTwosMode()
|
||||
? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):`
|
||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
|
||||
? `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");
|
||||
@@ -406,8 +442,12 @@
|
||||
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false";
|
||||
setToolboxVisible(!isOpen);
|
||||
const isCollapsed = binaryPage?.classList.contains("toolboxCollapsed");
|
||||
setToolboxCollapsed(!isCollapsed);
|
||||
});
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
computeColsForBitsGrid();
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
@@ -415,5 +455,5 @@
|
||||
----------------------------- */
|
||||
updateModeHint();
|
||||
buildBits(bitCount);
|
||||
setToolboxVisible(true);
|
||||
})();
|
||||
setToolboxCollapsed(false);
|
||||
})();
|
||||
241
src/scripts/hexColours.js
Normal file
241
src/scripts/hexColours.js
Normal file
@@ -0,0 +1,241 @@
|
||||
// src/scripts/hexColours.js
|
||||
// Computing:Box — Hex Colours logic
|
||||
|
||||
(() => {
|
||||
/* -----------------------------
|
||||
DOM
|
||||
----------------------------- */
|
||||
const colorGrid = document.getElementById("colorGrid");
|
||||
const denaryEl = document.getElementById("denaryNumber");
|
||||
const binaryEl = document.getElementById("binaryNumber");
|
||||
const hexEl = document.getElementById("hexNumber");
|
||||
const previewColor = document.getElementById("previewColor");
|
||||
const previewInverted = document.getElementById("previewInverted");
|
||||
|
||||
const btnCustomHex = document.getElementById("btnCustomHex");
|
||||
const btnCustomRGB = document.getElementById("btnCustomRGB");
|
||||
const btnInvert = document.getElementById("btnInvert");
|
||||
const btnRandom = document.getElementById("btnRandom");
|
||||
const btnClear = document.getElementById("btnClear");
|
||||
|
||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||
const colorPage = document.getElementById("colorPage");
|
||||
|
||||
/* -----------------------------
|
||||
STATE
|
||||
----------------------------- */
|
||||
// rgb[0]=Red, rgb[1]=Green, rgb[2]=Blue (Values 0-255)
|
||||
let rgb = [0, 0, 0];
|
||||
let randomTimer = null;
|
||||
|
||||
/* -----------------------------
|
||||
BUILD UI
|
||||
----------------------------- */
|
||||
function buildGrid() {
|
||||
if (!colorGrid) return;
|
||||
colorGrid.innerHTML = "";
|
||||
|
||||
const colorClasses = ['text-red', 'text-green', 'text-blue'];
|
||||
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const group = document.createElement("div");
|
||||
group.className = "colorGroup";
|
||||
|
||||
for (let i = 1; i >= 0; i--) {
|
||||
const col = document.createElement("div");
|
||||
col.className = "hexCol";
|
||||
|
||||
let cardHTML = `
|
||||
<div class="hexCard">
|
||||
<div class="hexCardButtons">
|
||||
<button class="hexCardBtn inc" id="colorInc-${c}-${i}">▲</button>
|
||||
<button class="hexCardBtn dec" id="colorDec-${c}-${i}">▼</button>
|
||||
</div>
|
||||
<div class="hexDigitDisplay num ${colorClasses[c]}" id="colorDisplay-${c}-${i}">0</div>
|
||||
<div class="hexNibbleRow">
|
||||
`;
|
||||
|
||||
for (let j = 3; j >= 0; j--) {
|
||||
cardHTML += `
|
||||
<div class="hexNibbleBit">
|
||||
<div class="bulb hexNibbleBulb" id="colorBulb-${c}-${i}-${j}" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="hexNibbleLabel">${1 << j}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
cardHTML += `
|
||||
</div>
|
||||
</div>
|
||||
<div class="hexColWeight ${colorClasses[c]}">${16 ** i}</div>
|
||||
`;
|
||||
|
||||
col.innerHTML = cardHTML;
|
||||
|
||||
const incBtn = col.querySelector(`#colorInc-${c}-${i}`);
|
||||
const decBtn = col.querySelector(`#colorDec-${c}-${i}`);
|
||||
|
||||
incBtn.addEventListener("click", () => {
|
||||
const weight = 16 ** i;
|
||||
rgb[c] = (rgb[c] + weight) % 256;
|
||||
updateUI();
|
||||
});
|
||||
|
||||
decBtn.addEventListener("click", () => {
|
||||
const weight = 16 ** i;
|
||||
rgb[c] = (rgb[c] - weight + 256) % 256;
|
||||
updateUI();
|
||||
});
|
||||
|
||||
group.appendChild(col);
|
||||
}
|
||||
colorGrid.appendChild(group);
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
UI UPDATE
|
||||
----------------------------- */
|
||||
function updateUI() {
|
||||
if (denaryEl) {
|
||||
denaryEl.innerHTML = `
|
||||
<span class="text-red">${rgb[0]}</span>
|
||||
<span class="text-green">${rgb[1]}</span>
|
||||
<span class="text-blue">${rgb[2]}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
const hexVals = rgb.map(v => v.toString(16).padStart(2, '0').toUpperCase());
|
||||
const fullHexString = `#${hexVals.join('')}`;
|
||||
|
||||
if (hexEl) {
|
||||
hexEl.innerHTML = `
|
||||
<span class="text-red"><span style="color:var(--muted)">#</span>${hexVals[0]}</span>
|
||||
<span class="text-green">${hexVals[1]}</span>
|
||||
<span class="text-blue">${hexVals[2]}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
if (binaryEl) {
|
||||
binaryEl.innerHTML = `
|
||||
<span class="text-red">${rgb[0].toString(2).padStart(8, '0')}</span>
|
||||
<span class="text-green">${rgb[1].toString(2).padStart(8, '0')}</span>
|
||||
<span class="text-blue">${rgb[2].toString(2).padStart(8, '0')}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
if (previewColor) previewColor.style.backgroundColor = fullHexString;
|
||||
|
||||
const invertedHexString = "#" + rgb.map(v => (255 - v).toString(16).padStart(2, '0').toUpperCase()).join('');
|
||||
if (previewInverted) previewInverted.style.backgroundColor = invertedHexString;
|
||||
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const val = rgb[c];
|
||||
const nibbles = [val % 16, Math.floor(val / 16)];
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const display = document.getElementById(`colorDisplay-${c}-${i}`);
|
||||
if (display) display.textContent = nibbles[i].toString(16).toUpperCase();
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const bulb = document.getElementById(`colorBulb-${c}-${i}-${j}`);
|
||||
if (bulb) {
|
||||
const isOn = (nibbles[i] & (1 << j)) !== 0;
|
||||
bulb.classList.toggle("on", isOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
ACTIONS
|
||||
----------------------------- */
|
||||
function clearAll() {
|
||||
rgb = [0, 0, 0];
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function setRandomOnce() {
|
||||
const arr = new Uint8Array(3);
|
||||
crypto.getRandomValues(arr);
|
||||
rgb = [arr[0], arr[1], arr[2]];
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function runRandomBriefly() {
|
||||
if (randomTimer) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
}
|
||||
if (btnRandom) btnRandom.classList.add("btnRandomRunning");
|
||||
|
||||
const start = Date.now();
|
||||
const durationMs = 1125;
|
||||
const tickMs = 80;
|
||||
|
||||
randomTimer = setInterval(() => {
|
||||
setRandomOnce();
|
||||
if (Date.now() - start >= durationMs) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
if (btnRandom) btnRandom.classList.remove("btnRandomRunning");
|
||||
}
|
||||
}, tickMs);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
EVENTS
|
||||
----------------------------- */
|
||||
btnCustomHex?.addEventListener("click", () => {
|
||||
let v = prompt("Enter a 6-character hex code (e.g. FF0055):");
|
||||
if (v === null) return;
|
||||
v = v.replace(/\s+/g, "").replace(/^#/i, "").toUpperCase();
|
||||
|
||||
if (!/^[0-9A-F]{6}$/.test(v)) return alert("Invalid hex code. Please enter exactly 6 hexadecimal characters.");
|
||||
|
||||
rgb = [
|
||||
parseInt(v.substring(0, 2), 16),
|
||||
parseInt(v.substring(2, 4), 16),
|
||||
parseInt(v.substring(4, 6), 16)
|
||||
];
|
||||
updateUI();
|
||||
});
|
||||
|
||||
btnCustomRGB?.addEventListener("click", () => {
|
||||
const v = prompt("Enter R, G, B values (0-255) separated by commas (e.g. 255, 128, 0):");
|
||||
if (v === null) return;
|
||||
|
||||
const parts = v.split(',').map(s => parseInt(s.trim(), 10));
|
||||
if (parts.length !== 3 || parts.some(isNaN) || parts.some(n => n < 0 || n > 255)) {
|
||||
return alert("Invalid input. Please provide three numbers between 0 and 255.");
|
||||
}
|
||||
|
||||
rgb = parts;
|
||||
updateUI();
|
||||
});
|
||||
|
||||
btnInvert?.addEventListener("click", () => {
|
||||
rgb = rgb.map(v => 255 - v);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
btnClear?.addEventListener("click", clearAll);
|
||||
btnRandom?.addEventListener("click", runRandomBriefly);
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const isCollapsed = colorPage?.classList.contains("toolboxCollapsed");
|
||||
colorPage.classList.toggle("toolboxCollapsed", !isCollapsed);
|
||||
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
INIT
|
||||
----------------------------- */
|
||||
buildGrid();
|
||||
updateUI();
|
||||
})();
|
||||
312
src/scripts/hexadecimal.js
Normal file
312
src/scripts/hexadecimal.js
Normal file
@@ -0,0 +1,312 @@
|
||||
// src/scripts/hexadecimal.js
|
||||
// Computing:Box — Hexadecimal page logic
|
||||
|
||||
(() => {
|
||||
/* -----------------------------
|
||||
DOM
|
||||
----------------------------- */
|
||||
const hexGrid = document.getElementById("hexGrid");
|
||||
const denaryEl = document.getElementById("denaryNumber");
|
||||
const binaryEl = document.getElementById("binaryNumber");
|
||||
const hexEl = document.getElementById("hexNumber");
|
||||
const digitsInput = document.getElementById("digitsInput");
|
||||
|
||||
const btnCustomBinary = document.getElementById("btnCustomBinary");
|
||||
const btnCustomDenary = document.getElementById("btnCustomDenary");
|
||||
const btnCustomHex = document.getElementById("btnCustomHex");
|
||||
|
||||
const btnDec = document.getElementById("btnDec");
|
||||
const btnInc = document.getElementById("btnInc");
|
||||
const btnClear = document.getElementById("btnClear");
|
||||
const btnRandom = document.getElementById("btnRandom");
|
||||
|
||||
const btnDigitsUp = document.getElementById("btnDigitsUp");
|
||||
const btnDigitsDown = document.getElementById("btnDigitsDown");
|
||||
|
||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||
const hexPage = document.getElementById("hexPage");
|
||||
|
||||
/* -----------------------------
|
||||
STATE
|
||||
----------------------------- */
|
||||
let hexCount = clampInt(Number(digitsInput?.value ?? 2), 1, 16);
|
||||
let nibbles = new Array(hexCount).fill(0);
|
||||
let randomTimer = null;
|
||||
|
||||
/* -----------------------------
|
||||
HELPERS
|
||||
----------------------------- */
|
||||
function clampInt(n, min, max) {
|
||||
if (!Number.isFinite(n)) return min;
|
||||
return Math.max(min, Math.min(max, Math.trunc(n)));
|
||||
}
|
||||
|
||||
function maxExclusive() {
|
||||
return 1n << BigInt(hexCount * 4);
|
||||
}
|
||||
|
||||
function maxValue() {
|
||||
return maxExclusive() - 1n;
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
let v = 0n;
|
||||
for (let i = 0; i < hexCount; i++) {
|
||||
v += BigInt(nibbles[i]) << BigInt(i * 4);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function setValue(v) {
|
||||
if (v < 0n) return false;
|
||||
if (v > maxValue()) return false;
|
||||
|
||||
for (let i = 0; i < hexCount; i++) {
|
||||
nibbles[i] = Number((v >> BigInt(i * 4)) & 0xFn);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
RESPONSIVE GRID
|
||||
----------------------------- */
|
||||
function computeColsForHexGrid() {
|
||||
if (!hexGrid) return;
|
||||
hexGrid.style.setProperty("--cols", String(Math.min(hexCount, 8)));
|
||||
hexGrid.classList.toggle("bitsFew", hexCount < 4);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
BUILD UI (CARDS + BULBS)
|
||||
----------------------------- */
|
||||
function buildGrid(count) {
|
||||
hexCount = clampInt(count, 1, 16);
|
||||
if (digitsInput) digitsInput.value = String(hexCount);
|
||||
|
||||
const oldNibbles = nibbles.slice();
|
||||
nibbles = new Array(hexCount).fill(0);
|
||||
for (let i = 0; i < Math.min(oldNibbles.length, hexCount); i++) {
|
||||
nibbles[i] = oldNibbles[i];
|
||||
}
|
||||
|
||||
hexGrid.innerHTML = "";
|
||||
|
||||
for (let i = hexCount - 1; i >= 0; i--) {
|
||||
const col = document.createElement("div");
|
||||
col.className = "hexCol";
|
||||
|
||||
let cardHTML = `
|
||||
<div class="hexCard">
|
||||
<div class="hexCardButtons">
|
||||
<button class="hexCardBtn inc" id="hexInc-${i}">▲</button>
|
||||
<button class="hexCardBtn dec" id="hexDec-${i}">▼</button>
|
||||
</div>
|
||||
<div class="hexDigitDisplay num" id="hexDisplay-${i}">0</div>
|
||||
<div class="hexNibbleRow">
|
||||
`;
|
||||
|
||||
for(let j = 3; j >= 0; j--) {
|
||||
cardHTML += `
|
||||
<div class="hexNibbleBit">
|
||||
<div class="bulb hexNibbleBulb" id="hexBulb-${i}-${j}" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="hexNibbleLabel">${1 << j}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
cardHTML += `
|
||||
</div>
|
||||
</div>
|
||||
<div class="hexColWeight">${(1n << BigInt(i * 4)).toString()}</div>
|
||||
`;
|
||||
|
||||
col.innerHTML = cardHTML;
|
||||
|
||||
const incBtn = col.querySelector(`#hexInc-${i}`);
|
||||
const decBtn = col.querySelector(`#hexDec-${i}`);
|
||||
|
||||
incBtn.addEventListener("click", () => {
|
||||
const span = maxExclusive();
|
||||
const weight = 1n << BigInt(i * 4);
|
||||
setValue((getValue() + weight) % span);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
decBtn.addEventListener("click", () => {
|
||||
const span = maxExclusive();
|
||||
const weight = 1n << BigInt(i * 4);
|
||||
setValue((getValue() - weight + span) % span);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
hexGrid.appendChild(col);
|
||||
}
|
||||
|
||||
computeColsForHexGrid();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
UI UPDATE
|
||||
----------------------------- */
|
||||
function updateUI() {
|
||||
const val = getValue();
|
||||
|
||||
if (denaryEl) denaryEl.textContent = val.toString();
|
||||
if (hexEl) hexEl.textContent = val.toString(16).toUpperCase().padStart(hexCount, '0');
|
||||
|
||||
if (binaryEl) {
|
||||
let binStr = "";
|
||||
for (let i = hexCount - 1; i >= 0; i--) {
|
||||
binStr += nibbles[i].toString(2).padStart(4, '0') + " ";
|
||||
}
|
||||
binaryEl.textContent = binStr.trimEnd();
|
||||
}
|
||||
|
||||
for (let i = 0; i < hexCount; i++) {
|
||||
const display = document.getElementById(`hexDisplay-${i}`);
|
||||
if (display) display.textContent = nibbles[i].toString(16).toUpperCase();
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const bulb = document.getElementById(`hexBulb-${i}-${j}`);
|
||||
if (bulb) {
|
||||
const isOn = (nibbles[i] & (1 << j)) !== 0;
|
||||
bulb.classList.toggle("on", isOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
CLEAR / INC / DEC
|
||||
----------------------------- */
|
||||
function clearAll() {
|
||||
nibbles.fill(0);
|
||||
buildGrid(2);
|
||||
}
|
||||
|
||||
function increment() {
|
||||
const span = maxExclusive();
|
||||
setValue((getValue() + 1n) % span);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
const span = maxExclusive();
|
||||
setValue((getValue() - 1n + span) % span);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
RANDOM
|
||||
----------------------------- */
|
||||
function cryptoRandomBigInt(maxExcl) {
|
||||
if (maxExcl <= 0n) return 0n;
|
||||
const bitLen = maxExcl.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 < maxExcl) return x;
|
||||
}
|
||||
}
|
||||
|
||||
function setRandomOnce() {
|
||||
const u = cryptoRandomBigInt(maxExclusive());
|
||||
setValue(u);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function runRandomBriefly() {
|
||||
if (randomTimer) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
}
|
||||
if (btnRandom) btnRandom.classList.add("btnRandomRunning");
|
||||
|
||||
const start = Date.now();
|
||||
const durationMs = 1125;
|
||||
const tickMs = 80;
|
||||
|
||||
randomTimer = setInterval(() => {
|
||||
setRandomOnce();
|
||||
if (Date.now() - start >= durationMs) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
if (btnRandom) btnRandom.classList.remove("btnRandomRunning");
|
||||
}
|
||||
}, tickMs);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
EVENTS
|
||||
----------------------------- */
|
||||
btnCustomHex?.addEventListener("click", () => {
|
||||
const v = prompt(`Enter hexadecimal (0-9, A-F). Current width: ${hexCount} digits`);
|
||||
if (v === null) return;
|
||||
const clean = v.replace(/\s+/g, "").toUpperCase();
|
||||
|
||||
if (!/^[0-9A-F]+$/.test(clean)) return alert("Invalid hexadecimal.");
|
||||
if (clean.length > hexCount) return alert("Value too large for current digit width.");
|
||||
|
||||
if (!setValue(BigInt("0x" + clean))) alert("Value out of range.");
|
||||
else updateUI();
|
||||
});
|
||||
|
||||
btnCustomBinary?.addEventListener("click", () => {
|
||||
const v = prompt(`Enter binary (0, 1). Current width: ${hexCount * 4} bits`);
|
||||
if (v === null) return;
|
||||
const clean = v.replace(/\s+/g, "");
|
||||
|
||||
if (!/^[01]+$/.test(clean)) return alert("Invalid binary.");
|
||||
if (clean.length > hexCount * 4) return alert("Value too large for current digit width.");
|
||||
|
||||
if (!setValue(BigInt("0b" + clean))) alert("Value out of range.");
|
||||
else updateUI();
|
||||
});
|
||||
|
||||
btnCustomDenary?.addEventListener("click", () => {
|
||||
const v = prompt(`Enter denary (0 to ${maxValue().toString()}):`);
|
||||
if (v === null) return;
|
||||
const clean = v.trim();
|
||||
|
||||
if (!/^\d+$/.test(clean)) return alert("Invalid denary. Digits only.");
|
||||
if (!setValue(BigInt(clean))) alert(`Value out of range. Enter a number between 0 and ${maxValue().toString()}.`);
|
||||
else updateUI();
|
||||
});
|
||||
|
||||
btnInc?.addEventListener("click", increment);
|
||||
btnDec?.addEventListener("click", decrement);
|
||||
|
||||
btnClear?.addEventListener("click", clearAll);
|
||||
btnRandom?.addEventListener("click", runRandomBriefly);
|
||||
|
||||
btnDigitsUp?.addEventListener("click", () => buildGrid(hexCount + 1));
|
||||
btnDigitsDown?.addEventListener("click", () => buildGrid(hexCount - 1));
|
||||
|
||||
digitsInput?.addEventListener("change", () => buildGrid(Number(digitsInput.value)));
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const isCollapsed = hexPage?.classList.contains("toolboxCollapsed");
|
||||
hexPage.classList.toggle("toolboxCollapsed", !isCollapsed);
|
||||
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
|
||||
});
|
||||
|
||||
window.addEventListener("resize", computeColsForHexGrid);
|
||||
|
||||
/* -----------------------------
|
||||
INIT
|
||||
----------------------------- */
|
||||
buildGrid(hexCount);
|
||||
|
||||
})();
|
||||
489
src/scripts/logicGates.js
Normal file
489
src/scripts/logicGates.js
Normal file
@@ -0,0 +1,489 @@
|
||||
// src/scripts/logicGates.js
|
||||
// Computing:Box — Drag & Drop Logic Builder
|
||||
|
||||
(() => {
|
||||
/* --- DOM Elements --- */
|
||||
const workspace = document.getElementById("workspace");
|
||||
const wireLayer = document.getElementById("wireLayer");
|
||||
const ttContainer = document.getElementById("truthTableContainer");
|
||||
const toolboxGrid = document.getElementById("toolboxGrid");
|
||||
|
||||
const btnClearBoard = document.getElementById("btnClearBoard");
|
||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||
const logicPage = document.getElementById("logicPage");
|
||||
|
||||
/* --- ANSI Gate SVGs (Strict 100x50 with built-in tails) --- */
|
||||
const GATE_SVGS = {
|
||||
'AND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M70,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/></g>`,
|
||||
'OR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M70,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/></g>`,
|
||||
'NOT': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,25 L30,25 M71,25 L100,25"/><path d="M30,10 L60,25 L30,40 Z" fill="var(--bg)"/><circle cx="65.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
|
||||
'NAND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M80,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/><circle cx="74.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
|
||||
'NOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M80,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/><circle cx="74.5" cy="25" r="4.5" fill="var(--bg)"/></g>`,
|
||||
'XOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L24,15 M0,35 L24,35 M75,25 L100,25"/><path d="M30,5 Q60,5 75,25 Q60,45 30,45 Q50,25 30,5 Z" fill="var(--bg)"/><path d="M20,5 Q40,25 20,45"/></g>`,
|
||||
'XNOR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L24,15 M0,35 L24,35 M85,25 L100,25"/><path d="M30,5 Q60,5 75,25 Q60,45 30,45 Q50,25 30,5 Z" fill="var(--bg)"/><path d="M20,5 Q40,25 20,45"/><circle cx="79.5" cy="25" r="4.5" fill="var(--bg)"/></g>`
|
||||
};
|
||||
|
||||
const INPUT_SVG = `<svg class="lg-line-svg" viewBox="0 0 30 50"><path d="M0,25 L30,25" stroke="#e8e8ee" stroke-width="3" fill="none"/></svg>`;
|
||||
const OUTPUT_SVG = `<svg class="lg-line-svg" viewBox="0 0 30 50"><path d="M0,25 L30,25" stroke="#e8e8ee" stroke-width="3" fill="none"/></svg>`;
|
||||
|
||||
/* --- State --- */
|
||||
let nodes = {};
|
||||
let connections = [];
|
||||
|
||||
let nextNodeId = 1;
|
||||
let nextWireId = 1;
|
||||
let inputCount = 0;
|
||||
let outputCount = 0;
|
||||
|
||||
let isDraggingNode = null;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
let clickStartX = 0, clickStartY = 0; // Fixes switch drag conflict
|
||||
|
||||
let wiringStart = null;
|
||||
let tempWirePath = null;
|
||||
|
||||
let selectedWireId = null;
|
||||
let selectedNodeId = null;
|
||||
|
||||
/* --- Setup Toolbox --- */
|
||||
function initToolbox() {
|
||||
if(!toolboxGrid) return;
|
||||
let html = `
|
||||
<div draggable="true" data-spawn="INPUT" class="drag-item tb-icon-box" title="Input Toggle">
|
||||
<div class="switch" style="pointer-events:none;"><span class="slider"></span></div>
|
||||
<div class="tb-icon-label">Input</div>
|
||||
</div>
|
||||
<div draggable="true" data-spawn="OUTPUT" class="drag-item tb-icon-box" title="Output Bulb">
|
||||
<div class="bulb on" style="pointer-events:none; width:28px; height:28px;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
|
||||
<div class="tb-icon-label">Output</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Object.keys(GATE_SVGS).forEach(gate => {
|
||||
html += `
|
||||
<div draggable="true" data-spawn="GATE" data-gate="${gate}" class="drag-item tb-icon-box" title="${gate} Gate">
|
||||
<svg viewBox="0 0 100 50" style="width:50px; height:25px; pointer-events:none;">${GATE_SVGS[gate]}</svg>
|
||||
<div class="tb-icon-label">${gate}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
toolboxGrid.innerHTML = html;
|
||||
|
||||
document.querySelectorAll('.drag-item').forEach(item => {
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('spawnType', item.dataset.spawn);
|
||||
if(item.dataset.spawn === 'GATE') e.dataTransfer.setData('gateType', item.dataset.gate);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* --- Math & Geometry --- */
|
||||
function getPortCoords(nodeId, portDataAttr) {
|
||||
const node = nodes[nodeId];
|
||||
if (!node || !node.el) return {x:0, y:0};
|
||||
|
||||
const portEl = node.el.querySelector(`[data-port="${portDataAttr}"]`);
|
||||
if (!portEl) return {x:0, y:0};
|
||||
|
||||
const wsRect = workspace.getBoundingClientRect();
|
||||
const portRect = portEl.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: portRect.left - wsRect.left + (portRect.width / 2),
|
||||
y: portRect.top - wsRect.top + (portRect.height / 2)
|
||||
};
|
||||
}
|
||||
|
||||
function drawBezier(x1, y1, x2, y2) {
|
||||
const cpDist = Math.abs(x2 - x1) * 0.6 + 20;
|
||||
return `M ${x1} ${y1} C ${x1 + cpDist} ${y1}, ${x2 - cpDist} ${y2}, ${x2} ${y2}`;
|
||||
}
|
||||
|
||||
/* --- Rendering --- */
|
||||
function renderWires() {
|
||||
let svgHTML = '';
|
||||
|
||||
connections.forEach(conn => {
|
||||
const from = getPortCoords(conn.fromNode, 'out');
|
||||
const to = getPortCoords(conn.toNode, `in${conn.toPort}`);
|
||||
const sourceNode = nodes[conn.fromNode];
|
||||
const isActive = sourceNode && sourceNode.value === true;
|
||||
const isSelected = conn.id === selectedWireId;
|
||||
|
||||
svgHTML += `<path class="lg-wire ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
|
||||
});
|
||||
|
||||
if (wiringStart && tempWirePath) {
|
||||
svgHTML += `<path class="lg-wire lg-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
|
||||
}
|
||||
|
||||
wireLayer.innerHTML = svgHTML;
|
||||
}
|
||||
|
||||
function updateNodePositions() {
|
||||
Object.values(nodes).forEach(n => {
|
||||
if (n.el) {
|
||||
n.el.style.left = `${n.x}px`;
|
||||
n.el.style.top = `${n.y}px`;
|
||||
}
|
||||
});
|
||||
renderWires();
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedWireId = null;
|
||||
selectedNodeId = null;
|
||||
document.querySelectorAll('.lg-node.selected').forEach(el => el.classList.remove('selected'));
|
||||
renderWires();
|
||||
}
|
||||
|
||||
/* --- Logic Evaluation --- */
|
||||
function evaluateGraph(overrideInputs = null) {
|
||||
let context = {};
|
||||
|
||||
Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => {
|
||||
context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value;
|
||||
});
|
||||
|
||||
let changed = true;
|
||||
let loops = 0;
|
||||
|
||||
while (changed && loops < 10) {
|
||||
changed = false;
|
||||
loops++;
|
||||
|
||||
Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => {
|
||||
let in1Conn = connections.find(c => c.toNode === gate.id && c.toPort === '1');
|
||||
let in2Conn = connections.find(c => c.toNode === gate.id && c.toPort === '2');
|
||||
|
||||
let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false;
|
||||
let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false;
|
||||
|
||||
let res = false;
|
||||
switch(gate.gateType) {
|
||||
case 'AND': res = val1 && val2; break;
|
||||
case 'OR': res = val1 || val2; break;
|
||||
case 'NOT': res = !val1; break;
|
||||
case 'NAND': res = !(val1 && val2); break;
|
||||
case 'NOR': res = !(val1 || val2); break;
|
||||
case 'XOR': res = val1 !== val2; break;
|
||||
case 'XNOR': res = val1 === val2; break;
|
||||
}
|
||||
|
||||
if (context[gate.id] !== res) {
|
||||
context[gate.id] = res;
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let outStates = {};
|
||||
Object.values(nodes).filter(n => n.type === 'OUTPUT').forEach(out => {
|
||||
let conn = connections.find(c => c.toNode === out.id);
|
||||
let res = conn ? (context[conn.fromNode] || false) : false;
|
||||
outStates[out.id] = res;
|
||||
});
|
||||
|
||||
if (!overrideInputs) {
|
||||
Object.values(nodes).forEach(n => {
|
||||
if (n.type === 'GATE') n.value = context[n.id] || false;
|
||||
if (n.type === 'OUTPUT') {
|
||||
n.value = outStates[n.id] || false;
|
||||
const bulb = n.el.querySelector('.bulb');
|
||||
if (bulb) bulb.classList.toggle('on', n.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return outStates;
|
||||
}
|
||||
|
||||
/* --- Truth Table Generation --- */
|
||||
function generateTruthTable() {
|
||||
if (!ttContainer) return;
|
||||
|
||||
const inNodes = Object.values(nodes).filter(n => n.type === 'INPUT').sort((a,b) => a.label.localeCompare(b.label));
|
||||
const outNodes = Object.values(nodes).filter(n => n.type === 'OUTPUT').sort((a,b) => a.label.localeCompare(b.label));
|
||||
|
||||
if (inNodes.length === 0 || outNodes.length === 0) {
|
||||
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Add inputs and outputs to generate table.</div>';
|
||||
return;
|
||||
}
|
||||
if (inNodes.length > 6) {
|
||||
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Maximum 6 inputs supported.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<table class="tt-table"><thead><tr>';
|
||||
inNodes.forEach(n => html += `<th>${n.label}</th>`);
|
||||
outNodes.forEach(n => html += `<th style="color:var(--text);">${n.label}</th>`);
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
const numRows = Math.pow(2, inNodes.length);
|
||||
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
let override = {};
|
||||
inNodes.forEach((n, idx) => {
|
||||
override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1;
|
||||
});
|
||||
|
||||
let outStates = evaluateGraph(override);
|
||||
|
||||
html += '<tr>';
|
||||
inNodes.forEach(n => {
|
||||
let val = override[n.id];
|
||||
html += `<td class="${val ? 'tt-on' : ''}">${val ? 1 : 0}</td>`;
|
||||
});
|
||||
outNodes.forEach(n => {
|
||||
let val = outStates[n.id];
|
||||
html += `<td class="${val ? 'tt-on' : ''}" style="font-weight:bold;">${val ? 1 : 0}</td>`;
|
||||
});
|
||||
html += '</tr>';
|
||||
}
|
||||
|
||||
html += '</tbody></table>';
|
||||
ttContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function runSimulation() {
|
||||
evaluateGraph();
|
||||
renderWires();
|
||||
generateTruthTable();
|
||||
}
|
||||
|
||||
/* --- Node Creation --- */
|
||||
function createNodeElement(node) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `lg-node`;
|
||||
el.dataset.id = node.id;
|
||||
el.style.left = `${node.x}px`;
|
||||
el.style.top = `${node.y}px`;
|
||||
|
||||
let innerHTML = `<div class="lg-header">${node.label}</div><div class="lg-gate-container">`;
|
||||
|
||||
if (node.type === 'INPUT') {
|
||||
innerHTML += `
|
||||
<div class="switch" style="margin:0;"><span class="slider"></span></div>
|
||||
${INPUT_SVG}
|
||||
<div class="lg-port port-out" data-port="out" style="top: 25px; left: 86px;"></div>
|
||||
`;
|
||||
}
|
||||
else if (node.type === 'OUTPUT') {
|
||||
innerHTML += `
|
||||
<div class="lg-port port-in-1" data-port="in1" style="top: 25px; left: 0;"></div>
|
||||
${OUTPUT_SVG}
|
||||
<div class="bulb" style="margin:0;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
|
||||
`;
|
||||
}
|
||||
else if (node.type === 'GATE') {
|
||||
const isNot = node.gateType === 'NOT';
|
||||
innerHTML += `
|
||||
<div class="lg-port port-in-1" data-port="in1" style="top: ${isNot ? '25px' : '15px'}; left: 0;"></div>
|
||||
${!isNot ? `<div class="lg-port port-in-2" data-port="in2" style="top: 35px; left: 0;"></div>` : ''}
|
||||
<svg class="lg-gate-svg" viewBox="0 0 100 50">${GATE_SVGS[node.gateType]}</svg>
|
||||
<div class="lg-port port-out" data-port="out" style="top: 25px; left: 100px;"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
innerHTML += `</div>`;
|
||||
el.innerHTML = innerHTML;
|
||||
workspace.appendChild(el);
|
||||
node.el = el;
|
||||
|
||||
if (node.type === 'INPUT') {
|
||||
// Custom click handler to prevent dragging from toggling the switch
|
||||
el.querySelector('.switch').addEventListener('click', (e) => {
|
||||
const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY);
|
||||
if (isDraggingNode || dist > 3) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
node.value = !node.value;
|
||||
el.querySelector('.switch').classList.toggle('active-sim', node.value);
|
||||
el.querySelector('.slider').style.background = node.value ? 'rgba(40,240,122,.25)' : '';
|
||||
el.querySelector('.slider').style.borderColor = node.value ? 'rgba(40,240,122,.30)' : '';
|
||||
el.querySelector('.slider').innerHTML = node.value ? `<style>#${node.id} .slider::before { transform: translateX(28px); }</style>` : '';
|
||||
runSimulation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function spawnNode(type, gateType = null, dropX = null, dropY = null) {
|
||||
let label = '';
|
||||
if (type === 'INPUT') { inputCount++; label = String.fromCharCode(64 + inputCount); }
|
||||
if (type === 'OUTPUT') { outputCount++; label = `Q${outputCount}`; }
|
||||
if (type === 'GATE') { label = gateType; }
|
||||
|
||||
const id = `node_${nextNodeId++}`;
|
||||
|
||||
const offset = Math.floor(Math.random() * 40);
|
||||
const x = dropX !== null ? dropX : (type === 'INPUT' ? 50 : (type === 'OUTPUT' ? 600 : 300) + offset);
|
||||
const y = dropY !== null ? dropY : 150 + offset;
|
||||
|
||||
const node = { id, type, gateType, label, x, y, value: false, el: null };
|
||||
nodes[id] = node;
|
||||
createNodeElement(node);
|
||||
runSimulation();
|
||||
}
|
||||
|
||||
/* --- Global Interaction Handlers --- */
|
||||
|
||||
workspace.addEventListener('mousedown', (e) => {
|
||||
clickStartX = e.clientX;
|
||||
clickStartY = e.clientY;
|
||||
|
||||
const port = e.target.closest('.lg-port');
|
||||
if (port) {
|
||||
const nodeEl = port.closest('.lg-node');
|
||||
const portId = port.dataset.port;
|
||||
|
||||
if (portId.startsWith('in')) {
|
||||
const existingIdx = connections.findIndex(c => c.toNode === nodeEl.dataset.id && c.toPort === portId.replace('in', ''));
|
||||
if (existingIdx !== -1) {
|
||||
connections.splice(existingIdx, 1);
|
||||
runSimulation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (portId === 'out') {
|
||||
const coords = getPortCoords(nodeEl.dataset.id, 'out');
|
||||
wiringStart = { node: nodeEl.dataset.id, port: portId, x: coords.x, y: coords.y };
|
||||
tempWirePath = { x: coords.x, y: coords.y };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const wire = e.target.closest('.lg-wire');
|
||||
if (wire && wire.dataset.connId) {
|
||||
clearSelection();
|
||||
selectedWireId = wire.dataset.connId;
|
||||
renderWires();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeEl = e.target.closest('.lg-node');
|
||||
if (nodeEl) {
|
||||
clearSelection();
|
||||
selectedNodeId = nodeEl.dataset.id;
|
||||
nodeEl.classList.add('selected');
|
||||
|
||||
isDraggingNode = nodeEl.dataset.id;
|
||||
const rect = nodeEl.getBoundingClientRect();
|
||||
dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||
return;
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
const wsRect = workspace.getBoundingClientRect();
|
||||
|
||||
if (isDraggingNode) {
|
||||
const node = nodes[isDraggingNode];
|
||||
let newX = e.clientX - wsRect.left - dragOffset.x;
|
||||
let newY = e.clientY - wsRect.top - dragOffset.y;
|
||||
node.x = Math.max(10, Math.min(newX, wsRect.width - 80));
|
||||
node.y = Math.max(20, Math.min(newY, wsRect.height - 60));
|
||||
updateNodePositions();
|
||||
}
|
||||
|
||||
if (wiringStart) {
|
||||
tempWirePath = {
|
||||
x: e.clientX - wsRect.left,
|
||||
y: e.clientY - wsRect.top
|
||||
};
|
||||
renderWires();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', (e) => {
|
||||
isDraggingNode = null;
|
||||
|
||||
if (wiringStart) {
|
||||
const port = e.target.closest('.lg-port');
|
||||
if (port && port.dataset.port.startsWith('in')) {
|
||||
const targetNodeId = port.closest('.lg-node').dataset.id;
|
||||
const targetPortId = port.dataset.port.replace('in', '');
|
||||
|
||||
if (targetNodeId !== wiringStart.node) {
|
||||
connections = connections.filter(c => !(c.toNode === targetNodeId && c.toPort === targetPortId));
|
||||
|
||||
connections.push({
|
||||
id: `conn_${nextWireId++}`,
|
||||
fromNode: wiringStart.node,
|
||||
fromPort: 'out',
|
||||
toNode: targetNodeId,
|
||||
toPort: targetPortId
|
||||
});
|
||||
}
|
||||
}
|
||||
wiringStart = null;
|
||||
tempWirePath = null;
|
||||
runSimulation();
|
||||
}
|
||||
});
|
||||
|
||||
/* --- Deletion --- */
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
if (selectedWireId) {
|
||||
connections = connections.filter(c => c.id !== selectedWireId);
|
||||
clearSelection();
|
||||
runSimulation();
|
||||
}
|
||||
else if (selectedNodeId) {
|
||||
connections = connections.filter(c => c.fromNode !== selectedNodeId && c.toNode !== selectedNodeId);
|
||||
if (nodes[selectedNodeId] && nodes[selectedNodeId].el) {
|
||||
workspace.removeChild(nodes[selectedNodeId].el);
|
||||
}
|
||||
delete nodes[selectedNodeId];
|
||||
clearSelection();
|
||||
runSimulation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* --- Drag and Drop --- */
|
||||
workspace.addEventListener('dragover', (e) => { e.preventDefault(); });
|
||||
workspace.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const spawnType = e.dataTransfer.getData('spawnType');
|
||||
if (spawnType) {
|
||||
const gateType = e.dataTransfer.getData('gateType');
|
||||
const wsRect = workspace.getBoundingClientRect();
|
||||
const x = e.clientX - wsRect.left - 40;
|
||||
const y = e.clientY - wsRect.top - 30;
|
||||
spawnNode(spawnType, gateType || null, x, y);
|
||||
}
|
||||
});
|
||||
|
||||
/* --- Init --- */
|
||||
btnClearBoard?.addEventListener('click', () => {
|
||||
workspace.querySelectorAll('.lg-node').forEach(el => el.remove());
|
||||
nodes = {};
|
||||
connections = [];
|
||||
inputCount = 0;
|
||||
outputCount = 0;
|
||||
runSimulation();
|
||||
});
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const isCollapsed = logicPage?.classList.contains("toolboxCollapsed");
|
||||
logicPage.classList.toggle("toolboxCollapsed", !isCollapsed);
|
||||
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
|
||||
setTimeout(renderWires, 450);
|
||||
});
|
||||
|
||||
initToolbox();
|
||||
spawnNode('INPUT', null, 80, 150);
|
||||
spawnNode('INPUT', null, 80, 250);
|
||||
spawnNode('GATE', 'AND', 320, 200);
|
||||
spawnNode('OUTPUT', null, 600, 200);
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user