diff --git a/.gitea/workflows/pre-release.yaml b/.gitea/workflows/pre-release.yaml index fad7df3..f965a20 100644 --- a/.gitea/workflows/pre-release.yaml +++ b/.gitea/workflows/pre-release.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout (full history + tags) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 3204a7f..f2dbe21 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -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" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml deleted file mode 100644 index 3204a7f..0000000 --- a/.gitea/workflows/release.yml +++ /dev/null @@ -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" diff --git a/.gitignore b/.gitignore index 90d2417..016b59e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # build output -# dist/ +dist/ # generated types .astro/ diff --git a/README.md b/README.md index fd80d69..fbc07a0 100644 --- a/README.md +++ b/README.md @@ -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: 1st September 2025 -## Version 2.0 Release Date (Goal): 1st April 2026 +## Version 2.0 Release Date (Goal): 1st May 2026 Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] diff --git a/dist/_astro/astro.CXuftnGC.svg b/dist/_astro/astro.CXuftnGC.svg deleted file mode 100644 index 8cf8fb0..0000000 --- a/dist/_astro/astro.CXuftnGC.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/dist/_astro/background.Mahwsfbs.svg b/dist/_astro/background.Mahwsfbs.svg deleted file mode 100644 index 4b2be0a..0000000 --- a/dist/_astro/background.Mahwsfbs.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/dist/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js b/dist/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js new file mode 100644 index 0000000..a23a7d5 --- /dev/null +++ b/dist/_astro/binary.astro_astro_type_script_index_0_lang.C_c_A3x5.js @@ -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(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=0;e--){const o=document.createElement("div");o.className="bit",o.innerHTML=` + +
+ + `,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{const n=Number(t.dataset.index);t.checked=!!s[n]})}function Q(){for(let t=0;tc)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;nn&&(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;e0n&&(c=c>>U),c{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)})(); diff --git a/dist/_astro/hexadecimal.C_Opoo6d.css b/dist/_astro/hexadecimal.C_Opoo6d.css deleted file mode 100644 index 80caf76..0000000 --- a/dist/_astro/hexadecimal.C_Opoo6d.css +++ /dev/null @@ -1 +0,0 @@ -@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:.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,.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:#ffffff1a}.hex-digits{margin-top:18px;display:flex;justify-content:center;gap:18px;flex-wrap:wrap}.hex-digit-col{width:160px;border-radius:18px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);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,.18)}.hex-digit-place{font-family:SevenSegment,ui-monospace,monospace;opacity:.65;font-size:14px;letter-spacing:1px}.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:.35;filter:grayscale(30%);transition:opacity .16s ease,filter .16s ease}.hex-bulb .hex-bulb-cap{width:18px;height:18px;border-radius:999px;background:#ffffff38;border:1px solid rgba(255,255,255,.14)}.hex-bulb .hex-bulb-glow{width:18px;height:10px;border-radius:999px;background:#46ff8a00;box-shadow:0 0 #46ff8a00;transition:background .16s ease,box-shadow .16s ease}.hex-bulb .hex-bulb-label{font-family:SevenSegment,ui-monospace,monospace;font-size:12px;opacity:.8}.hex-bulb.is-on{opacity:1;filter:none}.hex-bulb.is-on .hex-bulb-cap{background:#ffffff59}.hex-bulb.is-on .hex-bulb-glow{background:#46ff8a40;box-shadow:0 0 18px #46ff8a59}.hex-btn{padding:10px 12px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:#ffffff0f;color:#e7e8ee;font-weight:800;cursor:pointer;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}.hex-btn:hover{background:#ffffff1a}.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:#2ec8782e;border-color:#2ec87859}.hex-btn--green:hover{background:#2ec87842}.hex-btn--green2{background:#2ec8782e;border-color:#2ec87859}.hex-btn--red{background:#dc3c462e;border-color:#dc3c4659}.hex-btn--random.is-running{border-color:#50ffa08c;background:#2ec87838;box-shadow:0 0 18px #50ffa059;animation:hexPulseGreen .9s ease-in-out infinite}@keyframes hexPulseGreen{0%,to{box-shadow:0 0 14px #50ffa040}50%{box-shadow:0 0 26px #50ffa073}}.hex-btn--reset:hover{background:#dc3c4647;border-color:#ff505a8c;animation:hexPulseRed .9s ease-in-out infinite}@keyframes hexPulseRed{0%,to{box-shadow:0 0 12px #ff505a33}50%{box-shadow:0 0 22px #ff505a61}}.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,.14);background:#ffffff0f;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,.35))}.hex-toolbox{position:fixed;top:140px;right:28px;width:340px;display:grid;gap:14px;z-index:25;transform:translate(0);opacity:1;transition:transform .22s ease,opacity .22s ease}.hex-toolbox:not(.is-open){transform:translate(380px);opacity:0;pointer-events:none}.hex-panel{border-radius:16px;background:#ffffff0a;border:1px solid rgba(255,255,255,.1);padding:14px}.hex-panel-title{font-family:SevenSegment,ui-sans-serif,system-ui;font-size:12px;letter-spacing:2px;opacity:.7;margin-bottom:10px}.hex-setting-title{font-weight:900;opacity:.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:#00000038;border:1px solid rgba(255,255,255,.1);padding:10px 12px;display:flex;justify-content:space-between;align-items:baseline}.hex-width-label{font-family:SevenSegment,ui-sans-serif,system-ui;opacity:.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:.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:.6;letter-spacing:1px;font-family:SevenSegment,ui-sans-serif,system-ui}.hex-dialog{border:none;padding:0;background:transparent}.hex-dialog::backdrop{background:#0000008c}.hex-dialog-card{width:min(560px,92vw);border-radius:18px;background:#1a1b24;border:1px solid rgba(255,255,255,.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;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:#00000040;color:#e7e8ee;font-size:18px}.hex-dialog-hint{margin-top:10px;opacity:.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}} diff --git a/dist/binary/index.html b/dist/binary/index.html index 9799acf..be4f129 100644 --- a/dist/binary/index.html +++ b/dist/binary/index.html @@ -1,7 +1,3 @@ - Binary Simulator
Denary
0
Binary
0000 0000
\ No newline at end of file +
Bit width
Bits
Custom Number
Random runs briefly then stops automatically.
Tools
COMPUTER SCIENCE CONCEPT SIMULATORS
© 2026 COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL
\ No newline at end of file diff --git a/dist/favicon.svg b/dist/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/dist/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dist/hexadecimal/index.html b/dist/hexadecimal/index.html index ef3f400..f88bac9 100644 --- a/dist/hexadecimal/index.html +++ b/dist/hexadecimal/index.html @@ -1,5 +1,3 @@ - Hexadecimal | Computing:Box -
DENARY
0
HEXADECIMAL
00
BINARY
0000 0000
Custom
\ No newline at end of file + Hexadecimal Simulator | Computing:Box
Denary
0
Hexadecimal
00
Binary
00000000
COMPUTER SCIENCE CONCEPT SIMULATORS
© 2026 COMPUTING:BOX • CREATED WITH ♥ BY MR LYALL
\ No newline at end of file diff --git a/dist/index.html b/dist/index.html index 1e92f62..f669b69 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,5 +1,5 @@ - Astro Basics
Astro Homepage

+ Astro Basics
Astro Homepage

To get started, open the
src/pages
directory in your project.

What's New in Astro 5.0?

From content layers to server islands, click to learn more about the new features and diff --git a/dist/js/binary/unsigned-binary.js b/dist/js/binary/unsigned-binary.js new file mode 100644 index 0000000..e28ab9f --- /dev/null +++ b/dist/js/binary/unsigned-binary.js @@ -0,0 +1,115 @@ +// Browser-only script. Safe because it's loaded via + + diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 022fd45..6fa4a20 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,4 +1,6 @@ --- +import '../styles/global.css'; + const { title = "Computing:Box" } = Astro.props; --- @@ -8,105 +10,25 @@ const { title = "Computing:Box" } = Astro.props; {title} - - + + + + -

@@ -114,5 +36,12 @@ const { title = "Computing:Box" } = Astro.props;
+ +
+
+
Computer Science Concept Simulators
+
© {new Date().getFullYear()} Computing:Box • Created with ♥ by Mr A Lyall
+
+
- + \ No newline at end of file diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro deleted file mode 100644 index e455c61..0000000 --- a/src/layouts/Layout.astro +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - Astro Basics - - - - - - - diff --git a/src/pages/binary.astro b/src/pages/binary.astro index e585a28..0caf8dc 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,24 +1,27 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; -import "../styles/binary.css"; --- - - + +
+ -
- -
+
Denary
0
Binary
-
0000 0000
+
00000000
@@ -28,9 +31,7 @@ import "../styles/binary.css";
- -
-
-
Custom
- -
- - +
Custom Number
+
+ +
- - - +
Random runs briefly then stops automatically.
-
Tools
- -
- - +
+ +
- -
- - +
+ +
- - +

-
+
- - + + \ No newline at end of file diff --git a/src/pages/hex-colours.astro b/src/pages/hex-colours.astro new file mode 100644 index 0000000..d1e7e40 --- /dev/null +++ b/src/pages/hex-colours.astro @@ -0,0 +1,99 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+ + +
+
+ +
+
+
+
Denary (R, G, B)
+
+ 0 + 0 + 0 +
+
+ +
+
Hexadecimal
+
+ #00 + 00 + 00 +
+
+ +
+
Binary
+
+ 00000000 + 00000000 + 00000000 +
+
+
+ +
+
+
+
Colour
+
+
+
+
Inverted
+
+
+
+ +
+ +
+
+
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/pages/hexadecimal.astro b/src/pages/hexadecimal.astro index edfe098..e74ca17 100644 --- a/src/pages/hexadecimal.astro +++ b/src/pages/hexadecimal.astro @@ -1,8 +1,94 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; -import HexSimulator from "../components/simulators/HexSimulator.astro"; --- - - - + +
+ + +
+
+
+
Denary
+
0
+ +
Hexadecimal
+
00
+ +
Binary
+
00000000
+
+ +
+ +
+
+
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/pages/logic-gates.astro b/src/pages/logic-gates.astro new file mode 100644 index 0000000..7eb4c54 --- /dev/null +++ b/src/pages/logic-gates.astro @@ -0,0 +1,63 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import "../styles/logic-gates.css"; +--- + + +
+ + + +
+ +
+
+
Interactive Simulator
+
LOGIC GATES
+
+
+ Drag items from the toolbox to the board. Drag from output ports to input ports to wire. Click a wire or node and press Delete to remove it. +
+
+ +
+ +
+ +
+ +
+ + +
+ + +
\ No newline at end of file diff --git a/src/scripts/binary.js b/src/scripts/binary.js index a557ff9..e596092 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,6 +1,3 @@ -// src/scripts/binary.js -// Computing:Box — Binary page logic (Unsigned + Two's Complement) - (() => { /* ----------------------------- DOM @@ -12,6 +9,10 @@ const modeToggle = document.getElementById("modeToggle"); const modeHint = document.getElementById("modeHint"); + + // Connect the text labels to the JS + const lblUnsigned = document.getElementById("lblUnsigned"); + const lblTwos = document.getElementById("lblTwos"); const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); @@ -27,14 +28,13 @@ const btnBitsDown = document.getElementById("btnBitsDown"); const toolboxToggle = document.getElementById("toolboxToggle"); - const toolboxPanel = document.getElementById("toolboxPanel"); + const binaryPage = document.getElementById("binaryPage"); /* ----------------------------- STATE ----------------------------- */ let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); let bits = new Array(bitCount).fill(false); - let randomTimer = null; /* ----------------------------- @@ -54,7 +54,7 @@ } function unsignedMaxExclusive(nBits) { - return pow2Big(nBits); + return pow2Big(nBits); } function unsignedMaxValue(nBits) { @@ -94,7 +94,8 @@ function signedBigIntToBitsTwos(vSigned) { const span = pow2Big(bitCount); - let v = ((vSigned % span) + span) % span; + let v = vSigned; + v = ((v % span) + span) % span; unsignedBigIntToBits(v); } @@ -102,17 +103,33 @@ let s = ""; for (let i = bitCount - 1; i >= 0; i--) { s += bits[i] ? "1" : "0"; - const posFromRight = (bitCount - i); - if (i !== 0 && posFromRight % 4 === 0) s += " "; + const posFromLeft = (bitCount - i); + if (i !== 0 && posFromLeft % 4 === 0) s += " "; } - return s; + return s.trimEnd(); } function updateModeHint() { if (!modeHint) return; - modeHint.textContent = isTwosMode() - ? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value." - : "Tip: In unsigned binary, all bits represent positive values."; + if (isTwosMode()) { + modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value."; + } else { + modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; + } + } + + /* ----------------------------- + RESPONSIVE GRID COLS + ----------------------------- */ + function computeColsForBitsGrid() { + if (!bitsGrid) return; + const wrap = bitsGrid.parentElement; + if (!wrap) return; + + const width = wrap.getBoundingClientRect().width; + const minCell = 100; + const cols = clampInt(Math.floor(width / minCell), 1, 12); + bitsGrid.style.setProperty("--cols", String(Math.min(cols, bitCount))); } /* ----------------------------- @@ -127,18 +144,25 @@ for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; bitsGrid.innerHTML = ""; + bitsGrid.classList.toggle("bitsFew", bitCount < 8); for (let i = bitCount - 1; i >= 0; i--) { const bitEl = document.createElement("div"); bitEl.className = "bit"; + bitEl.innerHTML = ` - +
`; + 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); +})(); \ No newline at end of file diff --git a/src/scripts/hexColours.js b/src/scripts/hexColours.js new file mode 100644 index 0000000..e64be80 --- /dev/null +++ b/src/scripts/hexColours.js @@ -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 = ` +
+
+ + +
+
0
+
+ `; + + for (let j = 3; j >= 0; j--) { + cardHTML += ` +
+ +
${1 << j}
+
+ `; + } + + cardHTML += ` +
+
+
${16 ** i}
+ `; + + 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 = ` + ${rgb[0]} + ${rgb[1]} + ${rgb[2]} + `; + } + + const hexVals = rgb.map(v => v.toString(16).padStart(2, '0').toUpperCase()); + const fullHexString = `#${hexVals.join('')}`; + + if (hexEl) { + hexEl.innerHTML = ` + #${hexVals[0]} + ${hexVals[1]} + ${hexVals[2]} + `; + } + + if (binaryEl) { + binaryEl.innerHTML = ` + ${rgb[0].toString(2).padStart(8, '0')} + ${rgb[1].toString(2).padStart(8, '0')} + ${rgb[2].toString(2).padStart(8, '0')} + `; + } + + 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(); +})(); \ No newline at end of file diff --git a/src/scripts/hexadecimal.js b/src/scripts/hexadecimal.js new file mode 100644 index 0000000..8b79df1 --- /dev/null +++ b/src/scripts/hexadecimal.js @@ -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 = ` +
+
+ + +
+
0
+
+ `; + + for(let j = 3; j >= 0; j--) { + cardHTML += ` +
+ +
${1 << j}
+
+ `; + } + + cardHTML += ` +
+
+
${(1n << BigInt(i * 4)).toString()}
+ `; + + 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); + +})(); \ No newline at end of file diff --git a/src/scripts/logicGates.js b/src/scripts/logicGates.js new file mode 100644 index 0000000..a39d333 --- /dev/null +++ b/src/scripts/logicGates.js @@ -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': ``, + 'OR': ``, + 'NOT': ``, + 'NAND': ``, + 'NOR': ``, + 'XOR': ``, + 'XNOR': `` + }; + + const INPUT_SVG = ``; + const OUTPUT_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 = ` +
+
+
Input
+
+
+
+
Output
+
+ `; + + Object.keys(GATE_SVGS).forEach(gate => { + html += ` +
+ ${GATE_SVGS[gate]} +
${gate}
+
+ `; + }); + + 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 += ``; + }); + + if (wiringStart && tempWirePath) { + svgHTML += ``; + } + + 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 = '
Add inputs and outputs to generate table.
'; + return; + } + if (inNodes.length > 6) { + ttContainer.innerHTML = '
Maximum 6 inputs supported.
'; + return; + } + + let html = ''; + inNodes.forEach(n => html += ``); + outNodes.forEach(n => html += ``); + html += ''; + + 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 += ''; + inNodes.forEach(n => { + let val = override[n.id]; + html += ``; + }); + outNodes.forEach(n => { + let val = outStates[n.id]; + html += ``; + }); + html += ''; + } + + html += '
${n.label}${n.label}
${val ? 1 : 0}${val ? 1 : 0}
'; + 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 = `
${node.label}
`; + + if (node.type === 'INPUT') { + innerHTML += ` +
+ ${INPUT_SVG} +
+ `; + } + else if (node.type === 'OUTPUT') { + innerHTML += ` +
+ ${OUTPUT_SVG} +
+ `; + } + else if (node.type === 'GATE') { + const isNot = node.gateType === 'NOT'; + innerHTML += ` +
+ ${!isNot ? `
` : ''} + ${GATE_SVGS[node.gateType]} +
+ `; + } + + innerHTML += `
`; + 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 ? `` : ''; + 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); + +})(); \ No newline at end of file diff --git a/src/src/assets/astro.svg b/src/src/assets/astro.svg deleted file mode 100644 index 8cf8fb0..0000000 --- a/src/src/assets/astro.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/src/assets/background.svg b/src/src/assets/background.svg deleted file mode 100644 index 4b2be0a..0000000 --- a/src/src/assets/background.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/src/components/Footer.astro b/src/src/components/Footer.astro deleted file mode 100644 index 2dbcdbd..0000000 --- a/src/src/components/Footer.astro +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
Computer Science Concept Simulators
-
- © 2025 Computing:Box · Created with 💗 by Mr Lyall
- Powered by ADCM Networks -
-
-
- - diff --git a/src/src/components/Header.astro b/src/src/components/Header.astro deleted file mode 100644 index 6c42d5c..0000000 --- a/src/src/components/Header.astro +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/src/src/components/Welcome.astro b/src/src/components/Welcome.astro deleted file mode 100644 index 52e0333..0000000 --- a/src/src/components/Welcome.astro +++ /dev/null @@ -1,210 +0,0 @@ ---- -import astroLogo from '../assets/astro.svg'; -import background from '../assets/background.svg'; ---- - - - - diff --git a/src/src/components/simulators/HexSimulator.astro b/src/src/components/simulators/HexSimulator.astro deleted file mode 100644 index 4f212bb..0000000 --- a/src/src/components/simulators/HexSimulator.astro +++ /dev/null @@ -1,104 +0,0 @@ ---- -import "./hex/hex-simulator.css"; ---- - -
-
-
-
DENARY
-
0
- -
HEXADECIMAL
-
00
- -
BINARY
-
0000 0000
-
- -
- -
-
- - - - - - - - - -
-
Custom
- - - -
-
- -
- - -
-
-
- - -
diff --git a/src/src/components/simulators/hex/hex-simulator.css b/src/src/components/simulators/hex/hex-simulator.css deleted file mode 100644 index 8a15d61..0000000 --- a/src/src/components/simulators/hex/hex-simulator.css +++ /dev/null @@ -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; } -} diff --git a/src/src/components/simulators/hex/hex-simulator.ts b/src/src/components/simulators/hex/hex-simulator.ts deleted file mode 100644 index 08bc8aa..0000000 --- a/src/src/components/simulators/hex/hex-simulator.ts +++ /dev/null @@ -1,232 +0,0 @@ -type DialogMode = "hex" | "den" | "bin"; - -const root = document.querySelector("[data-hex-sim]"); -if (!root) throw new Error("Hex simulator root not found"); - -const outDen = root.querySelector('[data-out="denary"]')!; -const outHex = root.querySelector('[data-out="hex"]')!; -const outBin = root.querySelector('[data-out="bin"]')!; -const outDigitsRow = root.querySelector('[data-out="digitsRow"]')!; - -const toolbox = root.querySelector('[data-out="toolbox"]')!; -const toolboxBtn = root.querySelector('[data-action="toggleToolbox"]')!; -const digitsCount = root.querySelector('[data-out="digitsCount"]')!; -const bitsHint = root.querySelector('[data-out="bitsHint"]')!; -const randomBtn = root.querySelector("[data-random]")!; - -const dialog = root.querySelector('[data-out="dialog"]')!; -const dialogTitle = root.querySelector('[data-out="dialogTitle"]')!; -const dialogInput = root.querySelector('[data-out="dialogInput"]')!; -const dialogHint = root.querySelector('[data-out="dialogHint"]')!; -const dialogError = root.querySelector('[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 = ` -
- - -
- -
${digitChar}
- - -
- ${[8,4,2,1].map((w, idx) => { - const on = nibbleBits[idx] === 1; - return ` -
-
-
-
${w}
-
- `; - }).join("")} -
- -
${placeValue}
- `; - outDigitsRow.appendChild(col); - } -} - -function openDialog(mode: DialogMode): void { - stopRandom(); - dialogMode = mode; - - dialogError.textContent = ""; - dialogInput.value = ""; - - if (mode === "hex") { - dialogTitle.textContent = "Custom Hexadecimal"; - dialogHint.textContent = `Enter 1–${digits} hex digit(s) (0–9, A–F).`; - dialogInput.placeholder = "A1"; - dialogInput.inputMode = "text"; - } else if (mode === "den") { - dialogTitle.textContent = "Custom Denary"; - dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`; - dialogInput.placeholder = "42"; - dialogInput.inputMode = "numeric"; - } else { - dialogTitle.textContent = "Custom Binary"; - dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`; - dialogInput.placeholder = "00101010"; - dialogInput.inputMode = "text"; - } - - dialog.showModal(); - window.setTimeout(() => dialogInput.focus(), 0); -} - -function closeDialog(): void { - dialogMode = null; - dialogError.textContent = ""; - if (dialog.open) dialog.close(); -} - -function applyDialog(): void { - const raw = (dialogInput.value || "").trim(); - if (!dialogMode) return closeDialog(); - if (raw.length === 0) return closeDialog(); - - const max = maxForDigits(digits); - const bits = digits * 4; - - if (dialogMode === "hex") { - const v = raw.toUpperCase(); - if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; } - if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; } - value = clamp(parseInt(v, 16), 0, max); - render(); - return closeDialog(); - } - - if (dialogMode === "den") { - if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; } - const n = Number(raw); - if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; } - value = clamp(n, 0, max); - render(); - return closeDialog(); - } - - // bin - if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; } - if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; } - value = clamp(parseInt(raw, 2), 0, max); - render(); - return closeDialog(); -} - -function applyDigitDelta(i: number, delta: number): void { - stopRandom(); - const hexArr = padHex(value, digits).split(""); - let v = parseInt(hexArr[i], 16); - v = (v + delta) % 16; - if (v < 0) v += 16; - hexArr[i] = v.toString(16).toUpperCase(); - value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits)); - render(); -} - -// dialog cancel / backdrop -dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); }); -dialog.addEventListener("click", (e) => { - const card = dialog.querySelector(".hex-dialog-card"); - if (card && !card.contains(e.target as Node)) closeDialog(); -}); -dialogInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") applyDialog(); - if (e.key === "Escape") closeDialog(); -}); - -// main click handler -root.addEventListener("click", (e) => { - const btn = (e.target as HTMLElement).closest("[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(); diff --git a/src/src/layouts/BaseLayout.astro b/src/src/layouts/BaseLayout.astro deleted file mode 100644 index 022fd45..0000000 --- a/src/src/layouts/BaseLayout.astro +++ /dev/null @@ -1,118 +0,0 @@ ---- -const { title = "Computing:Box" } = Astro.props; ---- - - - - - - - {title} - - - - - - - -
- -
- - diff --git a/src/src/layouts/Layout.astro b/src/src/layouts/Layout.astro deleted file mode 100644 index e455c61..0000000 --- a/src/src/layouts/Layout.astro +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - Astro Basics - - - - - - - diff --git a/src/src/pages/binary.astro b/src/src/pages/binary.astro deleted file mode 100644 index fc195e5..0000000 --- a/src/src/pages/binary.astro +++ /dev/null @@ -1,115 +0,0 @@ ---- -import BaseLayout from "../layouts/BaseLayout.astro"; -import "../styles/binary.css"; ---- - - -
- - - -
- -
-
-
Denary
-
0
- -
Binary
- -
00000000
-
- -
- -
-
-
-
- - - -
-
- - -
diff --git a/src/src/pages/hexadecimal.astro b/src/src/pages/hexadecimal.astro deleted file mode 100644 index edfe098..0000000 --- a/src/src/pages/hexadecimal.astro +++ /dev/null @@ -1,8 +0,0 @@ ---- -import BaseLayout from "../layouts/BaseLayout.astro"; -import HexSimulator from "../components/simulators/HexSimulator.astro"; ---- - - - - diff --git a/src/src/pages/index.astro b/src/src/pages/index.astro deleted file mode 100644 index c04f360..0000000 --- a/src/src/pages/index.astro +++ /dev/null @@ -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. ---- - - - - diff --git a/src/src/scripts/binary.js b/src/src/scripts/binary.js deleted file mode 100644 index 41c1c50..0000000 --- a/src/src/scripts/binary.js +++ /dev/null @@ -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 two’s complement, the left-most bit (MSB) represents a negative value." - : "Tip: In unsigned binary, all bits represent positive values."; - } - - /* ----------------------------- - TOP BINARY DISPLAY: responsive wrap by nibble count - ----------------------------- */ - function ensureWrapMeasurer() { - if (wrapMeasureSpan || !binaryEl) return; - wrapMeasureSpan = document.createElement("span"); - wrapMeasureSpan.style.position = "absolute"; - wrapMeasureSpan.style.visibility = "hidden"; - wrapMeasureSpan.style.whiteSpace = "pre"; - wrapMeasureSpan.style.pointerEvents = "none"; - // Inherit font/letterspacing from binaryEl - wrapMeasureSpan.style.font = getComputedStyle(binaryEl).font; - wrapMeasureSpan.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing; - document.body.appendChild(wrapMeasureSpan); - } - - function computeNibblesPerLine() { - if (!binaryEl) return null; - ensureWrapMeasurer(); - - // Available width = width of the readout area (binaryEl parent) - const host = binaryEl.parentElement; - if (!host) return null; - - const hostW = host.getBoundingClientRect().width; - if (!Number.isFinite(hostW) || hostW <= 0) return null; - - // Measure one nibble including trailing space ("0000 ") - wrapMeasureSpan.textContent = "0000 "; - const nibbleW = wrapMeasureSpan.getBoundingClientRect().width || 1; - - // Safety: keep at least 1 nibble per line - const max = Math.max(1, Math.floor(hostW / nibbleW)); - return max; - } - - function formatBinaryWrapped() { - // EXACT bitCount digits (no padding to 4) - let raw = ""; - for (let i = bitCount - 1; i >= 0; i--) raw += bits[i] ? "1" : "0"; - - // If <= 4 bits, do NOT insert spaces/newlines at all - if (bitCount <= 4) return raw; - - const groups = []; - for (let i = 0; i < raw.length; i += 4) { - groups.push(raw.slice(i, i + 4)); - } - - const perLine = nibblesPerLine ?? groups.length; - if (perLine >= groups.length) return groups.join(" "); - - const lines = []; - for (let i = 0; i < groups.length; i += perLine) { - lines.push(groups.slice(i, i + perLine).join(" ")); - } - return lines.join("\n"); - } - - function refreshBinaryWrap() { - const next = computeNibblesPerLine(); - // Only update if it actually changes (prevents jitter) - if (next !== nibblesPerLine) nibblesPerLine = next; - updateReadout(); // re-render with new wrap - } - - /* ----------------------------- - BUILD UI (BITS) - ----------------------------- */ - function buildBits(count) { - bitCount = clampInt(count, 1, 64); - if (bitsInput) bitsInput.value = String(bitCount); - - const oldBits = bits.slice(); - bits = new Array(bitCount).fill(false); - for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i]; - - bitsGrid.innerHTML = ""; - - bitsGrid.classList.toggle("bitsFew", bitCount < 8); - if (bitCount < 8) { - bitsGrid.style.setProperty("--cols", String(bitCount)); - } else { - bitsGrid.style.removeProperty("--cols"); - } - - for (let i = bitCount - 1; i >= 0; i--) { - const bitEl = document.createElement("div"); - bitEl.className = "bit"; - - bitEl.innerHTML = ` - -
- - `; - - 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); -})(); diff --git a/src/src/styles/binary.css b/src/src/styles/binary.css deleted file mode 100644 index 4398c01..0000000 --- a/src/src/styles/binary.css +++ /dev/null @@ -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; } -} diff --git a/src/src/styles/global.css b/src/src/styles/global.css deleted file mode 100644 index a71bbdf..0000000 --- a/src/src/styles/global.css +++ /dev/null @@ -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; -} diff --git a/src/src/styles/site.css b/src/src/styles/site.css deleted file mode 100644 index bcaa838..0000000 --- a/src/src/styles/site.css +++ /dev/null @@ -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; -} diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..46400b8 --- /dev/null +++ b/src/styles/base.css @@ -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); } diff --git a/src/styles/binary.css b/src/styles/binary.css deleted file mode 100644 index 6655a70..0000000 --- a/src/styles/binary.css +++ /dev/null @@ -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); -} diff --git a/src/styles/fonts.css b/src/styles/fonts.css new file mode 100644 index 0000000..768cb39 --- /dev/null +++ b/src/styles/fonts.css @@ -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; +} diff --git a/src/styles/global.css b/src/styles/global.css index a71bbdf..0a6b418 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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)); } } \ No newline at end of file diff --git a/src/styles/logic-gates.css b/src/styles/logic-gates.css new file mode 100644 index 0000000..36d054d --- /dev/null +++ b/src/styles/logic-gates.css @@ -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); } \ No newline at end of file diff --git a/src/styles/md3-tokens.css b/src/styles/md3-tokens.css new file mode 100644 index 0000000..beb9cf3 --- /dev/null +++ b/src/styles/md3-tokens.css @@ -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); + } +}