Completed Wave 3 features:
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:
2026-03-01 16:22:58 +00:00
parent 4facee954c
commit ffab71cfcc
57 changed files with 3602 additions and 3488 deletions

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -95,14 +95,14 @@ jobs:
shell: bash
run: |
set -e
if [ ! -d "export" ]; then
echo "❌ export/ folder not found in repo root"
if [ ! -d "dist" ]; then
echo "❌ dist/ folder not found in repo root"
ls -la
exit 1
fi
rm -f "Computing:Box Website.zip"
(cd export && zip -r "../Computing:Box Website.zip" .)
(cd dist && zip -r "../Computing:Box Website.zip" .)
test -s "Computing:Box Website.zip"
ls -lh "Computing:Box Website.zip"

View File

@@ -1,235 +0,0 @@
name: Changelog + Release on main
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
changelog_and_release:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Stop if this is the bot changelog commit
shell: bash
run: |
set -e
msg="$(git log -1 --pretty=%B)"
echo "$msg" | tr -d '\r' | grep -qi "\[skip ci\]" && {
echo "Skipping (bot commit with [skip ci])"
exit 0
} || true
- name: Install git-cliff
shell: bash
run: |
set -e
GIT_CLIFF_VERSION="2.11.0"
URL="https://github.com/orhun/git-cliff/releases/download/v${GIT_CLIFF_VERSION}/git-cliff-${GIT_CLIFF_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -L "$URL" -o /tmp/git-cliff.tar.gz
tar -xzf /tmp/git-cliff.tar.gz -C /tmp
sudo install /tmp/git-cliff-*/git-cliff /usr/local/bin/git-cliff
git-cliff --version
- name: Generate CHANGELOG.md (Keep a Changelog)
shell: bash
run: |
set -e
git-cliff --config cliff.toml --output CHANGELOG.md
test -s CHANGELOG.md
- name: Commit and push CHANGELOG.md if changed (CHANGELOG_PAT)
shell: bash
env:
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
run: |
set -e
if git diff --quiet -- CHANGELOG.md; then
echo "No changelog changes."
else
git config user.name "changelog-bot"
git config user.email "changelog-bot@users.noreply.local"
git add CHANGELOG.md
git commit -m "docs(changelog): update changelog [skip ci]"
origin_url="$(git remote get-url origin)"
# Convert SSH origin to HTTPS if needed
if echo "$origin_url" | grep -q "^git@"; then
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
origin_url="https://$host/$path"
fi
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
git push "$authed_url" HEAD:main
fi
- name: Extract newest changelog section for release body
shell: bash
run: |
set -e
# Extract the first "## ..." section (newest section) from CHANGELOG.md
# Includes the "## ..." heading and everything until the next "## ..." heading.
awk '
/^## / { if (seen) exit; seen=1 }
seen { print }
' CHANGELOG.md > RELEASE_NOTES.md
# Clean trailing whitespace/newlines a bit
sed -i 's/[[:space:]]*$//' RELEASE_NOTES.md
test -s RELEASE_NOTES.md
echo "---- RELEASE_NOTES.md ----"
head -n 60 RELEASE_NOTES.md
echo "--------------------------"
- name: Create export zip (Computing:Box Website.zip)
shell: bash
run: |
set -e
if [ ! -d "export" ]; then
echo "❌ export/ folder not found in repo root"
ls -la
exit 1
fi
rm -f "Computing:Box Website.zip"
(cd export && zip -r "../Computing:Box Website.zip" .)
test -s "Computing:Box Website.zip"
ls -lh "Computing:Box Website.zip"
- name: Prepare YY.MM.DD letter-suffix tag + release name
shell: bash
run: |
set -e
# Version: YY.MM.DD (UTC). Swap to `date +...` if you prefer UK-local runner time.
VERSION="$(date -u +'%y.%m.%d')"
PREFIX="v${VERSION}."
last_letter="$(
git tag --list "${PREFIX}[a-z]" \
| sed -E "s/^${PREFIX}([a-z])$/\1/" \
| sort \
| tail -n 1
)"
if [ -z "$last_letter" ]; then
next_letter="a"
else
if [ "$last_letter" = "z" ]; then
echo "❌ Already have v${VERSION}.z today. Refusing to create more than 26 releases/day."
exit 1
fi
next_letter="$(printf "%b" "$(printf '\\%03o' "$(( $(printf '%d' "'$last_letter") + 1 ))")")"
fi
TAG="${PREFIX}${next_letter}"
RELEASE_NAME="Computing:Box v${VERSION}.${next_letter}"
echo "TAG=$TAG" >> "$GITHUB_ENV"
echo "RELEASE_NAME=$RELEASE_NAME" >> "$GITHUB_ENV"
echo "ZIP_PATH=Computing:Box Website.zip" >> "$GITHUB_ENV"
echo "Using tag: $TAG"
echo "Release name: $RELEASE_NAME"
- name: Create and push tag (CHANGELOG_PAT)
shell: bash
env:
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
run: |
set -e
git tag -f "$TAG"
origin_url="$(git remote get-url origin)"
# Convert SSH origin to HTTPS if needed
if echo "$origin_url" | grep -q "^git@"; then
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
origin_url="https://$host/$path"
fi
authed_url="$(echo "$origin_url" | sed -E "s#^https://#https://oauth2:${CHANGELOG_PAT}@#")"
git push "$authed_url" "refs/tags/$TAG" --force
- name: Create Gitea release + upload asset (CHANGELOG_PAT)
shell: bash
env:
CHANGELOG_PAT: ${{ secrets.CHANGELOG_PAT }}
run: |
set -e
origin_url="$(git remote get-url origin)"
if echo "$origin_url" | grep -q "^git@"; then
host="$(echo "$origin_url" | sed -E 's#git@([^:]+):.*#\1#')"
path="$(echo "$origin_url" | sed -E 's#git@[^:]+:(.*)#\1#')"
origin_url="https://$host/$path"
fi
base="$(echo "$origin_url" | sed -E 's#(https?://[^/]+)/.*#\1#')"
repo_path="$(echo "$origin_url" | sed -E 's#https?://[^/]+/##')"
repo_path="$(echo "$repo_path" | sed -E 's/\.git$//')"
owner="$(echo "$repo_path" | cut -d/ -f1)"
repo="$(echo "$repo_path" | cut -d/ -f2-)"
api="$base/api/v1"
python3 - <<'PY'
import json, os
tag = os.environ["TAG"]
name = os.environ["RELEASE_NAME"]
with open("RELEASE_NOTES.md", "r", encoding="utf-8") as f:
body = f.read()
payload = {
"tag_name": tag,
"target_commitish": "main",
"name": name,
"body": body, # newest section only
"draft": False,
"prerelease": False,
}
with open("release.json", "w", encoding="utf-8") as f:
json.dump(payload, f)
PY
curl -sS -X POST \
-H "Authorization: Bearer ${CHANGELOG_PAT}" \
-H "Content-Type: application/json" \
"${api}/repos/${owner}/${repo}/releases" \
--data-binary @release.json \
-o release_response.json
release_id="$(python3 - <<'PY'
import json
with open("release_response.json","r",encoding="utf-8") as f:
data=json.load(f)
rid=data.get("id")
if not rid:
raise SystemExit("No release id returned. Response:\n" + json.dumps(data, indent=2))
print(rid)
PY
)"
echo "Created release id: $release_id"
curl -sS -X POST \
-H "Authorization: Bearer ${CHANGELOG_PAT}" \
"${api}/repos/${owner}/${repo}/releases/${release_id}/assets?name=Computing%3ABox%20Website.zip" \
-F "attachment=@${ZIP_PATH}" \
>/dev/null
echo "✅ Release created: ${RELEASE_NAME} (tag: ${TAG}) with asset uploaded"

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# build output
# dist/
dist/
# generated types
.astro/

View File

