diff --git a/assets/style.css b/assets/style.css index 4e830f6..78e5c53 100644 --- a/assets/style.css +++ b/assets/style.css @@ -160,6 +160,33 @@ pre.code b { max-width: 68ch; } +.walk { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.75rem; + margin: 1.5rem 0 0; +} + +.walk .summary { + margin: 0; +} + +[data-step] { + scroll-margin-top: 2rem; + transition: color 0.2s; +} + +[data-step].here { + color: var(--accent); +} + +@media (prefers-reduced-motion: reduce) { + [data-step] { + transition: none; + } +} + .next { display: grid; grid-template-columns: repeat(3, 1fr); diff --git a/assets/walk.js b/assets/walk.js new file mode 100644 index 0000000..03148dc --- /dev/null +++ b/assets/walk.js @@ -0,0 +1,66 @@ +// The thirty seconds a recording would have shown, without the recording. +// +// A video of this page would go stale the moment the compiler said anything +// differently, and this page's whole claim is that it prints what the compiler +// prints. So the walkthrough moves through the page that is already here +// rather than a copy of it filmed in the past. +// +// No sound to turn off, and nothing that only works if you can see colour: the +// caption says what to look at and the section is scrolled to. + +const BUTTON = document.getElementById("walk"); +const CAPTION = document.getElementById("caption"); +const STEPS = Array.from(document.querySelectorAll("[data-step]")); + +const STILL = matchMedia("(prefers-reduced-motion: reduce)"); +const PAUSE = 4500; + +let at = -1; +let timer = null; + +function show(index) { + at = index; + const step = STEPS[at]; + for (const other of STEPS) other.classList.toggle("here", other === step); + CAPTION.textContent = `${at + 1} of ${STEPS.length}. ${step.dataset.step}`; + step.scrollIntoView({ + block: "center", + behavior: STILL.matches ? "auto" : "smooth", + }); +} + +function stop() { + clearTimeout(timer); + timer = null; + BUTTON.textContent = at + 1 < STEPS.length ? "Keep going" : "Start again"; +} + +function next() { + if (at + 1 >= STEPS.length) { + stop(); + return; + } + show(at + 1); + // Someone who wants to read a step should be able to. Auto-advance is off + // entirely when the reader has asked for less movement. + if (!STILL.matches) timer = setTimeout(next, PAUSE); + else stop(); +} + +BUTTON.addEventListener("click", () => { + if (timer) { + stop(); + return; + } + if (at + 1 >= STEPS.length) at = -1; + BUTTON.textContent = "Pause"; + next(); +}); + +// Anything that means "I am reading this myself" ends the tour rather than +// fighting it. +for (const event of ["wheel", "touchstart", "keydown"]) { + addEventListener(event, () => { + if (timer) stop(); + }, { passive: true }); +} diff --git a/one-clause/index.html b/one-clause/index.html index b05673a..797cf41 100644 --- a/one-clause/index.html +++ b/one-clause/index.html @@ -41,7 +41,12 @@

One clause, two worlds.

wrong, and why. --> -

Two functions

+
+ + +
+ +

Two functions

Identical, except for one line. They are demo/read_only.deed and @@ -82,7 +87,7 @@

read_write

-

Two worlds

+

Two worlds

Compiled to WebAssembly components, the import section of each module is its world. It lists what the module asks a host for, and @@ -108,7 +113,7 @@

read_write.wasm imports

code is the manifest.

-

What the host does with it

+

What the host does with it

A host that offers Io.read and not Io.save runs the first module and refuses the second. The refusal is not a @@ -134,7 +139,7 @@

What the host does with it

process executes.

-

None of this is checked by eye

+

None of this is checked by eye

The import sections, the host's refusal and the listing printed further up this page are all pinned in the compiler's repository, and @@ -156,7 +161,7 @@

None of this is checked by eye

import section with nothing to notice the prose no longer matching.

-

Try them

+

Try them

Both programs open in the playground, running the same compiler this page is describing. Add Io.save to the first one's @@ -166,14 +171,14 @@

Try them