diff --git a/content/programming/blog/tairu.tree b/content/programming/blog/tairu.tree index 01f01cb..3336f46 100644 --- a/content/programming/blog/tairu.tree +++ b/content/programming/blog/tairu.tree @@ -270,38 +270,42 @@ styles = ["tairu.css"] % id = "01HPQCCV4R557T2SN7ES7Z4EJ7" - we can verify this logic with a bit of code; with a bit of luck, we should be able to narrow down our tileset into something a lot more manageable. + % id = "01HPSY4Y19NQ6DZN10BP1KQEZN" + we'll start off by defining a bunch of variables to represent our ordinal directions: ```javascript ordinal-directions - const E = 0b00000001; - const SE = 0b00000010; - const S = 0b00000100; - const SW = 0b00001000; - const W = 0b00010000; - const NW = 0b00100000; - const N = 0b01000000; - const NE = 0b10000000; - const ALL = E | SE | S | SW | W | NW | N | NE; + export const E = 0b0000_0001; + export const SE = 0b0000_0010; + export const S = 0b0000_0100; + export const SW = 0b0000_1000; + export const W = 0b0001_0000; + export const NW = 0b0010_0000; + export const N = 0b0100_0000; + export const NE = 0b1000_0000; + export const ALL = E | SE | S | SW | W | NW | N | NE; ``` as I've already said, we represent each direction using a single bit. + % id = "01HPSY4Y19AW70YX8PPA7AS4DH" - I'm using JavaScript by the way, because it's the native programming language of your web browser. read on to the end of this tangent to see why. + % id = "01HPSY4Y19HPNXC54VP6TFFHXN" - now I don't know about you, but I find the usual C-style way of checking whether a bit is set extremely hard to read, so let's take care of that: ```javascript ordinal-directions - function isSet(integer, bit) { + export function isSet(integer, bit) { return (integer & bit) == bit; } ``` + % id = "01HPSY4Y1984H2FX6QY6K2KHKF" - now we can write a function that will remove the aforementioned redundancies. the logic is quite simple - for southeast, we only allow it to be set if both south and east are also set, and so on and so forth. ```javascript ordinal-directions // t is a tile index; variable name is short for brevity - function removeRedundancies(t) { + export function removeRedundancies(t) { if (isSet(t, SE) && (!isSet(t, S) || !isSet(t, E))) { t &= ~SE; } @@ -318,10 +322,11 @@ styles = ["tairu.css"] } ``` + % id = "01HPSY4Y19HWQQ9XBW1DDGW68T" - with that, we can find a set of all unique non-redundant combinations: ```javascript ordinal-directions - function ordinalDirections() { + export function ordinalDirections() { let unique = new Set(); for (let i = 0; i <= ALL; ++i) { unique.add(removeRedundancies(i)); @@ -330,11 +335,13 @@ styles = ["tairu.css"] } ``` + % id = "01HPSY4Y19KG8DC4SYXR1DJJ5F" - by the way, I find it quite funny how JavaScript's [`Array.prototype.sort`] defaults to ASCII ordering *for all types.* even numbers! ain't that silly? [`Array.prototype.sort`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort + % id = "01HPSY4Y19V62YKTGK3TTKEB38" - and now it's time to _Let It Cook™_: ```javascript ordinal-directions @@ -346,18 +353,62 @@ styles = ["tairu.css"] 47 ``` + % id = "01HPSY4Y194DYYDGSAT83MPQFR" - forty seven! that's how many unique tiles we actually need. + % id = "01HPSY4Y19C303Z595KNVXYYVS" - you may find pixel art tutorials saying you need forty *eight* and not forty *seven*, but that is not quite correct - the forty eighth tile is actually just the empty tile! saying it's part of the tileset is quite misleading IMO. + % id = "01HPSY4Y19TM2K2WN06HHEM3D0" - phew... the nesting's getting quite unwieldy, let's wrap up this tangent and return back to doing some bitwise autotiling! + % id = "01HPSY4Y192FZ37K3KXZM90K9J" - so in reality we actually only need 47 tiles and not 256 - that's a whole lot less, that's 81.640625% less tiles we have to draw! + % id = "01HPSY4Y19HEBWBTNMDMM0AZSC" - and it's even possible to autogenerate most of them given just a few smaller 4x4 pieces - but for now, let's not go down that path.\ maybe another time. + - so we only need to draw 47 tiles, but to actually display them in a game we still need to pack them into an image. + + - we *could* use a similar approach to the 16 tile version, but that would leave us with lots of wasted space! + + - think that with this redundancy elimination approach most of the tiles will never even be looked up by the renderer, because the bit combinations will be collapsed into a more canonical form before the lookup. + + - we could also use the approach I mentioned briefly [here][branch:01HPQCCV4RB65D5Q4RANJKGC0D], which involves introducing a lookup table - which sounds reasonable, so let's do it! + + - I don't want to write the lookup table by hand, so let's generate it! I'll reuse the redundancy elimination code from before to make this easier. + + - we'll start by obtaining our ordinal directions array again: + + ```javascript ordinal-directions + export let xToConnectionBitSet = ordinalDirections(); + ``` + + - then we'll turn that array upside down... in other words, invert the index-value relationship, so that we can look up which X position in the tile strip to use for a specific connection combination. + + remember that our array has only 256 values, so it should be pretty cheap to represent using a `Uint8Array`: + + ```javascript ordinal-directions + export let connectionBitSetToX = new Uint8Array(256); + for (let i = 0; i < xToConnectionBitSet.length; ++i) { + connectionBitSetToX[xToConnectionBitSet[i]] = i; + } + ``` + + - and there we go! we now have a mapping from our bitset to positions within the tile strip. try to play around with the code example to see which bitsets correspond to which position! + + ```javascript ordinal-directions + console.log(connectionBitSetToX[E | SE | S]); + ``` + ```output ordinal-directions + 4 + ``` + + TODO: The value from the previous output should not leak into this one. how do we do this? do we emit extra `pushMessage` calls inbetween the editors so that they know when to end? + maybe use a `classic` context instead of a module? or maybe have a way of sharing data between outputs? (return value?) + % id = "01HPD4XQPWT9N8X9BD9GKWD78F" - bitwise autotiling is a really cool technique that I've used in plenty of games in the past. @@ -423,6 +474,7 @@ styles = ["tairu.css"] % id = "01HPD4XQPWP847T0EAM0FJ88T4" - then vines + % id = "01HPSY4Y19FA2HGYE4F3Y9NJ57" - well... it's even simpler than that in terms of graphical presentation, but we'll get to that. % id = "01HPD4XQPWK58Z63X6962STADR" diff --git a/crates/treehouse/src/html/markdown.rs b/crates/treehouse/src/html/markdown.rs index 499be9d..d107fec 100644 --- a/crates/treehouse/src/html/markdown.rs +++ b/crates/treehouse/src/html/markdown.rs @@ -240,8 +240,12 @@ where program_name, } => { self.write(match kind { - LiterateCodeKind::Input => " " { + " { + " { self.write(match kind { CodeBlockKind::Fenced(language) => match CodeBlockMode::parse(&language) { - CodeBlockMode::LiterateProgram { - kind: LiterateCodeKind::Input, - .. - } => "", - CodeBlockMode::LiterateProgram { - kind: LiterateCodeKind::Output, - .. - } => "", + CodeBlockMode::LiterateProgram { .. } => "", _ => "", }, _ => "\n", diff --git a/static/css/main.css b/static/css/main.css index cbf1187..883c6e8 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -171,8 +171,7 @@ h4 { pre, code, kbd, -th-literate-editor, -th-literate-output { +th-literate-program { --recursive-mono: 1.0; --recursive-casl: 0.0; --recursive-slnt: 0.0; @@ -213,13 +212,13 @@ body { /* Make code examples a little prettier by giving them visual separation from the rest of the page */ code, -th-literate-editor { +th-literate-program { padding: 3px 4px; background-color: var(--shaded-background); border-radius: 4px; } -th-literate-editor, +th-literate-program, th-literate-output { display: block; } @@ -231,8 +230,7 @@ kbd { } pre, -th-literate-editor, -th-literate-output { +th-literate-program { padding: 8px 12px; margin: 12px 0; background-color: var(--shaded-against-background); @@ -241,22 +239,20 @@ th-literate-output { transition: background-color var(--transition-duration); } -th-literate-editor, -th-literate-output { +th-literate-program { white-space: pre; } .tree summary:hover { & pre, - & th-literate-editor, - & th-literate-output { + & th-literate-program { background-color: var(--shaded-against-background-twice); } } pre>code, -th-literate-output>code { +th-literate-program>code { padding: 0; background: none; border-radius: 0px; @@ -518,13 +514,13 @@ img[is="th-emoji"] { /* Literate programming support */ -th-literate-editor { +th-literate-program[data-mode="input"] { /* Override the cursor with an I-beam, because the editor captures clicks and does not bubble them back up to the caller */ cursor: text; } -th-literate-output { +th-literate-program[data-mode="output"] { position: relative; & code { diff --git a/static/js/components/literate-programming.js b/static/js/components/literate-programming.js index 130b56e..df457f1 100644 --- a/static/js/components/literate-programming.js +++ b/static/js/components/literate-programming.js @@ -5,74 +5,68 @@ let literatePrograms = new Map(); function getLiterateProgram(name) { if (literatePrograms.get(name) == null) { literatePrograms.set(name, { - editors: [], + frames: [], onChanged: [], + + outputCount: 0, + + nextOutputIndex() { + let index = this.outputCount; + ++this.outputCount; + return index; + } }); } return literatePrograms.get(name); } -function getLiterateProgramSourceCode(name) { - let sources = []; +function getLiterateProgramWorkerCommands(name) { + let commands = []; let literateProgram = getLiterateProgram(name); - for (let editor of literateProgram.editors) { - sources.push(editor.textContent); + for (let frame of literateProgram.frames) { + if (frame.mode == "input") { + commands.push({ kind: "module", source: frame.textContent }); + } else if (frame.mode == "output") { + commands.push({ kind: "output", expected: frame.textContent }); + } } - return sources.join("\n"); + return commands; } -class LiterateEditor extends HTMLElement { - constructor() { - super(); - } +class InputMode { + constructor(frame) { + this.frame = frame; - connectedCallback() { - this.literateProgramName = this.getAttribute("data-program"); - getLiterateProgram(this.literateProgramName).editors.push(this); - - this.codeJar = CodeJar(this, LiterateEditor.highlight); + this.codeJar = CodeJar(frame, InputMode.highlight); this.codeJar.onUpdate(() => { - let literateProgram = getLiterateProgram(this.literateProgramName); - for (let handler of literateProgram.onChanged) { - handler(this.literateProgramName); + for (let handler of frame.program.onChanged) { + handler(frame.programName); } }) - this.addEventListener("click", event => event.preventDefault()); + frame.addEventListener("click", event => event.preventDefault()); } - static highlight(editor) { + static highlight(frame) { // TODO: Syntax highlighting } } -customElements.define("th-literate-editor", LiterateEditor); - -function debounce(callback, timeout) { - let timeoutId = 0; - return (...args) => { - clearTimeout(timeout); - timeoutId = window.setTimeout(() => callback(...args), timeout); - }; -} - -class LiterateOutput extends HTMLElement { - constructor() { - super(); - +class OutputMode { + constructor(frame) { this.clearResultsOnNextOutput = false; - } - connectedCallback() { - this.literateProgramName = this.getAttribute("data-program"); + this.frame = frame; + + this.frame.program.onChanged.push(_ => this.evaluate()); + this.outputIndex = this.frame.program.nextOutputIndex(); + this.evaluate(); - - getLiterateProgram(this.literateProgramName).onChanged.push(_ => this.evaluate()); } - evaluate = () => { + evaluate() { // This is a small bit of debouncing. If we cleared the output right away, the page would - // jitter around irritatingly + // jitter around irritatingly. this.clearResultsOnNextOutput = true; if (this.worker != null) { @@ -80,23 +74,23 @@ class LiterateOutput extends HTMLElement { } this.worker = new Worker(`${TREEHOUSE_SITE}/static/js/components/literate-programming/worker.js`, { type: "module", - name: `evaluate LiterateOutput ${this.literateProgramName}` + name: `evaluate LiterateOutput ${this.frame.programName}` }); this.worker.addEventListener("message", event => { let message = event.data; if (message.kind == "evalComplete") { this.worker.terminate(); - } else if (message.kind == "output") { + } else if (message.kind == "output" && message.outputIndex == this.outputIndex) { this.addOutput(message.output); } }); this.worker.postMessage({ action: "eval", - input: getLiterateProgramSourceCode(this.literateProgramName), + input: getLiterateProgramWorkerCommands(this.frame.programName), }); - }; + } addOutput(output) { if (this.clearResultsOnNextOutput) { @@ -112,10 +106,13 @@ class LiterateOutput extends HTMLElement { line.classList.add("output"); line.classList.add(output.kind); - line.textContent = output.message.map(x => { - if (typeof x === "object") return JSON.stringify(x); - else return x + ""; - }).join(" "); + // One day this will be more fancy. Today is not that day. + line.textContent = output.message + .map(x => { + if (typeof x === "object") return JSON.stringify(x); + else return x + ""; + }) + .join(" "); if (output.kind == "result") { let returnValueText = document.createElement("span"); @@ -124,12 +121,30 @@ class LiterateOutput extends HTMLElement { line.insertBefore(returnValueText, line.firstChild); } - this.appendChild(line); + this.frame.appendChild(line); } clearResults() { - this.replaceChildren(); + this.frame.replaceChildren(); } } -customElements.define("th-literate-output", LiterateOutput); +class LiterateProgram extends HTMLElement { + connectedCallback() { + this.programName = this.getAttribute("data-program"); + this.program.frames.push(this); + + this.mode = this.getAttribute("data-mode"); + if (this.mode == "input") { + this.modeImpl = new InputMode(this); + } else if (this.mode == "output") { + this.modeImpl = new OutputMode(this); + } + } + + get program() { + return getLiterateProgram(this.programName); + } +} + +customElements.define("th-literate-program", LiterateProgram); diff --git a/static/js/components/literate-programming/worker.js b/static/js/components/literate-programming/worker.js index aedf20f..0dc220f 100644 --- a/static/js/components/literate-programming/worker.js +++ b/static/js/components/literate-programming/worker.js @@ -1,26 +1,50 @@ -console = { +let outputIndex = 0; + +let debugLog = console.log; + +globalThis.console = { log(...message) { postMessage({ kind: "output", output: { kind: "log", message: [...message], - } + }, + outputIndex, }); } }; -addEventListener("message", event => { +async function withTemporaryGlobalScope(callback) { + let state = { + oldValues: {}, + set(key, value) { + this.oldValues[key] = globalThis[key]; + globalThis[key] = value; + } + }; + await callback(state); + for (let key in state.oldValues) { + globalThis[key] = state.oldValues[key]; + } +} + +addEventListener("message", async event => { let message = event.data; if (message.action == "eval") { + outputIndex = 0; try { - let func = new Function(message.input); - let result = func.apply({}); - postMessage({ - kind: "output", - output: { - kind: "result", - message: [result], + await withTemporaryGlobalScope(async scope => { + for (let command of message.input) { + if (command.kind == "module") { + let blobUrl = URL.createObjectURL(new Blob([command.source], { type: "text/javascript" })); + let module = await import(blobUrl); + for (let exportedKey in module) { + scope.set(exportedKey, module[exportedKey]); + } + } else if (command.kind == "output") { + ++outputIndex; + } } }); } catch (error) { @@ -29,7 +53,8 @@ addEventListener("message", event => { output: { kind: "error", message: [error.toString()], - } + }, + outputIndex, }); } diff --git a/static/js/tairu/tairu.js b/static/js/tairu/tairu.js index 93cd094..cb3e256 100644 --- a/static/js/tairu/tairu.js +++ b/static/js/tairu/tairu.js @@ -36,7 +36,6 @@ export class TileEditor extends Frame { this.tileSize = parseInt(this.getAttribute("data-tile-size")); let tilemapId = this.getAttribute("data-tilemap-id"); - console.log(tilemapRegistry); if (tilemapId != null) { this.tilemap = tilemapRegistry[this.getAttribute("data-tilemap-id")]; } else { @@ -188,5 +187,3 @@ export class TileEditor extends Frame { } } defineFrame("tairu-editor", TileEditor); - -console.log("tairu editor loaded"); diff --git a/static/js/tree.js b/static/js/tree.js index b401397..9100b1f 100644 --- a/static/js/tree.js +++ b/static/js/tree.js @@ -163,15 +163,12 @@ function expandDetailsRecursively(element) { } function navigateToPage(page) { - console.log(page); window.location.pathname = `${page}.html` } async function navigateToBranch(fragment) { if (fragment.length == 0) return; - console.log(`nagivating to branch: ${fragment}`); - let element = document.getElementById(fragment); if (element !== null) { // If the element is already loaded on the page, we're good. @@ -179,7 +176,6 @@ async function navigateToBranch(fragment) { rehash(); } else { // The element is not loaded, we need to load the tree that has it. - console.log("element is not loaded"); let parts = fragment.split(':'); if (parts.length >= 2) { let [page, _id] = parts; @@ -189,7 +185,6 @@ async function navigateToBranch(fragment) { // navigation maps with roots other than index. Currently though only index is // generated so that doesn't matter. let [_root, ...path] = fullPath; - console.log(`_root: ${_root}, path: ${path}`) if (path !== undefined) { let isNotAtIndexHtml = window.location.pathname != "" &&