@@ -28,15 +28,18 @@ An evolution of Bit:Box & CS:Box to incorporate different elements of the UK Com
- [X] XNOR Gate Simulator
### Wave 3 CS:Box Features (Spring 2026)
- [ ] New User Interface (Responsive)
- [X] Two's Compliment Simulator
- [ ] Extended Binary Simulator (Custom bit sizes)
- [ ] Unified Binary Simulator (Unsigned & Two's Completment combined)
- [X] Extended Binary Simulator (Custom bit sizes)
- [X] Unified Binary Simulator (Unsigned & Two's Completment combined)
- [ ] Extended Hexadecimal Simulator
- [ ] Unified Hexadecimal Simulator (For GCSE & A Level Specification)
- [ ] Enhanced Gate Simulator (Truth Table Creator)
- [ ] Compound Gate Simulator
- [ ] Computer Components Simulator
## Version 1.0 Release Date: 1<sup>st</sup> September 2025
## Version 2.0 Release Date (Goal): 1<sup>st</sup> April 2026
## Version 2.0 Release Date (Goal): 1<sup>st</sup> May 2026
Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa]

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,12 @@
(()=>{const d=document.getElementById("bitsGrid"),h=document.getElementById("denaryNumber"),M=document.getElementById("binaryNumber"),f=document.getElementById("bitsInput"),m=document.getElementById("modeToggle"),E=document.getElementById("modeHint"),T=document.getElementById("lblUnsigned"),k=document.getElementById("lblTwos"),H=document.getElementById("btnCustomBinary"),G=document.getElementById("btnCustomDenary"),P=document.getElementById("btnShiftLeft"),V=document.getElementById("btnShiftRight"),q=document.getElementById("btnDec"),Z=document.getElementById("btnInc"),z=document.getElementById("btnClear"),I=document.getElementById("btnRandom"),O=document.getElementById("btnBitsUp"),W=document.getElementById("btnBitsDown"),R=document.getElementById("toolboxToggle"),w=document.getElementById("binaryPage");let i=b(Number(f?.value??8),1,64),s=new Array(i).fill(!1),u=null;function b(t,n,e){return Number.isFinite(t)?Math.max(n,Math.min(e,Math.trunc(t))):n}function l(){return!!m?.checked}function a(t){return 1n<<BigInt(t)}function y(t){return a(t)}function $(t){return a(t)-1n}function B(t){return-a(t-1)}function p(t){return a(t-1)-1n}function v(){let t=0n;for(let n=0;n<i;n++)s[n]&&(t+=a(n));return t}function g(t){const n=y(i),e=(t%n+n)%n;for(let o=0;o<i;o++)s[o]=(e>>BigInt(o)&1n)===1n}function L(){const t=v();return s[i-1]===!0?t-a(i):t}function x(t){const n=a(i);let e=t;e=(e%n+n)%n,g(e)}function j(){let t="";for(let n=i-1;n>=0;n--){t+=s[n]?"1":"0";const e=i-n;n!==0&&e%4===0&&(t+=" ")}return t.trimEnd()}function D(){E&&(l()?E.textContent="Tip: In two's complement, the left-most bit (MSB) represents a negative value.":E.textContent="Tip: In unsigned binary, all bits represent positive values.")}function A(){if(!d)return;const t=d.parentElement;if(!t)return;const n=t.getBoundingClientRect().width,o=b(Math.floor(n/100),1,12);d.style.setProperty("--cols",String(Math.min(o,i)))}function C(t){i=b(t,1,64),f&&(f.value=String(i));const n=s.slice();s=new Array(i).fill(!1);for(let e=0;e<Math.min(n.length,i);e++)s[e]=n[e];d.innerHTML="",d.classList.toggle("bitsFew",i<8);for(let e=i-1;e>=0;e--){const o=document.createElement("div");o.className="bit",o.innerHTML=`
<div class="bulb" id="bulb-${e}" 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-${e}"></div>
<label class="switch" aria-label="Toggle bit ${e}">
<input type="checkbox" data-index="${e}">
<span class="slider"></span>
</label>
`,d.appendChild(o)}d.querySelectorAll('input[type="checkbox"]').forEach(e=>{e.addEventListener("change",()=>{const o=Number(e.dataset.index);s[o]=e.checked,r()})}),A(),r()}function J(){for(let t=0;t<i;t++){const n=document.getElementById(`bitLabel-${t}`);if(!n)continue;let e;l()&&t===i-1?e=`-${a(i-1).toString()}`:e=a(t).toString(),n.textContent=e,n.style.setProperty("--len",e.length)}}function K(){d.querySelectorAll('input[type="checkbox"]').forEach(t=>{const n=Number(t.dataset.index);t.checked=!!s[n]})}function Q(){for(let t=0;t<i;t++){const n=document.getElementById(`bulb-${t}`);n&&n.classList.toggle("on",s[t]===!0)}}function X(){!h||!M||(l()?h.textContent=L().toString():h.textContent=v().toString(),M.textContent=j())}function r(){D(),T&&k&&(T.classList.toggle("activeMode",!l()),k.classList.toggle("activeMode",l())),J(),K(),Q(),X()}function Y(t){const n=String(t??"").replace(/\s+/g,"");if(!/^[01]+$/.test(n))return!1;const e=n.slice(-i).padStart(i,"0");for(let o=0;o<i;o++){const c=e[e.length-1-o];s[o]=c==="1"}return r(),!0}function _(t){const n=String(t??"").trim();if(!n)return!1;let e;try{if(!/^-?\d+$/.test(n))return!1;e=BigInt(n)}catch{return!1}if(l()){const o=B(i),c=p(i);if(e<o||e>c)return!1;x(e)}else{if(e<0n||e>$(i))return!1;g(e)}return r(),!0}function tt(){for(let t=i-1;t>=1;t--)s[t]=s[t-1];s[0]=!1,r()}function nt(){const t=s[i-1];for(let n=0;n<i-1;n++)s[n]=s[n+1];s[i-1]=l()?t:!1,r()}function et(){s=[],m&&(m.checked=!1),C(8)}function it(){if(l()){const t=B(i),n=p(i);let e=L()+1n;e>n&&(e=t),x(e)}else{const t=y(i);g((v()+1n)%t)}r()}function ot(){if(l()){const t=B(i),n=p(i);let e=L()-1n;e<t&&(e=n),x(e)}else{const t=y(i);g((v()-1n+t)%t)}r()}function st(t){if(t<=0n)return 0n;const n=t.toString(2).length,e=Math.ceil(n/8);for(;;){const o=new Uint8Array(e);crypto.getRandomValues(o);let c=0n;for(const ct of o)c=c<<8n|BigInt(ct);const U=BigInt(e*8-n);if(U>0n&&(c=c>>U),c<t)return c}}function lt(){const t=y(i),n=st(t);g(n),r()}function N(t){I&&I.classList.toggle("btnRandomRunning",!!t)}function rt(){u&&(clearInterval(u),u=null),N(!0);const t=Date.now(),n=1125;u=setInterval(()=>{lt(),Date.now()-t>=n&&(clearInterval(u),u=null,N(!1))},80)}function S(t){const n=b(t,1,64);C(n)}function F(t){if(!w)return;w.classList.toggle("toolboxCollapsed",!!t);const n=!t;R?.setAttribute("aria-expanded",n?"true":"false")}m?.addEventListener("change",r),H?.addEventListener("click",()=>{const t=prompt(`Enter binary (spaces allowed). Current width: ${i} bits`);t!==null&&(Y(t)||alert("Invalid binary"))}),G?.addEventListener("click",()=>{const t=prompt(l()?`Enter denary (${B(i).toString()} to ${p(i).toString()}):`:`Enter denary (0 to ${$(i).toString()}):`);t!==null&&(_(t)||alert("Invalid denary for current mode/bit width"))}),P?.addEventListener("click",tt),V?.addEventListener("click",nt),Z?.addEventListener("click",it),q?.addEventListener("click",ot),z?.addEventListener("click",et),I?.addEventListener("click",rt),O?.addEventListener("click",()=>S(i+1)),W?.addEventListener("click",()=>S(i-1)),f?.addEventListener("change",()=>S(Number(f.value))),R?.addEventListener("click",()=>{const t=w?.classList.contains("toolboxCollapsed");F(!t)}),window.addEventListener("resize",()=>{A()}),D(),C(i),F(!1)})();

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,3 @@
<!DOCTYPE html><html lang="en" data-astro-cid-37fxchfa> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Binary Simulator</title><style>:root{--nav-h: 108px;--bg: #1f2027;--text: #e8e8ee;--muted: #a9acb8;--line: rgba(255,255,255,.1)}body{margin:0;background:var(--bg);color:var(--text)}.siteNav[data-astro-cid-37fxchfa]{position:sticky;top:0;z-index:50;height:var(--nav-h);background:#0000001a;border-bottom:1px solid var(--line);backdrop-filter:blur(8px)}.navInner[data-astro-cid-37fxchfa]{height:100%;max-width:1400px;margin:0 auto;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text)}.brandLogo[data-astro-cid-37fxchfa]{width:2em;height:2em;image-rendering:pixelated}.brandName[data-astro-cid-37fxchfa]{letter-spacing:.12em;font-weight:900;text-transform:uppercase;font-size:18px}.navLinks[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]{color:var(--muted);text-decoration:none;font-weight:800;letter-spacing:.12em;font-size:16px;text-transform:uppercase}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]:hover{color:var(--text)}.pageWrap[data-astro-cid-37fxchfa]{max-width:1400px;margin:0 auto}
:root{--panel-w: 360px;--gap: 22px}.wrap{max-width:1400px;margin:0 auto;padding:26px 20px 48px;position:relative}.topGrid{display:grid;grid-template-columns:1fr var(--panel-w);gap:var(--gap);align-items:start}body.toolboxClosed .topGrid{grid-template-columns:1fr}body.toolboxClosed #toolboxPanel{display:none}.mainCol{min-width:0}.readout{text-align:center;margin-top:8px}.label{opacity:.8;letter-spacing:.12em;text-transform:uppercase;font-size:12px}.num{display:inline-block;width:fit-content;max-width:100%;white-space:pre-line;letter-spacing:2px}.denaryValue{font-size:54px;margin:6px 0 10px}.binaryValue{font-size:56px;margin:4px 0 18px}.divider{height:1px;background:#ffffff1a;margin:14px auto 24px;max-width:900px}.bitsWrap{padding-top:6px}.bitsGrid{display:grid;gap:24px;justify-content:center}.bitsGrid{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;line-height:1;opacity:.45}.bitVal{font-size:22px;line-height:1.05;text-align:center;white-space:nowrap}.switch{position:relative;display:inline-block;width:52px;height:28px}.switch input{display:none}.slider{position:absolute;inset:0;border-radius:999px;background:#ffffff2e;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:translate(22px)}.toolboxToggle{position:absolute;right:20px;top:18px;z-index:20;display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:12px;border:1px solid rgba(255,255,255,.14);background:#ffffff0f;color:#ffffffeb;cursor:pointer}.toolboxText{letter-spacing:.12em;font-weight:900}.panelCol{position:sticky;top:calc(var(--nav-h, 72px) + 18px);align-self:start;display:grid;gap:16px}.card{border:1px solid rgba(255,255,255,.12);border-radius:16px;background:#ffffff0d;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}.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,.1);border-radius:14px;background:#0000001f;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,.1);border-radius:12px;background:#ffffff0a}.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:#ffffff0f;color:#ffffffe6;font-size:18px;cursor:pointer}.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:#ffffff0f;color:#ffffffeb;padding:12px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;cursor:pointer}.btnWide{width:100%}.btnAccent{background:#00ff8c1f;border-color:#00ff8c38}.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:#ffffff0f;color:#ffffffeb;font-size:18px;cursor:pointer}.toolDec{background:#ff000024;border-color:#f003}.toolInc{background:#00ff8c24;border-color:#00ff8c33}.toolRow2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.btnReset{color:#ffffffeb}
</style></head> <body data-astro-cid-37fxchfa> <header class="siteNav" data-astro-cid-37fxchfa> <div class="navInner" data-astro-cid-37fxchfa> <a class="brand" href="/" data-astro-cid-37fxchfa> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" data-astro-cid-37fxchfa> <span class="brandName" data-astro-cid-37fxchfa>COMPUTING:BOX</span> </a> <nav class="navLinks" aria-label="Site navigation" data-astro-cid-37fxchfa> <a href="/about" data-astro-cid-37fxchfa>ABOUT</a> <a href="/binary" data-astro-cid-37fxchfa>BINARY</a> <a href="/hexadecimal" data-astro-cid-37fxchfa>HEXADECIMAL</a> <a href="/hex-colours" data-astro-cid-37fxchfa>HEX COLOURS</a> <a href="/logic-gates" data-astro-cid-37fxchfa>LOGIC GATES</a> </nav> </div> </header> <main class="pageWrap" data-astro-cid-37fxchfa> <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> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">0000 0000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Bit switches"> <div class="bitsGrid" id="bitsGrid"></div> </section> </div> <!-- 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> <div class="toggleLabel" id="lblTwos">Two&rsquo;s complement</div> </div> <div class="hint" id="modeHint">
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Binary Simulator | Computing:Box</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_astro/binary.BbxrcYRT.css"></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"> <div class="binaryPage" id="binaryPage"> <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"> <div class="leftCol"> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Binary</div> <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> <aside id="toolboxPanel" class="panelCol" aria-label="Toolbox"> <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> <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 --> <div class="card"> <div class="cardTitle">Custom</div> <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="toolBtn toolWide toolRandom" 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="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="twoBtnRow"> <button class="btn" id="btnShiftLeft" type="button">Left Shift</button> <button class="btn" id="btnShiftRight" type="button">Right Shift</button> </div> <button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button> </div> </aside> </section> </main> <script type="module" src="/src/scripts/binary.js"></script> </main> </body></html>
</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> <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> <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> </div> <script type="module" src="/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js"></script> </main> <footer class="siteFooter"> <div class="footerInner"> <div>COMPUTER SCIENCE CONCEPT SIMULATORS</div> <div>© 2026 COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL</div> </div> </footer> </body></html>

9
dist/favicon.svg vendored Normal file
View File

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

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -1,5 +1,3 @@
<!DOCTYPE html><html lang="en" data-astro-cid-37fxchfa> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hexadecimal | Computing:Box</title><style>:root{--nav-h: 108px;--bg: #1f2027;--text: #e8e8ee;--muted: #a9acb8;--line: rgba(255,255,255,.1)}body{margin:0;background:var(--bg);color:var(--text)}.siteNav[data-astro-cid-37fxchfa]{position:sticky;top:0;z-index:50;height:var(--nav-h);background:#0000001a;border-bottom:1px solid var(--line);backdrop-filter:blur(8px)}.navInner[data-astro-cid-37fxchfa]{height:100%;max-width:1400px;margin:0 auto;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text)}.brandLogo[data-astro-cid-37fxchfa]{width:2em;height:2em;image-rendering:pixelated}.brandName[data-astro-cid-37fxchfa]{letter-spacing:.12em;font-weight:900;text-transform:uppercase;font-size:18px}.navLinks[data-astro-cid-37fxchfa]{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]{color:var(--muted);text-decoration:none;font-weight:800;letter-spacing:.12em;font-size:16px;text-transform:uppercase}.navLinks[data-astro-cid-37fxchfa] a[data-astro-cid-37fxchfa]:hover{color:var(--text)}.pageWrap[data-astro-cid-37fxchfa]{max-width:1400px;margin:0 auto}
</style>
<link rel="stylesheet" href="/_astro/hexadecimal.C_Opoo6d.css"></head> <body data-astro-cid-37fxchfa> <header class="siteNav" data-astro-cid-37fxchfa> <div class="navInner" data-astro-cid-37fxchfa> <a class="brand" href="/" data-astro-cid-37fxchfa> <img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" data-astro-cid-37fxchfa> <span class="brandName" data-astro-cid-37fxchfa>COMPUTING:BOX</span> </a> <nav class="navLinks" aria-label="Site navigation" data-astro-cid-37fxchfa> <a href="/about" data-astro-cid-37fxchfa>ABOUT</a> <a href="/binary" data-astro-cid-37fxchfa>BINARY</a> <a href="/hexadecimal" data-astro-cid-37fxchfa>HEXADECIMAL</a> <a href="/hex-colours" data-astro-cid-37fxchfa>HEX COLOURS</a> <a href="/logic-gates" data-astro-cid-37fxchfa>LOGIC GATES</a> </nav> </div> </header> <main class="pageWrap" data-astro-cid-37fxchfa> <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> <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> <path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path> </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> </main> </body></html>
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hexadecimal Simulator | Computing:Box</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_astro/binary.BbxrcYRT.css"></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"> <div class="binaryPage" id="hexPage"> <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"> <div class="leftCol"> <div class="readout"> <div class="label">Denary</div> <div id="denaryNumber" class="num denaryValue">0</div> <div class="label">Hexadecimal</div> <div id="hexNumber" class="num hexValue">00</div> <div class="label">Binary</div> <div id="binaryNumber" class="num binaryValue">00000000</div> </div> <div class="divider"></div> <section class="bitsWrap" aria-label="Hexadecimal sliders"> <div class="hexGrid" id="hexGrid"></div> </section> </div> <aside id="toolboxPanel" class="panelCol" aria-label="Toolbox"> <div class="card"> <div class="cardTitle">Settings</div> <div class="hint" style="margin-top: 0; margin-bottom: 14px;">
Hexadecimal represents numbers using base 16 (0-9, A-F).
</div> <div class="subCard"> <div class="subTitle">Digit width</div> <div class="bitWidthRow"> <button class="miniBtn" id="btnDigitsDown" type="button" aria-label="Decrease digits"></button> <div class="bitInputWrap"> <div class="bitInputLabel">Digits</div> <input id="digitsInput" class="bitInput" type="number" inputmode="numeric" min="1" max="16" step="1" value="2" aria-label="Number of hex digits"> </div> <button class="miniBtn" id="btnDigitsUp" type="button" aria-label="Increase digits">+</button> </div> </div> </div> <div class="card"> <div class="cardTitle">Custom Number</div> <div class="controlsRow"> <button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button> <button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button> </div> <div class="controlsRow"> <button class="btn btnAccent btnWide" id="btnCustomBinary" type="button">Custom Binary</button> </div> <button class="btn btnWide" id="btnRandom" type="button">Random</button> <div class="hint">Random runs briefly then stops automatically.</div> </div> <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> <button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button> </div> </aside> </section> </div> <script type="module" src="/_astro/hexadecimal.astro_astro_type_script_index_0_lang.C4Wx7oaX.js"></script> </main> <footer class="siteFooter"> <div class="footerInner"> <div>COMPUTER SCIENCE CONCEPT SIMULATORS</div> <div>© 2026 COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL</div> </div> </footer> </body></html>

4
dist/index.html vendored
View File

@@ -1,5 +1,5 @@
<!DOCTYPE html><html lang="en" data-astro-cid-sckkx6r4> <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 v5.16.6"><title>Astro Basics</title><style>#background[data-astro-cid-mmc7otgs]{position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;filter:blur(100px)}#container[data-astro-cid-mmc7otgs]{font-family:Inter,Roboto,Helvetica Neue,Arial Nova,Nimbus Sans,Arial,sans-serif;height:100%}main[data-astro-cid-mmc7otgs]{height:100%;display:flex;justify-content:center}#hero[data-astro-cid-mmc7otgs]{display:flex;align-items:start;flex-direction:column;justify-content:center;padding:16px}h1[data-astro-cid-mmc7otgs]{font-size:22px;margin-top:.25em}#links[data-astro-cid-mmc7otgs]{display:flex;gap:16px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]{display:flex;align-items:center;padding:10px 12px;color:#111827;text-decoration:none;transition:color .2s}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]:hover{color:#4e5056}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs] svg[data-astro-cid-mmc7otgs]{height:1em;margin-left:8px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{color:#fff;background:linear-gradient(83.21deg,#3245ff,#bc52ee);box-shadow:inset 0 0 0 1px #ffffff1f,inset 0 -2px #0000003d;border-radius:10px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button:hover{color:#e6e6e6;box-shadow:none}pre[data-astro-cid-mmc7otgs]{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-weight:400;background:linear-gradient(14deg,#d83333,#f041ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin:0}h2[data-astro-cid-mmc7otgs]{margin:0 0 1em;font-weight:400;color:#111827;font-size:20px}p[data-astro-cid-mmc7otgs]{color:#4b5563;font-size:16px;line-height:24px;letter-spacing:-.006em;margin:0}code[data-astro-cid-mmc7otgs]{display:inline-block;background:linear-gradient(66.77deg,#f3cddd,#f5cee7) padding-box,linear-gradient(155deg,#d83333,#f041ff 18%,#f5cee7 45%) border-box;border-radius:8px;border:1px solid transparent;padding:6px 8px}.box[data-astro-cid-mmc7otgs]{padding:16px;background:#fff;border-radius:16px;border:1px solid white}#news[data-astro-cid-mmc7otgs]{position:absolute;bottom:16px;right:16px;max-width:300px;text-decoration:none;transition:background .2s;backdrop-filter:blur(50px)}#news[data-astro-cid-mmc7otgs]:hover{background:#ffffff8c}@media screen and (max-height:368px){#news[data-astro-cid-mmc7otgs]{display:none}}@media screen and (max-width:768px){#container[data-astro-cid-mmc7otgs]{display:flex;flex-direction:column}#hero[data-astro-cid-mmc7otgs]{display:block;padding-top:10%}#links[data-astro-cid-mmc7otgs]{flex-wrap:wrap}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{padding:14px 18px}#news[data-astro-cid-mmc7otgs]{right:16px;left:16px;bottom:2.5rem;max-width:100%}h1[data-astro-cid-mmc7otgs]{line-height:1.5}}html,body{margin:0;width:100%;height:100%}
</style></head> <body data-astro-cid-sckkx6r4> <div id="container" data-astro-cid-mmc7otgs> <img id="background" src="/_astro/background.Mahwsfbs.svg" alt="" fetchpriority="high" data-astro-cid-mmc7otgs> <main data-astro-cid-mmc7otgs> <section id="hero" data-astro-cid-mmc7otgs> <a href="https://astro.build" data-astro-cid-mmc7otgs><img src="/_astro/astro.CXuftnGC.svg" width="115" height="48" alt="Astro Homepage" data-astro-cid-mmc7otgs></a> <h1 data-astro-cid-mmc7otgs>
<!DOCTYPE html><html lang="en" data-astro-cid-sckkx6r4> <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 v5.18.0"><title>Astro Basics</title><style>#background[data-astro-cid-mmc7otgs]{position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;filter:blur(100px)}#container[data-astro-cid-mmc7otgs]{font-family:Inter,Roboto,Helvetica Neue,Arial Nova,Nimbus Sans,Arial,sans-serif;height:100%}main[data-astro-cid-mmc7otgs]{height:100%;display:flex;justify-content:center}#hero[data-astro-cid-mmc7otgs]{display:flex;align-items:start;flex-direction:column;justify-content:center;padding:16px}h1[data-astro-cid-mmc7otgs]{font-size:22px;margin-top:.25em}#links[data-astro-cid-mmc7otgs]{display:flex;gap:16px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]{display:flex;align-items:center;padding:10px 12px;color:#111827;text-decoration:none;transition:color .2s}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]:hover{color:#4e5056}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs] svg[data-astro-cid-mmc7otgs]{height:1em;margin-left:8px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{color:#fff;background:linear-gradient(83.21deg,#3245ff,#bc52ee);box-shadow:inset 0 0 0 1px #ffffff1f,inset 0 -2px #0000003d;border-radius:10px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button:hover{color:#e6e6e6;box-shadow:none}pre[data-astro-cid-mmc7otgs]{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-weight:400;background:linear-gradient(14deg,#d83333,#f041ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin:0}h2[data-astro-cid-mmc7otgs]{margin:0 0 1em;font-weight:400;color:#111827;font-size:20px}p[data-astro-cid-mmc7otgs]{color:#4b5563;font-size:16px;line-height:24px;letter-spacing:-.006em;margin:0}code[data-astro-cid-mmc7otgs]{display:inline-block;background:linear-gradient(66.77deg,#f3cddd,#f5cee7) padding-box,linear-gradient(155deg,#d83333,#f041ff 18%,#f5cee7 45%) border-box;border-radius:8px;border:1px solid transparent;padding:6px 8px}.box[data-astro-cid-mmc7otgs]{padding:16px;background:#fff;border-radius:16px;border:1px solid white}#news[data-astro-cid-mmc7otgs]{position:absolute;bottom:16px;right:16px;max-width:300px;text-decoration:none;transition:background .2s;backdrop-filter:blur(50px)}#news[data-astro-cid-mmc7otgs]:hover{background:#ffffff8c}@media screen and (max-height:368px){#news[data-astro-cid-mmc7otgs]{display:none}}@media screen and (max-width:768px){#container[data-astro-cid-mmc7otgs]{display:flex;flex-direction:column}#hero[data-astro-cid-mmc7otgs]{display:block;padding-top:10%}#links[data-astro-cid-mmc7otgs]{flex-wrap:wrap}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{padding:14px 18px}#news[data-astro-cid-mmc7otgs]{right:16px;left:16px;bottom:2.5rem;max-width:100%}h1[data-astro-cid-mmc7otgs]{line-height:1.5}}html,body{margin:0;width:100%;height:100%}
</style></head> <body data-astro-cid-sckkx6r4> <div id="container" data-astro-cid-mmc7otgs> <img id="background" src="/_astro/background.BPKAcmfN.svg" alt="" fetchpriority="high" data-astro-cid-mmc7otgs> <main data-astro-cid-mmc7otgs> <section id="hero" data-astro-cid-mmc7otgs> <a href="https://astro.build" data-astro-cid-mmc7otgs><img src="/_astro/astro.Dm8K3lV8.svg" width="115" height="48" alt="Astro Homepage" data-astro-cid-mmc7otgs></a> <h1 data-astro-cid-mmc7otgs>
To get started, open the <code data-astro-cid-mmc7otgs><pre data-astro-cid-mmc7otgs>src/pages</pre></code> directory in your project.
</h1> <section id="links" data-astro-cid-mmc7otgs> <a class="button" href="https://docs.astro.build" data-astro-cid-mmc7otgs>Read our docs</a> <a href="https://astro.build/chat" data-astro-cid-mmc7otgs>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" data-astro-cid-mmc7otgs><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" data-astro-cid-mmc7otgs></path></svg> </a> </section> </section> </main> <a href="https://astro.build/blog/astro-5/" id="news" class="box" data-astro-cid-mmc7otgs> <svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg" data-astro-cid-mmc7otgs><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" data-astro-cid-mmc7otgs></path></svg> <h2 data-astro-cid-mmc7otgs>What's New in Astro 5.0?</h2> <p data-astro-cid-mmc7otgs>
From content layers to server islands, click to learn more about the new features and

115
dist/js/binary/unsigned-binary.js vendored Normal file
View File

@@ -0,0 +1,115 @@
// Browser-only script. Safe because it's loaded via <script> (not server-imported).
const BIT_COUNT = 8; // unsigned page = 8 bits
const bitValues = [128, 64, 32, 16, 8, 4, 2, 1];
const elDenary = document.getElementById("denaryNumber");
const elBinary = document.getElementById("binaryNumber");
const elSwitches = document.getElementById("bitSwitches");
const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnReset = document.getElementById("btnReset");
let bits = new Array(BIT_COUNT).fill(false);
function renderSwitches() {
elSwitches.innerHTML = "";
bitValues.forEach((value, index) => {
const id = `bit-${value}`;
const wrapper = document.createElement("div");
wrapper.className = "switch-col";
const labelTop = document.createElement("div");
labelTop.className = "bit-label";
labelTop.textContent = value;
const label = document.createElement("label");
label.className = "rocker";
label.setAttribute("for", id);
const input = document.createElement("input");
input.type = "checkbox";
input.id = id;
input.checked = bits[index];
input.addEventListener("change", () => {
bits[index] = input.checked;
updateNumbers();
});
const span = document.createElement("span");
span.className = "rocker-body";
span.setAttribute("aria-hidden", "true");
label.appendChild(input);
label.appendChild(span);
wrapper.appendChild(labelTop);
wrapper.appendChild(label);
elSwitches.appendChild(wrapper);
});
}
function updateNumbers() {
const binary = bits.map(b => (b ? "1" : "0")).join("");
const denary = bits.reduce((acc, b, i) => acc + (b ? bitValues[i] : 0), 0);
elBinary.textContent = binary;
elDenary.textContent = denary.toString();
}
function resetAll() {
bits = new Array(BIT_COUNT).fill(false);
renderSwitches();
updateNumbers();
}
function requestCustomDenary() {
let input = prompt(`Enter a denary number (0 to 255):`);
if (input === null) return;
const n = Number.parseInt(input, 10);
if (Number.isNaN(n) || n < 0 || n > 255) {
alert("Invalid input. Enter a number from 0 to 255.");
return;
}
let remaining = n;
bits = bitValues.map(v => {
if (remaining >= v) {
remaining -= v;
return true;
}
return false;
});
renderSwitches();
updateNumbers();
}
function requestCustomBinary() {
let input = prompt(`Enter an ${BIT_COUNT}-bit binary number (e.g. 01010101):`);
if (input === null) return;
input = input.trim();
const re = new RegExp(`^[01]{${BIT_COUNT}}$`);
if (!re.test(input)) {
alert(`Invalid input. Enter exactly ${BIT_COUNT} digits using only 0 or 1.`);
return;
}
bits = input.split("").map(c => c === "1");
renderSwitches();
updateNumbers();
}
btnCustomDenary?.addEventListener("click", requestCustomDenary);
btnCustomBinary?.addEventListener("click", requestCustomBinary);
btnReset?.addEventListener("click", resetAll);
renderSwitches();
updateNumbers();

72
dist/js/tools/unsigned-binary.js vendored Normal file
View File

@@ -0,0 +1,72 @@
// public/js/tools/unsigned-binary.js
// Lightweight: no frameworks. Works on weak devices.
const BIT_COUNT = 8;
const MAX_DENARY = 255;
let bits = new Array(BIT_COUNT).fill(false);
function bitsToBinaryString(){
return bits.map(b => (b ? "1" : "0")).join("");
}
function bitsToDenary(){
// MSB on the left: 128..1
const weights = [128,64,32,16,8,4,2,1];
return bits.reduce((acc, b, i) => acc + (b ? weights[i] : 0), 0);
}
function render(){
const grid = document.getElementById("bitGrid");
grid.innerHTML = "";
const weights = [128,64,32,16,8,4,2,1];
bits.forEach((on, i) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn";
btn.style.width = "100%";
btn.style.justifyContent = "space-between";
btn.setAttribute("aria-pressed", on ? "true" : "false");
btn.innerHTML = `<span>${weights[i]}</span><b>${on ? "1" : "0"}</b>`;
btn.addEventListener("click", () => {
bits[i] = !bits[i];
update();
});
grid.appendChild(btn);
});
}
function update(){
document.getElementById("binaryNumber").innerText = bitsToBinaryString();
document.getElementById("denaryNumber").innerText = bitsToDenary();
render();
}
function requestBinary(){
let v;
do{
v = prompt("Enter an 8-bit binary value (8 digits, only 0 or 1):", bitsToBinaryString());
if(v === null) return;
v = v.trim();
}while(!/^[01]{8}$/.test(v));
bits = v.split("").map(ch => ch === "1");
update();
}
function requestDenary(){
let v;
do{
v = prompt("Enter a denary value (0 to 255):", String(bitsToDenary()));
if(v === null) return;
v = Number(v);
}while(!Number.isInteger(v) || v < 0 || v > MAX_DENARY);
// set bits from MSB to LSB
const weights = [128,64,32,16,8,4,2,1];
bits = weights.map(w => {
if(v >= w){ v -= w; return true; }
return false;
});
update();
}
function reset(){
bits = new Array(BIT_COUNT).fill(false);
update();
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("btnCustomBinary")?.addEventListener("click", requestBinary);
document.getElementById("btnCustomDenary")?.addEventListener("click", requestDenary);
document.getElementById("btnReset")?.addEventListener("click", reset);
update();
});

96
dist/styles.css vendored Normal file
View File

@@ -0,0 +1,96 @@
/* src/styles/md3-tokens.css */
/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */
:root{
/* Typography */
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
/* Spacing + shape */
--radius-1: 10px;
--radius-2: 16px;
--radius-3: 22px;
--shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
/* Color roles (keep simple) */
--md-surface: #ffffff;
--md-surface-2: #f6f7fb;
--md-on-surface: #111318;
--md-primary: #2f6fed; /* calm blue */
--md-on-primary: #ffffff;
--md-secondary: #5a5f72; /* muted */
--md-on-secondary: #ffffff;
--md-tertiary: #0f766e; /* teal for "practical" tools */
--md-on-tertiary: #ffffff;
--md-outline: #d7dbe7;
--md-success: #1a7f37;
--md-warning: #b54708;
--md-danger: #b42318;
/* Focus ring for accessibility */
--md-focus: 0 0 0 3px rgba(47,111,237,.28);
}
@media (prefers-color-scheme: dark){
:root{
--md-surface: #0b0e14;
--md-surface-2: #121725;
--md-on-surface: #e8eaf2;
--md-primary: #9bb6ff;
--md-on-primary: #0b0e14;
--md-secondary: #b8bccd;
--md-on-secondary: #0b0e14;
--md-tertiary: #4fd1c5;
--md-on-tertiary: #0b0e14;
--md-outline: #2b3244;
--md-focus: 0 0 0 3px rgba(155,182,255,.25);
}
}
/* src/styles/base.css */
@import "./md3-tokens.css";
html, body{ height:100%; }
body{
margin:0;
font-family: var(--font-sans);
background: var(--md-surface-2);
color: var(--md-on-surface);
}
a{ color: var(--md-primary); text-decoration: none; }
a:hover{ text-decoration: underline; }
.container{
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
.card{
background: var(--md-surface);
border: 1px solid var(--md-outline);
border-radius: var(--radius-2);
box-shadow: var(--shadow-1);
padding: 16px;
}
.btn{
display:inline-flex;
gap:8px;
align-items:center;
justify-content:center;
border-radius: 999px;
border: 1px solid var(--md-outline);
background: var(--md-surface);
color: var(--md-on-surface);
padding: 10px 14px;
font-weight: 600;
cursor: pointer;
}
.btn:hover{ filter: brightness(0.98); }
.btn:focus{ outline:none; box-shadow: var(--md-focus); }
.btn-primary{
background: var(--md-primary);
color: var(--md-on-primary);
border-color: transparent;
}
.badge{
display:inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--md-outline);
background: var(--md-surface-2);
}
code, pre{ font-family: var(--font-mono); }

1035
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "Computing:Box",
"name": "computing-box",
"type": "module",
"version": "2.0.0 Alpha",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
@@ -9,6 +9,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.16.6"
"astro": "^5.18.0"
}
}

