diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d90262c..8485160 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,9 @@ jobs: - name: Check sources run: make check - name: Test interactive tools - run: node tests/base-converter.test.js + run: | + node tests/base-converter.test.js + node tests/bit-visualizer.test.js - name: Build HTML, EPUB, PDF and Kindle run: make all - name: Upload course artifacts diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 0965328..3257d18 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -57,6 +57,7 @@ jobs: Norsk English Basekonverterer + Bit-visualisering diff --git a/interactive/bit-visualizer/bits.js b/interactive/bit-visualizer/bits.js new file mode 100644 index 0000000..ac6fdfd --- /dev/null +++ b/interactive/bit-visualizer/bits.js @@ -0,0 +1,29 @@ +"use strict"; +const valueEl=document.querySelector("#value"), widthEl=document.querySelector("#width"); +const bitsEl=document.querySelector("#bits"), errorEl=document.querySelector("#error"); +function maxForWidth(w){return (1n<max) throw new Error(`Verdien må være mellom 0 og ${max} for ${w} bit.`); + return n; +} +function formatBinary(n,w){return n.toString(2).padStart(w,"0").replace(/(.{8})(?=.)/g,"$1 ")} +function render(){ + const w=Number(widthEl.value); let n; + try{n=parseUnsigned(valueEl.value,w);errorEl.textContent=""}catch(e){errorEl.textContent=e.message;bitsEl.replaceChildren();return} + document.querySelector("#decimal").textContent=n.toString(10); + document.querySelector("#hex").textContent="0x"+n.toString(16).toUpperCase().padStart(w/4,"0"); + document.querySelector("#binary").textContent=formatBinary(n,w); + bitsEl.replaceChildren(); + for(let pos=w-1;pos>=0;pos--){ + const mask=1n<bit ${pos}${on?1:0}2${pos}${on?mask:0n}`; + b.addEventListener("click",()=>{valueEl.value=(n^mask).toString();render()}); + bitsEl.appendChild(b); + } +} +valueEl.addEventListener("input",render);widthEl.addEventListener("change",render);render(); diff --git a/interactive/bit-visualizer/index.html b/interactive/bit-visualizer/index.html new file mode 100644 index 0000000..b3dad83 --- /dev/null +++ b/interactive/bit-visualizer/index.html @@ -0,0 +1,23 @@ + + + + +EduNumbers – Bit-visualisering + +
+

← EduNumbers

+

Bit-visualisering

+

Se hvordan et usignert heltall bygges opp av individuelle biter. Klikk på et bit for å slå det av eller på.

+
+ + + +
+
+

Desimal:

+

Hex:

+

Binær:

+
+
+

Hvert kort viser bitposisjon, 2-potens og bidraget til totalverdien. Tykkere mellomrom markerer bytegrenser.

+
\ No newline at end of file diff --git a/interactive/bit-visualizer/style.css b/interactive/bit-visualizer/style.css new file mode 100644 index 0000000..6f80a05 --- /dev/null +++ b/interactive/bit-visualizer/style.css @@ -0,0 +1,6 @@ +:root{font-family:system-ui,sans-serif;line-height:1.5;color-scheme:light dark}body{margin:0}main{max-width:72rem;margin:3rem auto;padding:0 1.25rem} +.controls{display:flex;gap:1.5rem;flex-wrap:wrap;align-items:end}.controls label{display:grid;gap:.3rem}input,select{font:inherit;padding:.45rem} +#error{flex-basis:100%;min-height:1.5em;margin:0;font-weight:600}.summary{display:flex;gap:1.5rem;flex-wrap:wrap}code{overflow-wrap:anywhere} +.bits{display:flex;flex-wrap:wrap;gap:.35rem;align-items:stretch}.bit{font:inherit;min-width:4.2rem;padding:.55rem .35rem;display:grid;gap:.15rem;text-align:center;border:1px solid #8888;border-radius:.4rem;background:transparent;cursor:pointer} +.bit[aria-pressed="true"]{outline:2px solid currentColor}.bit strong{font-size:1.5rem}.bit span{font-size:.75rem}.byte-start{margin-left:1rem}.contrib{opacity:.75}.hint{margin-top:1.5rem}a{color:inherit} +@media(max-width:600px){.byte-start{margin-left:.35rem}.bit{min-width:3.8rem}} diff --git a/tests/bit-visualizer.test.js b/tests/bit-visualizer.test.js new file mode 100644 index 0000000..d759d0f --- /dev/null +++ b/tests/bit-visualizer.test.js @@ -0,0 +1,11 @@ +const fs=require("fs"),vm=require("vm"); +const src=fs.readFileSync("interactive/bit-visualizer/bits.js","utf8"); +const start=src.indexOf("function maxForWidth"),end=src.indexOf("function render"); +const box={};vm.createContext(box);vm.runInContext(src.slice(start,end),box); +const eq=(a,b,m)=>{if(a!==b)throw new Error(m+": "+a+" != "+b)}; +eq(box.maxForWidth(8),255n,"8-bit max");eq(box.maxForWidth(16),65535n,"16-bit max"); +eq(box.maxForWidth(32),4294967295n,"32-bit max"); +eq(box.parseUnsigned("255",8),255n,"parse 255"); +eq(box.formatBinary(0x1234n,16),"00010010 00110100","binary grouping"); +for(const [v,w] of [["256",8],["-1",8],["12x",16]]){let ok=false;try{box.parseUnsigned(v,w)}catch{ok=true}if(!ok)throw new Error("Expected rejection "+v)} +console.log("Bit visualizer tests passed");