9
public/favicon.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,115 @@
// Browser-only script. Safe because it's loaded via <script> (not server-imported).
const BIT_COUNT = 8; // unsigned page = 8 bits
const bitValues = [128, 64, 32, 16, 8, 4, 2, 1];
const elDenary = document.getElementById("denaryNumber");
const elBinary = document.getElementById("binaryNumber");
const elSwitches = document.getElementById("bitSwitches");
const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnReset = document.getElementById("btnReset");
let bits = new Array(BIT_COUNT).fill(false);
function renderSwitches() {
elSwitches.innerHTML = "";
bitValues.forEach((value, index) => {
const id = `bit-${value}`;
const wrapper = document.createElement("div");
wrapper.className = "switch-col";
const labelTop = document.createElement("div");
labelTop.className = "bit-label";
labelTop.textContent = value;
const label = document.createElement("label");
label.className = "rocker";
label.setAttribute("for", id);
const input = document.createElement("input");
input.type = "checkbox";
input.id = id;
input.checked = bits[index];
input.addEventListener("change", () => {
bits[index] = input.checked;
updateNumbers();
});
const span = document.createElement("span");
span.className = "rocker-body";
span.setAttribute("aria-hidden", "true");
label.appendChild(input);
label.appendChild(span);
wrapper.appendChild(labelTop);
wrapper.appendChild(label);
elSwitches.appendChild(wrapper);
});
}
function updateNumbers() {
const binary = bits.map(b => (b ? "1" : "0")).join("");
const denary = bits.reduce((acc, b, i) => acc + (b ? bitValues[i] : 0), 0);
elBinary.textContent = binary;
elDenary.textContent = denary.toString();
}
function resetAll() {
bits = new Array(BIT_COUNT).fill(false);
renderSwitches();
updateNumbers();
}
function requestCustomDenary() {
let input = prompt(`Enter a denary number (0 to 255):`);
if (input === null) return;
const n = Number.parseInt(input, 10);
if (Number.isNaN(n) || n < 0 || n > 255) {
alert("Invalid input. Enter a number from 0 to 255.");
return;
}
let remaining = n;
bits = bitValues.map(v => {
if (remaining >= v) {
remaining -= v;
return true;
}
return false;
});
renderSwitches();
updateNumbers();
}
function requestCustomBinary() {
let input = prompt(`Enter an ${BIT_COUNT}-bit binary number (e.g. 01010101):`);
if (input === null) return;
input = input.trim();
const re = new RegExp(`^[01]{${BIT_COUNT}}$`);
if (!re.test(input)) {
alert(`Invalid input. Enter exactly ${BIT_COUNT} digits using only 0 or 1.`);
return;
}
bits = input.split("").map(c => c === "1");
renderSwitches();
updateNumbers();
}
btnCustomDenary?.addEventListener("click", requestCustomDenary);
btnCustomBinary?.addEventListener("click", requestCustomBinary);
btnReset?.addEventListener("click", resetAll);
renderSwitches();
updateNumbers();

View File

@@ -0,0 +1,72 @@
// public/js/tools/unsigned-binary.js
// Lightweight: no frameworks. Works on weak devices.
const BIT_COUNT = 8;
const MAX_DENARY = 255;
let bits = new Array(BIT_COUNT).fill(false);
function bitsToBinaryString(){
return bits.map(b => (b ? "1" : "0")).join("");
}
function bitsToDenary(){
// MSB on the left: 128..1
const weights = [128,64,32,16,8,4,2,1];
return bits.reduce((acc, b, i) => acc + (b ? weights[i] : 0), 0);
}
function render(){
const grid = document.getElementById("bitGrid");
grid.innerHTML = "";
const weights = [128,64,32,16,8,4,2,1];
bits.forEach((on, i) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn";
btn.style.width = "100%";
btn.style.justifyContent = "space-between";
btn.setAttribute("aria-pressed", on ? "true" : "false");
btn.innerHTML = `<span>${weights[i]}</span><b>${on ? "1" : "0"}</b>`;
btn.addEventListener("click", () => {
bits[i] = !bits[i];
update();
});
grid.appendChild(btn);
});
}
function update(){
document.getElementById("binaryNumber").innerText = bitsToBinaryString();
document.getElementById("denaryNumber").innerText = bitsToDenary();
render();
}
function requestBinary(){
let v;
do{
v = prompt("Enter an 8-bit binary value (8 digits, only 0 or 1):", bitsToBinaryString());
if(v === null) return;
v = v.trim();
}while(!/^[01]{8}$/.test(v));
bits = v.split("").map(ch => ch === "1");
update();
}
function requestDenary(){
let v;
do{
v = prompt("Enter a denary value (0 to 255):", String(bitsToDenary()));
if(v === null) return;
v = Number(v);
}while(!Number.isInteger(v) || v < 0 || v > MAX_DENARY);
// set bits from MSB to LSB
const weights = [128,64,32,16,8,4,2,1];
bits = weights.map(w => {
if(v >= w){ v -= w; return true; }
return false;
});
update();
}
function reset(){
bits = new Array(BIT_COUNT).fill(false);
update();
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("btnCustomBinary")?.addEventListener("click", requestBinary);
document.getElementById("btnCustomDenary")?.addEventListener("click", requestDenary);
document.getElementById("btnReset")?.addEventListener("click", reset);
update();
});

96
public/styles.css Normal file
View File

@@ -0,0 +1,96 @@
/* src/styles/md3-tokens.css */
/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */
:root{
/* Typography */
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
/* Spacing + shape */
--radius-1: 10px;
--radius-2: 16px;
--radius-3: 22px;
--shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
/* Color roles (keep simple) */
--md-surface: #ffffff;
--md-surface-2: #f6f7fb;
--md-on-surface: #111318;
--md-primary: #2f6fed; /* calm blue */
--md-on-primary: #ffffff;
--md-secondary: #5a5f72; /* muted */
--md-on-secondary: #ffffff;
--md-tertiary: #0f766e; /* teal for "practical" tools */
--md-on-tertiary: #ffffff;
--md-outline: #d7dbe7;
--md-success: #1a7f37;
--md-warning: #b54708;
--md-danger: #b42318;
/* Focus ring for accessibility */
--md-focus: 0 0 0 3px rgba(47,111,237,.28);
}
@media (prefers-color-scheme: dark){
:root{
--md-surface: #0b0e14;
--md-surface-2: #121725;
--md-on-surface: #e8eaf2;
--md-primary: #9bb6ff;
--md-on-primary: #0b0e14;
--md-secondary: #b8bccd;
--md-on-secondary: #0b0e14;
--md-tertiary: #4fd1c5;
--md-on-tertiary: #0b0e14;
--md-outline: #2b3244;
--md-focus: 0 0 0 3px rgba(155,182,255,.25);
}
}
/* src/styles/base.css */
@import "./md3-tokens.css";
html, body{ height:100%; }
body{
margin:0;
font-family: var(--font-sans);
background: var(--md-surface-2);
color: var(--md-on-surface);
}
a{ color: var(--md-primary); text-decoration: none; }
a:hover{ text-decoration: underline; }
.container{
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
.card{
background: var(--md-surface);
border: 1px solid var(--md-outline);
border-radius: var(--radius-2);
box-shadow: var(--shadow-1);
padding: 16px;
}
.btn{
display:inline-flex;
gap:8px;
align-items:center;
justify-content:center;
border-radius: 999px;
border: 1px solid var(--md-outline);
background: var(--md-surface);
color: var(--md-on-surface);
padding: 10px 14px;
font-weight: 600;
cursor: pointer;
}
.btn:hover{ filter: brightness(0.98); }
.btn:focus{ outline:none; box-shadow: var(--md-focus); }
.btn-primary{
background: var(--md-primary);
color: var(--md-on-primary);
border-color: transparent;
}
.badge{
display:inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--md-outline);
background: var(--md-surface-2);
}
code, pre{ font-family: var(--font-mono); }

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

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

View File

@@ -1,4 +1,6 @@
---
import '../styles/global.css';
const { title = "Computing:Box" } = Astro.props;
---
@@ -8,105 +10,25 @@ const { title = "Computing:Box" } = Astro.props;
<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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800;900&display=swap" rel="stylesheet">
</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>
<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>
<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>
@@ -114,5 +36,12 @@ const { title = "Computing:Box" } = Astro.props;
<main class="pageWrap">
<slot />
</main>
<footer class="siteFooter">
<div class="footerInner">
<div>Computer Science Concept Simulators</div>
<div>© {new Date().getFullYear()} Computing:Box • Created with ♥ by Mr A Lyall</div>
</div>
</footer>
</body>
</html>
</html>

View File

@@ -1,22 +0,0 @@
<!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>

View File

@@ -1,24 +1,27 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/binary.css";
---
<BaseLayout title="Binary Simulator">
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxLabel">TOOLBOX</span>
</button>
<BaseLayout title="Binary Simulator | Computing:Box">
<div class="binaryPage" id="binaryPage">
<button
id="toolboxToggle"
class="toolboxToggle"
type="button"
aria-expanded="true"
>
<span class="toolboxIcon" aria-hidden="true">🧰</span>
<span class="toolboxText">TOOLBOX</span>
</button>
<main class="wrap">
<section class="topGrid">
<!-- LEFT -->
<div>
<div class="leftCol">
<div class="readout">
<div class="label">Denary</div>
<div id="denaryNumber" class="num denaryValue">0</div>
<div class="label">Binary</div>
<div id="binaryNumber" class="num binaryValue">0000 0000</div>
<div id="binaryNumber" class="num binaryValue">00000000</div>
</div>
<div class="divider"></div>
@@ -28,9 +31,7 @@ import "../styles/binary.css";
</section>
</div>
<!-- RIGHT TOOLBOX -->
<aside id="toolbox" class="panelCol" aria-label="Toolbox">
<!-- SETTINGS -->
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Settings</div>
@@ -40,7 +41,7 @@ import "../styles/binary.css";
<input id="modeToggle" type="checkbox" />
<span class="slider"></span>
</label>
<div class="toggleLabel" id="lblTwos">Two&rsquo;s complement</div>
<div class="toggleLabel" id="lblTwos">Two's complement</div>
</div>
<div class="hint" id="modeHint">
@@ -51,7 +52,6 @@ import "../styles/binary.css";
<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
@@ -66,47 +66,36 @@ import "../styles/binary.css";
aria-label="Number of bits"
/>
</div>
<button class="miniBtn" id="btnBitsUp" type="button" aria-label="Increase bits">+</button>
</div>
</div>
</div>
<!-- CUSTOM -->
<div class="card">
<div class="cardTitle">Custom</div>
<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 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="toolBtn toolWide toolRandom" id="btnRandom" type="button">
Random
</button>
<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="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 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="twoBtnRow">
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
<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="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button>
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div>
</aside>
</section>
</main>
</div>
<script type="module" src="/src/scripts/binary.js"></script>
</BaseLayout>
<script src="../scripts/binary.js"></script>
</BaseLayout>

View File

@@ -0,0 +1,99 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Hex Colours Simulator | Computing:Box">
<div class="binaryPage" id="colorPage">
<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">
<div class="leftCol">
<div class="readoutContainer">
<div class="readout">
<div class="readoutBlock">
<div class="label">Denary (R, G, B)</div>
<div id="denaryNumber" class="num denaryValue">
<span class="text-red">0</span>
<span class="text-green">0</span>
<span class="text-blue">0</span>
</div>
</div>
<div class="readoutBlock">
<div class="label">Hexadecimal</div>
<div id="hexNumber" class="num hexValue">
<span class="text-red"><span style="color:var(--muted)">#</span>00</span>
<span class="text-green">00</span>
<span class="text-blue">00</span>
</div>
</div>
<div class="readoutBlock">
<div class="label">Binary</div>
<div id="binaryNumber" class="num binaryValue">
<span class="text-red">00000000</span>
<span class="text-green">00000000</span>
<span class="text-blue">00000000</span>
</div>
</div>
</div>
<div class="colorPreviewSide">
<div class="colorBoxWrap">
<div id="previewColor" class="colorBox"></div>
<div class="colorBoxLabel">Colour</div>
</div>
<div class="colorBoxWrap">
<div id="previewInverted" class="colorBox"></div>
<div class="colorBoxLabel">Inverted</div>
</div>
</div>
</div>
<div class="divider"></div>
<section class="bitsWrap" aria-label="Hex Colour Cards">
<div class="colorGroupWrap" id="colorGrid"></div>
</section>
</div>
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Info</div>
<div class="hint" style="margin-top: 0;">
Hexadecimal colours are made of 3 channels (Red, Green, Blue). Each channel is an 8-bit value ranging from 00 to FF (0 to 255).
</div>
</div>
<div class="card">
<div class="cardTitle">Colour Tools</div>
<div class="controlsRow">
<button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button>
<button class="btn btnAccent btnHalf" id="btnCustomRGB" type="button">Custom RGB</button>
</div>
<div class="controlsRow">
<button class="btn btnAccent btnWide" id="btnInvert" type="button">Invert Colour</button>
</div>
<button class="btn btnWide" id="btnRandom" type="button">Random Colour</button>
<div class="hint">Random runs briefly then stops automatically.</div>
</div>
<div class="card">
<div class="cardTitle">Tools</div>
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div>
</aside>
</section>
</div>
<script src="../scripts/hexColours.js"></script>
</BaseLayout>

View File

@@ -1,8 +1,94 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import HexSimulator from "../components/simulators/HexSimulator.astro";
---
<BaseLayout title="Hexadecimal | Computing:Box">
<HexSimulator />
</BaseLayout>
<BaseLayout title="Hexadecimal Simulator | Computing:Box">
<div class="binaryPage" id="hexPage">
<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">
<div class="leftCol">
<div class="readout">
<div class="label">Denary</div>
<div id="denaryNumber" class="num denaryValue">0</div>
<div class="label">Hexadecimal</div>
<div id="hexNumber" class="num hexValue">00</div>
<div class="label">Binary</div>
<div id="binaryNumber" class="num binaryValue">00000000</div>
</div>
<div class="divider"></div>
<section class="bitsWrap" aria-label="Hexadecimal sliders">
<div class="hexGrid" id="hexGrid"></div>
</section>
</div>
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Settings</div>
<div class="hint" style="margin-top: 0; margin-bottom: 14px;">
Hexadecimal represents numbers using base 16 (0-9, A-F).
</div>
<div class="subCard">
<div class="subTitle">Digit width</div>
<div class="bitWidthRow">
<button class="miniBtn" id="btnDigitsDown" type="button" aria-label="Decrease digits"></button>
<div class="bitInputWrap">
<div class="bitInputLabel">Digits</div>
<input
id="digitsInput"
class="bitInput"
type="number"
inputmode="numeric"
min="1"
max="16"
step="1"
value="2"
aria-label="Number of hex digits"
/>
</div>
<button class="miniBtn" id="btnDigitsUp" type="button" aria-label="Increase digits">+</button>
</div>
</div>
</div>
<div class="card">
<div class="cardTitle">Custom Number</div>
<div class="controlsRow">
<button class="btn btnAccent btnHalf" id="btnCustomHex" type="button">Custom Hex</button>
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
</div>
<div class="controlsRow">
<button class="btn btnAccent btnWide" id="btnCustomBinary" type="button">Custom Binary</button>
</div>
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
<div class="hint">Random runs briefly then stops automatically.</div>
</div>
<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>
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
</div>
</aside>
</section>
</div>
<script src="../scripts/hexadecimal.js"></script>
</BaseLayout>

View File

@@ -0,0 +1,63 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/logic-gates.css";
---
<BaseLayout title="Logic Gate Builder | Computing:Box">
<div class="binaryPage" id="logicPage">
<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" style="display:flex; flex-direction:column; height: 100%;">
<div class="readout">
<div class="readoutBlock">
<div class="label" style="margin-bottom: 8px;">Interactive Simulator</div>
<div class="num denaryValue" style="font-size: 56px; line-height: 1;">LOGIC GATES</div>
</div>
<div style="color: var(--muted); font-size: 15px; font-family: var(--ui-font); text-align: center; max-width: 800px; margin-top: 12px;">
Drag items from the toolbox to the board. Drag from output ports to input ports to wire. Click a wire or node and press <kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:4px; font-family:var(--ui-font);">Delete</kbd> to remove it.
</div>
</div>
<div class="divider"></div>
<div class="lg-workspace" id="workspace">
<svg class="lg-svg-layer" id="wireLayer"></svg>
</div>
</section>
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
<div class="card">
<div class="cardTitle">Components</div>
<div class="tb-icon-grid" id="toolboxGrid">
</div>
</div>
<div class="card">
<div class="cardTitle">Live Truth Table</div>
<details open>
<summary class="tt-summary">Show / Hide Table</summary>
<div style="font-family: var(--ui-font); font-size: 12px; color: var(--muted); margin-bottom: 12px;">Auto-generates based on current wiring. (Max 6 inputs)</div>
<div class="tt-table-wrap" id="truthTableContainer">
</div>
</details>
</div>
<div class="card">
<div class="cardTitle">Tools</div>
<button class="btn btnReset btnWide" id="btnClearBoard" type="button" style="margin-bottom:0;">Clear Board</button>
</div>
</aside>
</div>
<script src="../scripts/logicGates.js"></script>
</BaseLayout>

View File

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

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,29 +0,0 @@
<footer class="siteFooter">
<div class="inner">
<div class="title">Computer Science Concept Simulators</div>
<div class="meta">
© 2025 Computing:Box · Created with 💗 by Mr Lyall<br />
Powered by ADCM Networks
</div>
</div>
</footer>
<style>
.siteFooter{
border-top: 1px solid rgba(255,255,255,.10);
background: rgba(0,0,0,.10);
}
.inner{
max-width:1200px;
margin:0 auto;
padding: 18px 20px;
color: rgba(255,255,255,.65);
font-size: 12px;
line-height: 1.6;
}
.title{
color: rgba(255,255,255,.80);
font-weight: 800;
margin-bottom: 6px;
}
</style>

View File

@@ -1,57 +0,0 @@
<header class="siteHeader">
<div class="inner">
<div class="brand">
<a href="/">Computing:Box</a>
</div>
<nav class="nav">
<a href="/binary">Binary</a>
<a href="/hexadecimal">Hexadecimal</a>
<a href="/hex-colours">Hex Colours</a>
<a href="/logic-gates">Logic Gates</a>
<a href="/about">About</a>
</nav>
</div>
</header>
<style>
.siteHeader{
height: 64px;
display:flex;
align-items:center;
border-bottom: 1px solid rgba(255,255,255,.10);
background: rgba(0,0,0,.10);
backdrop-filter: blur(8px);
}
.inner{
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0 20px;
display:flex;
align-items:center;
justify-content: space-between;
gap: 18px;
}
.brand a{
color:#fff;
text-decoration:none;
font-weight: 800;
letter-spacing: .02em;
}
.nav{
display:flex;
gap: 16px;
flex-wrap: wrap;
}
.nav a{
color: rgba(255,255,255,.78);
text-decoration:none;
font-weight: 700;
font-size: 13px;
letter-spacing:.02em;
}
.nav a:hover{
color:#fff;
}
</style>

View File

@@ -1,210 +0,0 @@
---
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>

View File

@@ -1,104 +0,0 @@
---
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>

View File

@@ -1,346 +0,0 @@
/* ================= 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; }
}

View File

@@ -1,232 +0,0 @@
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) (09, AF).`;
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 09 and AF 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();

View File

@@ -1,118 +0,0 @@
---
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>

View File

@@ -1,22 +0,0 @@
<!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>

View File

@@ -1,115 +0,0 @@
---
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&apos;s&nbsp;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>

View File

@@ -1,8 +0,0 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import HexSimulator from "../components/simulators/HexSimulator.astro";
---
<BaseLayout title="Hexadecimal | Computing:Box">
<HexSimulator />
</BaseLayout>

View File

@@ -1,11 +0,0 @@
---
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>

View File

@@ -1,522 +0,0 @@
// 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 twos 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);
})();

View File

@@ -1,342 +0,0 @@
/*
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; }
}

View File

@@ -1,85 +0,0 @@
: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;
}

View File

@@ -1,75 +0,0 @@
: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;
}

52
src/styles/base.css Normal file
View File

@@ -0,0 +1,52 @@
/* src/styles/base.css */
@import "./md3-tokens.css";
html, body{ height:100%; }
body{
margin:0;
font-family: var(--font-sans);
background: var(--md-surface-2);
color: var(--md-on-surface);
}
a{ color: var(--md-primary); text-decoration: none; }
a:hover{ text-decoration: underline; }
.container{
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
.card{
background: var(--md-surface);
border: 1px solid var(--md-outline);
border-radius: var(--radius-2);
box-shadow: var(--shadow-1);
padding: 16px;
}
.btn{
display:inline-flex;
gap:8px;
align-items:center;
justify-content:center;
border-radius: 999px;
border: 1px solid var(--md-outline);
background: var(--md-surface);
color: var(--md-on-surface);
padding: 10px 14px;
font-weight: 600;
cursor: pointer;
}
.btn:hover{ filter: brightness(0.98); }
.btn:focus{ outline:none; box-shadow: var(--md-focus); }
.btn-primary{
background: var(--md-primary);
color: var(--md-on-primary);
border-color: transparent;
}
.badge{
display:inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--md-outline);
background: var(--md-surface-2);
}
code, pre{ font-family: var(--font-mono); }

View File

@@ -1,326 +0,0 @@
/* Binary page styles (moved OUT of binary.astro) */
:root{
--panel-w: 360px;
--gap: 22px;
}
.wrap{
max-width: 1400px;
margin: 0 auto;
padding: 26px 20px 48px;
position: relative;
}
.topGrid{
display: grid;
grid-template-columns: 1fr var(--panel-w);
gap: var(--gap);
align-items: start;
}
/* When toolbox is hidden, reclaim space + centre content */
body.toolboxClosed .topGrid{
grid-template-columns: 1fr;
}
body.toolboxClosed #toolboxPanel{
display: none;
}
.mainCol{
min-width: 0;
}
.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;
}
.bitsWrap{
padding-top: 6px;
}
.bitsGrid{
display: grid;
gap: 24px;
justify-content: center;
}
/* Default: a single row of bits (will wrap automatically as bit count grows) */
.bitsGrid{
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
max-width: 1200px;
margin: 0 auto;
}
.bitsGrid.bitsFew{
justify-content: center;
}
/* Bit tile */
.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 toggle button */
.toolboxToggle{
position: absolute;
right: 20px;
top: 18px;
z-index: 20;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 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;
}
.toolboxText{
letter-spacing: .12em;
font-weight: 900;
}
/* Toolbox panel */
.panelCol{
position: sticky;
top: calc(var(--nav-h, 72px) + 18px);
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;
}
/* 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;
}
.btnWide{ width: 100%; }
.btnAccent{
background: rgba(0,255,140,.12);
border-color: rgba(0,255,140,.22);
}
.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);
}

11
src/styles/fonts.css Normal file
View File

@@ -0,0 +1,11 @@
@font-face {
font-family: "DSEG7";
src: url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
.dseg {
font-family: "DSEG7", monospace;
letter-spacing: 0.15em;
}

View File

@@ -1,85 +1,190 @@
:root{
/* Global fonts */
@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;
}
@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;
}
:root {
--nav-h: 92px;
--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);
--line: rgba(255,255,255,.10);
--ui-font: "Inter", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--num-font: "DSEG7Classic", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--bit-font: "SevenSegment", monospace;
}
*{ box-sizing:border-box; }
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--ui-font); display: flex; flex-direction: column; }
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
/* --- BASE LAYOUT --- */
.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: 2.5em; height: 2.5em; image-rendering: pixelated; }
.brandName { letter-spacing: .12em; font-weight: 900; 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; }
.navLinks a:hover, .navLinks a.active { color: #e8e8ee; }
.pageWrap { flex: 1; max-width: 1400px; margin: 0 auto; padding: 0 20px 40px; width: 100%; display: flex; flex-direction: column; }
.siteFooter { border-top: 1px solid var(--line); background: rgba(0,0,0,.08); }
.footerInner { max-width: 1400px; margin: 0 auto; padding: 18px 20px; color: var(--muted); font-size: 12px; letter-spacing: .08em; display: flex; flex-direction: column; gap: 6px; }
/* --- APP LAYOUT --- */
.binaryPage {
--toolbox-w: 360px;
--toolbox-gap: 22px;
--toolbox-toggle-top: calc(var(--nav-h) + 16px);
--toolbox-top: calc(var(--toolbox-toggle-top) + 60px);
position: relative; padding-top: 16px; flex: 1; display: flex; flex-direction: column;
}
.binaryPage:not(.toolboxCollapsed) { padding-right: calc(var(--toolbox-w) + var(--toolbox-gap)); }
.binaryPage.toolboxCollapsed { padding-right: 0; }
.topGrid { display: flex; align-items: stretch; gap: 28px; flex: 1; }
.leftCol { flex: 1 1 auto; min-width: 0; container-type: inline-size; display: flex; flex-direction: column; }
/* --- READOUT FORMATTING --- */
.readoutContainer { display: flex; align-items: center; justify-content: center; gap: 64px; width: 100%; }
.readout { display: flex; flex-direction: column; align-items: center; gap: 16px; padding-top: 4px; }
.readoutBlock { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.label { font-family: var(--bit-font); letter-spacing: .14em; text-transform: uppercase; font-size: 18px; opacity: .75; margin: 0; }
.num { font-family: var(--num-font); color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); letter-spacing: 2px; }
.denaryValue, .hexValue, .binaryValue { display: flex; gap: 16px; justify-content: center; white-space: pre-wrap; text-align: center; margin: 0; line-height: 1; }
.denaryValue { font-size: 56px; }
.hexValue { font-size: 48px; }
.binaryValue { font-size: 40px; }
.divider { height: 1px; background: rgba(255,255,255,.08); margin: 16px 0 16px; }
/* --- GRIDS & BITS --- */
.bitsWrap { width: 100%; }
.bitsGrid { --cols: 8; display: grid; grid-template-columns: repeat(var(--cols), minmax(92px, 1fr)); gap: 26px 22px; align-items: start; justify-items: center; }
.bitsGrid.bitsFew { justify-content: center; }
.bit { width: 100%; max-width: 140px; display: flex; flex-direction: column; align-items: center; gap: 8px; container-type: inline-size; }
.bitVal { font-family: var(--bit-font); font-size: min(32px, calc(140cqw / var(--len, 1))); letter-spacing: 2px; color: rgba(232,232,238,.85); white-space: nowrap; line-height: 1; }
.bulb { width: 44px; height: 44px; color: rgba(255,255,255,.15); margin-bottom: 8px; flex-shrink: 0; transition: 0.2s ease; background: transparent; display: flex; align-items: center; justify-content: center; }
.bulb svg { width: 100%; height: 100%; display: block; }
.bulb.on { color: #ffd86b !important; filter: drop-shadow(0 0 14px rgba(255, 216, 107, 1)) !important; }
.bulb.on svg { fill: #ffd86b !important; }
.switch { position: relative; width: 56px; height: 28px; display: inline-block; }
.switch input { display: none; }
.slider { position: absolute; inset: 0; background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.14); border-radius: 999px; transition: .2s ease; }
.slider::before { content: ""; position: absolute; width: 22px; height: 22px; left: 3px; top: 2px; background: rgba(255,255,255,.92); border-radius: 999px; transition: .2s ease; pointer-events: none; }
.switch input:checked + .slider { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.30); }
.switch input:checked + .slider::before { transform: translateX(28px); }
/* --- HEXADECIMAL --- */
.hexGrid { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), minmax(160px, 1fr)); gap: 32px 20px; align-items: start; justify-items: center; width: 100%; }
.hexGrid.bitsFew { justify-content: center; }
.hexCol { display: flex; flex-direction: column; align-items: center; width: 100%; }
/* --- HEX COLOURS SPECIFIC --- */
.colorGroupWrap { display: flex; flex-wrap: nowrap; gap: 16px; justify-content: center; width: 100%; }
.colorGroup { display: flex; gap: 12px; padding: 12px; background: rgba(255,255,255,.02); border-radius: 20px; border: 1px solid rgba(255,255,255,.05); flex: 0 1 auto; min-width: 0; }
.colorPreviewSide { display: flex; gap: 24px; align-items: center; justify-content: center; }
.colorBoxWrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.colorBox { width: 60px; height: 60px; border-radius: 12px; border: 2px solid rgba(255,255,255,.15); box-shadow: 0 4px 16px rgba(0,0,0,.4); background-color: #000000; transition: background-color 0.2s ease; }
.colorBoxLabel { font-family: var(--ui-font); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.text-red { color: #ff5555 !important; text-shadow: 0 0 18px rgba(255,85,85,.35) !important; }
.text-green { color: #28f07a !important; text-shadow: 0 0 18px rgba(40,240,122,.35) !important; }
.text-blue { color: #55aaff !important; text-shadow: 0 0 18px rgba(85,170,255,.35) !important; }
/* HEX CARD */
.hexCard { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 16px 14px; display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 190px; flex: 0 1 auto; min-width: 0; box-shadow: 0 4px 24px rgba(0,0,0,.2); backdrop-filter: blur(10px); }
.hexCardButtons { display: flex; gap: 10px; }
.hexCardBtn { width: 38px; height: 38px; border-radius: 10px; border: 1px solid rgba(255,255,255,.12); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; color: rgba(232,232,238,.92); transition: all 0.2s ease; }
.hexCardBtn.inc { background: rgba(40,240,122,.15); border-color: rgba(40,240,122,.25); }
.hexCardBtn.inc:hover { background: rgba(40,240,122,.25); border-color: rgba(40,240,122,.4); }
.hexCardBtn.dec { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.25); }
.hexCardBtn.dec:hover { background: rgba(255,80,80,.28); border-color: rgba(255,80,80,.4); }
.hexDigitDisplay { font-family: var(--num-font); font-size: 48px; color: #28f07a; text-shadow: 0 0 18px rgba(40,240,122,.35); line-height: 1; }
.hexNibbleRow { display: flex; gap: 10px; justify-content: center; width: 100%; }
.hexNibbleBit { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.hexNibbleBulb { width: 32px !important; height: 32px !important; margin-bottom: 2px !important; }
.hexNibbleLabel { font-family: var(--bit-font); font-size: 24px; color: rgba(232,232,238,.6); }
.hexColWeight { font-family: var(--bit-font); font-size: 40px; color: rgba(232,232,238,.6); margin-top: 14px; }
/* --- TOOLBOX --- */
.toolboxToggle { position: fixed; top: var(--toolbox-toggle-top); right: 22px; z-index: 90; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15); backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font); font-weight: 800; font-size: 16px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; }
.toolboxIcon { font-size: 20px; filter: drop-shadow(0 0 8px rgba(255,105,180,.35)); }
.toolboxToggle:hover { border-color: rgba(255,255,255,.22); }
.panelCol { position: fixed; top: var(--toolbox-top); right: 22px; width: var(--toolbox-w); z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0); opacity: 1; transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease; }
.binaryPage.toolboxCollapsed .panelCol { transform: translateX(calc(var(--toolbox-w) + 32px)); opacity: 0; pointer-events: none; }
.card { background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.10); border-radius: 16px; padding: 16px; backdrop-filter: blur(10px); }
.cardTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 18px; color: rgba(232,232,238,.9); margin-bottom: 12px; }
.hint { font-family: var(--bit-font); font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: rgba(232,232,238,.55); margin-top: 10px; line-height: 1.35; }
.toggleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.toggleLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; color: var(--muted); transition: color 0.2s, text-shadow 0.2s; }
.toggleLabel.activeMode { color: #28f07a; text-shadow: 0 0 12px rgba(40,240,122,.45); }
.subCard { margin-top: 12px; padding: 12px; border-radius: 14px; border: 1px solid rgba(255,255,255,.10); background: rgba(0,0,0,.12); }
.subTitle { font-family: var(--bit-font); font-weight: 900; letter-spacing: .14em; text-transform: uppercase; font-size: 16px; margin-bottom: 10px; opacity: .85; }
.bitWidthRow { display: flex; align-items: center; gap: 10px; }
.miniBtn { width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.9); font-family: var(--bit-font); font-weight: 900; font-size: 22px; cursor: pointer; }
.miniBtn:hover { border-color: rgba(255,255,255,.22); }
.bitInputWrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,.10); background: rgba(255,255,255,.04); padding: 10px 12px; }
.bitInputLabel { font-family: var(--bit-font); font-size: 16px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; }
.bitInput { width: 70px; text-align: right; font-family: var(--num-font); font-size: 28px; letter-spacing: 2px; color: #28f07a; background: transparent; border: none; outline: none; }
.btn { border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); border-radius: 12px; padding: 10px 12px; font-family: var(--bit-font); font-size: 14px; letter-spacing: .12em; text-transform: uppercase; font-weight: 900; cursor: pointer; }
.btn:hover { border-color: rgba(255,255,255,.22); }
.btnAccent { background: rgba(40,240,122,.12); border-color: rgba(40,240,122,.22); }
.btnAccent:hover { border-color: rgba(40,240,122,.35); }
.btnHalf { width: calc(50% - 6px); }
.btnWide { width: 100%; }
.controlsRow { display: flex; gap: 12px; margin-bottom: 12px; }
.toolRowCentered { display: flex; justify-content: center; gap: 10px; margin-bottom: 10px; }
.toolBtn { width: 46px; height: 46px; border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: rgba(232,232,238,.92); font-family: var(--bit-font); font-size: 16px; font-weight: 900; cursor: pointer; }
.toolDec { background: rgba(255,80,80,.20); border-color: rgba(255,80,80,.25); }
.toolInc { background: rgba(40,240,122,.18); border-color: rgba(40,240,122,.25); }
.btnReset { color: rgba(232,232,238,.95); }
.btnReset:hover { background: rgba(255,80,80,.18); border-color: rgba(255,80,80,.35); }
/* === CONTAINER QUERIES === */
@container (max-width: 1050px) {
.readoutContainer { gap: 40px; }
.colorGroupWrap { gap: 10px; }
.colorGroup { padding: 10px; gap: 8px; border-radius: 16px; }
.hexCard { padding: 12px 8px; width: 140px; gap: 12px; }
.hexDigitDisplay { font-size: 40px; }
.hexNibbleBulb { width: 24px !important; height: 24px !important; }
.hexNibbleLabel { font-size: 20px; }
.hexColWeight { font-size: 26px; margin-top: 10px; }
.hexCardBtn { width: 34px; height: 34px; font-size: 14px; }
}
.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);
@container (max-width: 800px) {
.readoutContainer { flex-direction: column; gap: 24px; }
.colorPreviewSide { padding-top: 0; }
.colorGroupWrap { gap: 6px; }
.colorGroup { padding: 6px; gap: 6px; border-radius: 12px; }
.hexCard { padding: 8px 4px; width: 90px; gap: 8px; border-radius: 10px; }
.hexDigitDisplay { font-size: 32px; }
.hexNibbleBulb { width: 16px !important; height: 16px !important; }
.hexNibbleLabel { font-size: 16px; }
.hexColWeight { font-size: 20px; margin-top: 6px; }
.hexCardBtn { width: 28px; height: 28px; font-size: 12px; }
.denaryValue, .hexValue, .binaryValue { font-size: 32px; gap: 10px; }
}
.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;
}
@media (max-width: 1100px) { .binaryPage { --toolbox-w: 330px; } .denaryValue { font-size: 48px; } .hexValue { font-size: 40px; } .binaryValue { font-size: 32px; } }
@media (max-width: 900px) { .binaryPage { --toolbox-w: 320px; } .bitsGrid { grid-template-columns: repeat(var(--cols), minmax(84px, 1fr)); } .hexGrid { grid-template-columns: repeat(var(--cols), minmax(130px, 1fr)); } }

163
src/styles/logic-gates.css Normal file
View File

@@ -0,0 +1,163 @@
/* === LOGIC GATES CANVAS CSS === */
.lg-workspace {
position: relative;
flex: 1;
width: 100%;
min-height: 750px;
background-color: transparent;
background-image: radial-gradient(rgba(255,255,255,0.12) 1px, transparent 1px);
background-size: 24px 24px;
border: none;
border-radius: 0;
box-shadow: none;
overflow: hidden;
}
.lg-svg-layer {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
/* Wires */
.lg-wire {
stroke: rgba(255,255,255,0.25);
stroke-width: 6;
fill: none;
stroke-linecap: round;
transition: stroke 0.1s ease, filter 0.1s ease, stroke-width 0.1s ease;
pointer-events: stroke; /* Allows wires to be clicked */
cursor: pointer;
}
.lg-wire:hover {
stroke: rgba(255,255,255,0.6);
stroke-width: 10;
}
.lg-wire.active {
stroke: #28f07a;
filter: drop-shadow(0 0 6px rgba(40,240,122,0.6));
}
.lg-wire.active:hover { stroke: #5dff9e; }
.lg-wire.selected {
stroke: #ff5555 !important;
stroke-width: 8 !important;
stroke-dasharray: 8 8;
filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important;
animation: wireDash 1s linear infinite;
}
@keyframes wireDash { to { stroke-dashoffset: -16; } }
.lg-wire-temp {
stroke: rgba(255,255,255,0.4);
stroke-dasharray: 8 8;
pointer-events: none;
}
/* Nodes (Borderless & Transparent) */
.lg-node {
position: absolute;
background: transparent;
border: none;
border-radius: 0;
padding: 4px;
display: flex;
flex-direction: column;
align-items: center;
cursor: grab;
z-index: 10;
box-shadow: none;
backdrop-filter: none;
user-select: none;
transition: filter 0.2s;
}
.lg-node:active { cursor: grabbing; z-index: 20; }
.lg-node.selected {
filter: drop-shadow(0 0 10px rgba(255,85,85,0.8));
}
/* Node Labels (Seven-Segment, +2 Sizes Bigger) */
.lg-header {
font-size: 24px;
color: var(--muted);
font-family: var(--bit-font);
letter-spacing: 2px;
pointer-events: none;
margin-bottom: 6px;
}
/* Container mapping SVGs to absolutely positioned connection dots */
.lg-gate-container {
position: relative;
display: inline-flex;
align-items: center;
}
.lg-gate-svg {
width: 100px;
height: 50px;
display: block;
}
.lg-line-svg {
width: 30px;
height: 50px;
display: block;
}
/* Connection Ports */
.lg-port {
width: 16px; height: 16px; background: #a9acb8; border-radius: 50%; cursor: crosshair;
border: 3px solid var(--bg); box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s;
position: absolute; z-index: 5;
transform: translate(-50%, -50%); /* Centers the dot exactly over the coordinate */
}
.lg-port:hover { transform: translate(-50%, -50%) scale(1.3); background: #fff; }
.lg-port.active { background: #28f07a; box-shadow: 0 0 12px rgba(40,240,122,0.8); border-color: #1f2027; }
/* Draggable Toolbox Visual Gates Grid */
.tb-icon-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.tb-icon-box {
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15);
border-radius: 12px; width: 100%; padding: 12px 0;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
cursor: grab; transition: background 0.2s, border-color 0.2s;
}
.tb-icon-box:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); }
.tb-icon-label { font-family: var(--ui-font); font-size: 11px; font-weight: 800; color: var(--text); letter-spacing: 1px; text-transform: uppercase; }
/* Toolbox Scroll Fix */
.panelCol {
max-height: calc(100vh - var(--nav-h) - 30px) !important;
overflow-y: auto;
padding-bottom: 30px;
pointer-events: auto;
}
/* Truth Table */
.tt-summary {
font-family: var(--ui-font); font-size: 14px; font-weight: 800;
color: var(--accent, #28f07a); cursor: pointer; user-select: none;
outline: none; margin-bottom: 10px; text-transform: uppercase;
}
.tt-table-wrap {
width: 100%; max-height: 300px; overflow-y: auto; overflow-x: auto;
border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2);
}
.tt-table {
width: 100%; border-collapse: collapse; text-align: center;
font-family: var(--num-font); font-size: 14px; color: #e8e8ee;
}
.tt-table th {
position: sticky; top: 0; background: rgba(31,32,39,0.95);
padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.15);
color: var(--muted); font-family: var(--bit-font); font-weight: normal;
}
.tt-table td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.tt-table .tt-on { color: #28f07a; text-shadow: 0 0 8px rgba(40,240,122,0.5); }

43
src/styles/md3-tokens.css Normal file
View File

@@ -0,0 +1,43 @@
/* src/styles/md3-tokens.css */
/* MD3-inspired tokens tuned for education: high readability, clear contrast, calm surfaces */
:root{
/* Typography */
--font-sans: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
/* Spacing + shape */
--radius-1: 10px;
--radius-2: 16px;
--radius-3: 22px;
--shadow-1: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
/* Color roles (keep simple) */
--md-surface: #ffffff;
--md-surface-2: #f6f7fb;
--md-on-surface: #111318;
--md-primary: #2f6fed; /* calm blue */
--md-on-primary: #ffffff;
--md-secondary: #5a5f72; /* muted */
--md-on-secondary: #ffffff;
--md-tertiary: #0f766e; /* teal for "practical" tools */
--md-on-tertiary: #ffffff;
--md-outline: #d7dbe7;
--md-success: #1a7f37;
--md-warning: #b54708;
--md-danger: #b42318;
/* Focus ring for accessibility */
--md-focus: 0 0 0 3px rgba(47,111,237,.28);
}
@media (prefers-color-scheme: dark){
:root{
--md-surface: #0b0e14;
--md-surface-2: #121725;
--md-on-surface: #e8eaf2;
--md-primary: #9bb6ff;
--md-on-primary: #0b0e14;
--md-secondary: #b8bccd;
--md-on-secondary: #0b0e14;
--md-tertiary: #4fd1c5;
--md-on-tertiary: #0b0e14;
--md-outline: #2b3244;
--md-focus: 0 0 0 3px rgba(155,182,255,.25);
}
}