From ca94c06c5f2272bf8a87b2739887c527fe1e43fb Mon Sep 17 00:00:00 2001 From: lqdev Date: Fri, 16 Feb 2024 22:01:19 +0100 Subject: [PATCH] wip: 47 tiles --- content/programming/blog/tairu.tree | 203 +++++-- crates/treehouse/src/html/markdown.rs | 82 ++- static/css/main.css | 70 ++- static/css/tairu.css | 101 +++- static/js/components/literate-programming.js | 135 +++++ .../components/literate-programming/worker.js | 40 ++ static/js/tairu/tilemap-registry.js | 7 + static/js/vendor/codejar.js | 511 ++++++++++++++++++ ...-heavy-metal-bitwise-16+pixel+height40.png | Bin 313 -> 0 bytes ...-heavy-metal-bitwise-16+pixel+width640.png | Bin 313 -> 311 bytes ...-tilemap-heavy-metal-48+pixel+width160.png | Bin 0 -> 554 bytes 11 files changed, 1098 insertions(+), 51 deletions(-) create mode 100644 static/js/components/literate-programming.js create mode 100644 static/js/components/literate-programming/worker.js create mode 100644 static/js/vendor/codejar.js delete mode 100644 static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+height40.png create mode 100644 static/pic/01HPQ97GRP075P4WYZPQKMNRH1-tilemap-heavy-metal-48+pixel+width160.png diff --git a/content/programming/blog/tairu.tree b/content/programming/blog/tairu.tree index d26e484..01f01cb 100644 --- a/content/programming/blog/tairu.tree +++ b/content/programming/blog/tairu.tree @@ -1,9 +1,12 @@ %% title = "tairu - an interactive exploration of 2D autotiling techniques" scripts = [ + "components/literate-programming.js", "tairu/cardinal-directions.js", "tairu/framework.js", "tairu/tairu.js", "tairu/tilemap-registry.js", + "tairu/tilemap.js", + "vendor/codejar.js", ] styles = ["tairu.css"] @@ -17,11 +20,11 @@ styles = ["tairu.css"] - TODO: short videos demoing this here % id = "01HPD4XQPWJBTJ4DWAQE3J87C9" -- once upon a time I heard of a technique called...\ +- once upon a time I stumbled upon a technique called...\ **bitwise autotiling** % id = "01HPD4XQPW6VK3FDW5QRCE6HSS" - + I learned about it back when I was building 2D Minecraft clones using [Construct 2](https://www.construct.net/en/construct-2/manuals/construct-2), and I wanted my terrain to look nice as it does in Terraria + + I learned about it way back when I was just a kid building 2D Minecraft clones using [Construct 2](https://www.construct.net/en/construct-2/manuals/construct-2), and I wanted my terrain to look nice as it does in Terraria % id = "01HPD4XQPWJ1CE9ZVRW98X7HE6" - Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile: @@ -112,6 +115,10 @@ styles = ["tairu.css"] ![horizontal tile strip of 16 8x8 pixel metal tiles][pic:01HPMMR6DGKYTPZ9CK0WQWKNX5] + % id = "01HPQCCV4RB65D5Q4RANJKGC0D" + - **hint:** you can actually just use the original image, but use a lookup table from these indices to (x, y) coordinates. + this makes creating the assets a lot easier! (at the expense of some CPU time, though it is totally possible to offload tilemap rendering to the GPU - in that case it barely even matters.) + % id = "01HPMVT9BMMEM4HT4ANZ40992P" - in JavaScript, drawing on a `` using bitwise autotiling would look like this: ```javascript @@ -193,21 +200,169 @@ styles = ["tairu.css"] - (I'm totally not trying to say this implementation is an L so far) % id = "01HPMVT9BMWG6QHQ125Z884W8Z" - + i'll cut right to the chase here and say it outright - the issue is that we simply don't have enough tiles to represent corner cases like this! + + i'll cut right to the chase here and say it outright - the issue is that we simply don't have enough tiles to represent *corner* cases like this! % id = "01HPMVT9BMQK8N1H68YV3J4CFQ" - see what I did there? - % id = "01HPMVT9BMJTG3KD3K5EJ3BC93" - - the solution here is to introduce more tiles to handle these edge cases. + % id = "01HPMVT9BMJTG3KD3K5EJ3BC93" + - the solution to that is to introduce more tiles to handle these edge cases. - TODO Explain + % classes.branch = "tileset-four-to-eight-demo" + id = "01HPQCCV4R5N97FJ1GS36HZJZ7" + - to represent the corners, we'll turn our four cardinal directions... + + + + into eight *ordinal* directions: + + + + % id = "01HPQCCV4R3GNEWZQFWGWH4Z6R" + - you might think that at this point we'll need 8 bits to represent our tiles, and that would make... + + ***256 tiles!?*** + + nobody in their right mind would actually draw 256 separate tiles, right? ***RIGHT???*** + + % template = true + id = "01HPQCCV4RX13VR4DJAP2F9PFA" + - ...right! if you experiment with the bit combinations, you'll quickly find out that there is no difference if, relative to a single center tile, we have tiles on the corners: + + + Your browser does not support <canvas>. + + + + these should all render the same way, despite technically having some [new neighbors](https://en.wikipedia.org/wiki/Moore_neighborhood). + + % classes.branch = "tileset-four-to-eight-demo" + id = "01HPQCCV4RHZ8A7VMT2KM7T27P" + - what we can do about this is to ignore corners whenever zero or one of the tiles at their cardinal directions is connected - + for example, in the case of `E | SE | S`: + + + + we can completely ignore what happens in the northeast, northwest, and southwest, because the tile's cardinal directions do not fully contain any of these direction pairs. + + % 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. + + + 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; + ``` + + as I've already said, we represent each direction using a single bit. + + - 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. + + - 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) { + return (integer & bit) == bit; + } + ``` + + - 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) { + if (isSet(t, SE) && (!isSet(t, S) || !isSet(t, E))) { + t &= ~SE; + } + if (isSet(t, SW) && (!isSet(t, S) || !isSet(t, W))) { + t &= ~SW; + } + if (isSet(t, NW) && (!isSet(t, N) || !isSet(t, W))) { + t &= ~NW; + } + if (isSet(t, NE) && (!isSet(t, N) || !isSet(t, E))) { + t &= ~NE; + } + return t; + } + ``` + + - with that, we can find a set of all unique non-redundant combinations: + + ```javascript ordinal-directions + function ordinalDirections() { + let unique = new Set(); + for (let i = 0; i <= ALL; ++i) { + unique.add(removeRedundancies(i)); + } + return Array.from(unique).sort((a, b) => a - b); + } + ``` + + - 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 + + - and now it's time to _Let It Cook™_: + + ```javascript ordinal-directions + let dirs = ordinalDirections(); + console.log(dirs.length); + ``` + + ```output ordinal-directions + 47 + ``` + + - forty seven! that's how many unique tiles we actually need. + + - 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. + + - phew... the nesting's getting quite unwieldy, let's wrap up this tangent and return back to doing some bitwise autotiling! + + - 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! + + - 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. % id = "01HPD4XQPWT9N8X9BD9GKWD78F" -- bitwise autotiling is a really cool technique that I've used in plenty of games in the past +- bitwise autotiling is a really cool technique that I've used in plenty of games in the past. % id = "01HPD4XQPW5FQY8M04S6JEBDHQ" - - as I mentioned before, [I've known it since my Construct 2 days][branch:01HPD4XQPW6VK3FDW5QRCE6HSS], but when it comes to my released games [Planet Overgamma] would probably be the first to utilize it properly + - as I mentioned before, [I've known it since my Construct 2 days][branch:01HPD4XQPW6VK3FDW5QRCE6HSS], but when it comes to any released games [Planet Overgamma] would probably be the first to utilize it properly. TODO video of some Planet Overgamma gameplay showing the autotiling in action @@ -215,7 +370,7 @@ styles = ["tairu.css"] % id = "01HPJ8GHDEN4XRPT1AJ1BTNTFJ" - this accursed game has been haunting me for years since; there have been many iterations. - he autotiling source code of the one in the video can be found [here][autotiling source code]. + the autotiling source code of the one in the video can be found [here][autotiling source code]. [autotiling source code]: https://github.com/liquidev/planet-overgamma/blob/classic/jam/map.lua#L209 @@ -223,19 +378,20 @@ styles = ["tairu.css"] + but one day I found a really cool project called [Tilekit](https://rxi.itch.io/tilekit) % id = "01HPD4XQPW11EQTBDQSGXW3S52" - + (of course it's really cool, after all rxi made it) + + (of course it's really cool, after all [rxi](https://github.com/rxi) made it) % id = "01HPD4XQPWYHS327BV586SB085" - - for context rxi is the genius behind the Lua-powered, simple, and modular text editor `lite` that I was using for quite a while + - for context rxi is the genius behind the Lua-powered, simple, and modular text editor [lite](https://github.com/rxi/lite) that I was using for quite a while % id = "01HPD4XQPWJ9QAQ5MF2J5JBB8M" - - after a while I switched to a fork - Lite XL, which had better font rendering and more features + - after a while I switched to a fork - [Lite XL](https://github.com/lite-xl/lite-xl), which had better font rendering and more features % id = "01HPD4XQPWB11TZSX5VAAJ6TCD" - I stopped using it because VS Code was just more feature packed and usable; no need to reinvent the wheel, rust-analyzer *just works.* % id = "01HPD4XQPW3G7BXTBBTD05MB8V" - - the LSP plugin for Lite XL had some issues around autocompletions not filling in properly :pensive:\ + - the LSP plugin for Lite XL had some issues around autocompletions not filling in properly :pensive: + it's likely a lot better now, but back then I decided this is too much for my nerves. while tinkering with your editor is something really cool, in my experience it's only cool up to a point. @@ -267,27 +423,10 @@ styles = ["tairu.css"] % id = "01HPD4XQPWP847T0EAM0FJ88T4" - then vines + - well... it's even simpler than that in terms of graphical presentation, but we'll get to that. + % id = "01HPD4XQPWK58Z63X6962STADR" - I mean, after all - bitwise autotiling is basically a clever solution to an `if` complexity problem, so why not extend that with more logic and rules and stuff to let you build more complex maps? % id = "01HPJ8GHDFRA2SPNHKJYD0SYPP" - of course Tilekit's solution is a lot more simple, streamlined, and user-friendly, but you get the gist. - -% id = "01HPD4XQPW4Y075XWJCT6AATB2" -- ever since then I've been wanting to build something just like Tilekit, but in the form of an educational, interactive blog post to demonstrate the ideas in a fun way - - % id = "01HPD4XQPWR8J9WCNBNCTJERZS" - - and what you're reading is the result of that. - -% id = "01HPD4XQPW1EP8YHACRJVMA0GM" -- so let's get going! first, we'll build a basic tile editor using JavaScript. - - % id = "01HPD4XQPWPNRTVJFNFGNHJMG1" - + not my favorite language, but we're on the Web so it's not like we have much more of a choice. - - % id = "01HPD4XQPWGK7M4XJYC99XE4T6" - - I could use TypeScript, but this page follows a philosophy of not introducing complexity where I can deal without it. - TypeScript is totally cool, but not necessary. - - % id = "01HPD4XQPWAE0ZH46WME6WJSVP" - - I'll be using Web Components (in particular, custom elements) combined with canvas to add stuff onto the page. diff --git a/crates/treehouse/src/html/markdown.rs b/crates/treehouse/src/html/markdown.rs index 679e40d..499be9d 100644 --- a/crates/treehouse/src/html/markdown.rs +++ b/crates/treehouse/src/html/markdown.rs @@ -227,16 +227,29 @@ where self.write_newline()?; } match info { - CodeBlockKind::Fenced(info) => { - let lang = info.split(' ').next().unwrap(); - if lang.is_empty() { - self.write("
")
-                        } else {
+                    CodeBlockKind::Fenced(language) => match CodeBlockMode::parse(&language) {
+                        CodeBlockMode::PlainText => self.write("
"),
+                        CodeBlockMode::SyntaxHighlightOnly { language } => {
                             self.write("
")
                         }
-                    }
+                        CodeBlockMode::LiterateProgram {
+                            language,
+                            kind,
+                            program_name,
+                        } => {
+                            self.write(match kind {
+                                LiterateCodeKind::Input => " "")
+                        }
+                    },
                     CodeBlockKind::Indented => self.write("
"),
                 }
             }
@@ -352,8 +365,21 @@ where
             Tag::BlockQuote => {
                 self.write("\n")?;
             }
-            Tag::CodeBlock(_) => {
-                self.write("
\n")?; + Tag::CodeBlock(kind) => { + self.write(match kind { + CodeBlockKind::Fenced(language) => match CodeBlockMode::parse(&language) { + CodeBlockMode::LiterateProgram { + kind: LiterateCodeKind::Input, + .. + } => "
", + CodeBlockMode::LiterateProgram { + kind: LiterateCodeKind::Output, + .. + } => "", + _ => "
", + }, + _ => "
\n", + })?; self.in_code_block = false; } Tag::List(Some(_)) => { @@ -518,6 +544,44 @@ where } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LiterateCodeKind { + Input, + Output, +} + +enum CodeBlockMode<'a> { + PlainText, + SyntaxHighlightOnly { + language: &'a str, + }, + LiterateProgram { + language: &'a str, + kind: LiterateCodeKind, + program_name: &'a str, + }, +} + +impl<'a> CodeBlockMode<'a> { + fn parse(language: &'a str) -> CodeBlockMode<'a> { + if language.is_empty() { + CodeBlockMode::PlainText + } else if let Some((language, program_name)) = language.split_once(' ') { + CodeBlockMode::LiterateProgram { + language, + kind: if language == "output" { + LiterateCodeKind::Output + } else { + LiterateCodeKind::Input + }, + program_name, + } + } else { + CodeBlockMode::SyntaxHighlightOnly { language } + } + } +} + /// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and /// push it to a `String`. /// diff --git a/static/css/main.css b/static/css/main.css index 8b73c17..568bc00 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -170,7 +170,9 @@ h4 { pre, code, -kbd { +kbd, +th-literate-editor, +th-literate-output { --recursive-mono: 1.0; --recursive-casl: 0.0; --recursive-slnt: 0.0; @@ -210,19 +212,27 @@ body { /* Make code examples a little prettier by giving them visual separation from the rest of the page */ -code { +code, +th-literate-editor { padding: 3px 4px; background-color: var(--shaded-background); border-radius: 4px; } +th-literate-editor, +th-literate-output { + display: block; +} + kbd { padding: 3px 6px; border: 1px solid var(--border-1); border-radius: 6px; } -pre { +pre, +th-literate-editor, +th-literate-output { padding: 8px 12px; margin: 12px 0; background-color: var(--shaded-against-background); @@ -231,11 +241,22 @@ pre { transition: background-color var(--transition-duration); } -.tree summary:hover pre { - background-color: var(--shaded-against-background-twice); +th-literate-editor, +th-literate-output { + white-space: pre; } -pre>code { +.tree summary:hover { + + & pre, + & th-literate-editor, + & th-literate-output { + background-color: var(--shaded-against-background-twice); + } +} + +pre>code, +th-literate-output>code { padding: 0; background: none; border-radius: 0px; @@ -493,3 +514,40 @@ img[is="th-emoji"] { display: inline; animation: 4s hello-there forwards; } + +/* Literate programming support */ + +th-literate-editor { + /* 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 { + position: relative; + + & code { + display: block; + } + + & code.error { + color: #e39393; + } + + & code .return-value { + content: 'Return value: '; + opacity: 50%; + } + + &::after { + content: 'Output'; + + padding: 8px; + + position: absolute; + right: 0; + top: 0; + + opacity: 50%; + } +} diff --git a/static/css/tairu.css b/static/css/tairu.css index 9106172..533e2dd 100644 --- a/static/css/tairu.css +++ b/static/css/tairu.css @@ -63,6 +63,11 @@ font-size: 14px; position: absolute; color: #d3dce9; + text-shadow: + 1px 0 0 #1a2039, + -1px 0 0 #1a2039, + 0 1px 0 #1a2039, + 0 -1px 0 #1a2039; padding: 2px 4px; } @@ -99,11 +104,11 @@ } & .x-1 { - background-position-x: 33.3333%; + background-position-x: calc(100% / 3); } & .x-2 { - background-position-x: 66.6666%; + background-position-x: calc(200% / 3); } & .x-3 { @@ -115,14 +120,102 @@ } & .y-1 { - background-position-y: 33.3333%; + background-position-y: calc(100% / 3); } & .y-2 { - background-position-y: 66.6666%; + background-position-y: calc(200% / 3); } & .y-3 { background-position-y: 100%; } } + +.tileset-four-to-eight-demo th-bc { + & .directions-square { + --recursive-wght: 900; + --recursive-casl: 0.0; + --recursive-slnt: 0.0; + --recursive-mono: 1.0; + color: #d3dce9; + text-shadow: + 1px 0 0 #1a2039, + -1px 0 0 #1a2039, + 0 1px 0 #1a2039, + 0 -1px 0 #1a2039; + + margin-block: 8px; + margin-left: 16px; + padding-left: 0; + + width: 72px; + height: 72px; + + background-image: url('../pic/01HPHVDRV0F0251MD0A2EG66C4-tilemap-heavy-metal-16+pixel+width160.png'); + background-size: 400%; + background-position: 100% 100%; + image-rendering: crisp-edges; + + position: relative; + + li { + padding: 2px 4px; + position: absolute; + } + + a { + text-decoration: none; + color: #d3dce9; + cursor: text; + } + + & .east { + right: 0; + top: 50%; + transform: translateY(-50%); + } + + & .south-east { + right: 0; + bottom: 0; + } + + & .west { + left: 0; + top: 50%; + transform: translateY(-50%); + } + + & .south-west { + left: 0; + bottom: 0; + } + + & .north { + left: 50%; + top: 0; + transform: translateX(-50%); + } + + & .north-west { + left: 0; + top: 0; + } + + & .south { + left: 50%; + bottom: 0; + transform: translateX(-50%); + } + + & .north-east { + right: 0; + top: 0; + } + + &.e-s { + background-position: 0% 0%; + } + } +} diff --git a/static/js/components/literate-programming.js b/static/js/components/literate-programming.js new file mode 100644 index 0000000..130b56e --- /dev/null +++ b/static/js/components/literate-programming.js @@ -0,0 +1,135 @@ +import { CodeJar } from "../vendor/codejar.js"; + +let literatePrograms = new Map(); + +function getLiterateProgram(name) { + if (literatePrograms.get(name) == null) { + literatePrograms.set(name, { + editors: [], + onChanged: [], + }); + } + return literatePrograms.get(name); +} + +function getLiterateProgramSourceCode(name) { + let sources = []; + let literateProgram = getLiterateProgram(name); + for (let editor of literateProgram.editors) { + sources.push(editor.textContent); + } + return sources.join("\n"); +} + +class LiterateEditor extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.literateProgramName = this.getAttribute("data-program"); + getLiterateProgram(this.literateProgramName).editors.push(this); + + this.codeJar = CodeJar(this, LiterateEditor.highlight); + this.codeJar.onUpdate(() => { + let literateProgram = getLiterateProgram(this.literateProgramName); + for (let handler of literateProgram.onChanged) { + handler(this.literateProgramName); + } + }) + + this.addEventListener("click", event => event.preventDefault()); + } + + static highlight(editor) { + // 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(); + + this.clearResultsOnNextOutput = false; + } + + connectedCallback() { + this.literateProgramName = this.getAttribute("data-program"); + this.evaluate(); + + getLiterateProgram(this.literateProgramName).onChanged.push(_ => this.evaluate()); + } + + evaluate = () => { + // This is a small bit of debouncing. If we cleared the output right away, the page would + // jitter around irritatingly + this.clearResultsOnNextOutput = true; + + if (this.worker != null) { + this.worker.terminate(); + } + this.worker = new Worker(`${TREEHOUSE_SITE}/static/js/components/literate-programming/worker.js`, { + type: "module", + name: `evaluate LiterateOutput ${this.literateProgramName}` + }); + + this.worker.addEventListener("message", event => { + let message = event.data; + if (message.kind == "evalComplete") { + this.worker.terminate(); + } else if (message.kind == "output") { + this.addOutput(message.output); + } + }); + + this.worker.postMessage({ + action: "eval", + input: getLiterateProgramSourceCode(this.literateProgramName), + }); + }; + + addOutput(output) { + if (this.clearResultsOnNextOutput) { + this.clearResultsOnNextOutput = false; + this.clearResults(); + } + + // Don't show anything if the function didn't return a value. + if (output.kind == "result" && output.message[0] === undefined) return; + + let line = document.createElement("code"); + + 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(" "); + + if (output.kind == "result") { + let returnValueText = document.createElement("span"); + returnValueText.classList.add("return-value"); + returnValueText.textContent = "Return value: "; + line.insertBefore(returnValueText, line.firstChild); + } + + this.appendChild(line); + } + + clearResults() { + this.replaceChildren(); + } +} + +customElements.define("th-literate-output", LiterateOutput); diff --git a/static/js/components/literate-programming/worker.js b/static/js/components/literate-programming/worker.js new file mode 100644 index 0000000..aedf20f --- /dev/null +++ b/static/js/components/literate-programming/worker.js @@ -0,0 +1,40 @@ +console = { + log(...message) { + postMessage({ + kind: "output", + output: { + kind: "log", + message: [...message], + } + }); + } +}; + +addEventListener("message", event => { + let message = event.data; + if (message.action == "eval") { + try { + let func = new Function(message.input); + let result = func.apply({}); + postMessage({ + kind: "output", + output: { + kind: "result", + message: [result], + } + }); + } catch (error) { + postMessage({ + kind: "output", + output: { + kind: "error", + message: [error.toString()], + } + }); + } + + postMessage({ + kind: "evalComplete", + }); + } +}); diff --git a/static/js/tairu/tilemap-registry.js b/static/js/tairu/tilemap-registry.js index 1f6a09a..f66bcbc 100644 --- a/static/js/tairu/tilemap-registry.js +++ b/static/js/tairu/tilemap-registry.js @@ -29,5 +29,12 @@ export default { " xxx ", " ", ]), + bitwiseAutotilingCorners: parseTilemap([ + " ", + " x x ", + " x ", + " x x ", + " ", + ]), }; diff --git a/static/js/vendor/codejar.js b/static/js/vendor/codejar.js new file mode 100644 index 0000000..1333763 --- /dev/null +++ b/static/js/vendor/codejar.js @@ -0,0 +1,511 @@ +const globalWindow = window; +export function CodeJar(editor, highlight, opt = {}) { + const options = { + tab: '\t', + indentOn: /[({\[]$/, + moveToNewLine: /^[)}\]]/, + spellcheck: false, + catchTab: true, + preserveIdent: true, + addClosing: true, + history: true, + window: globalWindow, + ...opt, + }; + const window = options.window; + const document = window.document; + const listeners = []; + const history = []; + let at = -1; + let focus = false; + let onUpdate = () => void 0; + let prev; // code content prior keydown event + editor.setAttribute('contenteditable', 'plaintext-only'); + editor.setAttribute('spellcheck', options.spellcheck ? 'true' : 'false'); + editor.style.outline = 'none'; + editor.style.overflowWrap = 'break-word'; + editor.style.overflowY = 'auto'; + editor.style.whiteSpace = 'pre-wrap'; + const doHighlight = (editor, pos) => { + highlight(editor, pos); + }; + let isLegacy = false; // true if plaintext-only is not supported + if (editor.contentEditable !== 'plaintext-only') + isLegacy = true; + if (isLegacy) + editor.setAttribute('contenteditable', 'true'); + const debounceHighlight = debounce(() => { + const pos = save(); + doHighlight(editor, pos); + restore(pos); + }, 30); + let recording = false; + const shouldRecord = (event) => { + return !isUndo(event) && !isRedo(event) + && event.key !== 'Meta' + && event.key !== 'Control' + && event.key !== 'Alt' + && !event.key.startsWith('Arrow'); + }; + const debounceRecordHistory = debounce((event) => { + if (shouldRecord(event)) { + recordHistory(); + recording = false; + } + }, 300); + const on = (type, fn) => { + listeners.push([type, fn]); + editor.addEventListener(type, fn); + }; + on('keydown', event => { + if (event.defaultPrevented) + return; + prev = toString(); + if (options.preserveIdent) + handleNewLine(event); + else + legacyNewLineFix(event); + if (options.catchTab) + handleTabCharacters(event); + if (options.addClosing) + handleSelfClosingCharacters(event); + if (options.history) { + handleUndoRedo(event); + if (shouldRecord(event) && !recording) { + recordHistory(); + recording = true; + } + } + if (isLegacy && !isCopy(event)) + restore(save()); + }); + on('keyup', event => { + if (event.defaultPrevented) + return; + if (event.isComposing) + return; + if (prev !== toString()) + debounceHighlight(); + debounceRecordHistory(event); + onUpdate(toString()); + }); + on('focus', _event => { + focus = true; + }); + on('blur', _event => { + focus = false; + }); + on('paste', event => { + recordHistory(); + handlePaste(event); + recordHistory(); + onUpdate(toString()); + }); + on('cut', event => { + recordHistory(); + handleCut(event); + recordHistory(); + onUpdate(toString()); + }); + function save() { + const s = getSelection(); + const pos = { start: 0, end: 0, dir: undefined }; + let { anchorNode, anchorOffset, focusNode, focusOffset } = s; + if (!anchorNode || !focusNode) + throw 'error1'; + // If the anchor and focus are the editor element, return either a full + // highlight or a start/end cursor position depending on the selection + if (anchorNode === editor && focusNode === editor) { + pos.start = (anchorOffset > 0 && editor.textContent) ? editor.textContent.length : 0; + pos.end = (focusOffset > 0 && editor.textContent) ? editor.textContent.length : 0; + pos.dir = (focusOffset >= anchorOffset) ? '->' : '<-'; + return pos; + } + // Selection anchor and focus are expected to be text nodes, + // so normalize them. + if (anchorNode.nodeType === Node.ELEMENT_NODE) { + const node = document.createTextNode(''); + anchorNode.insertBefore(node, anchorNode.childNodes[anchorOffset]); + anchorNode = node; + anchorOffset = 0; + } + if (focusNode.nodeType === Node.ELEMENT_NODE) { + const node = document.createTextNode(''); + focusNode.insertBefore(node, focusNode.childNodes[focusOffset]); + focusNode = node; + focusOffset = 0; + } + visit(editor, el => { + if (el === anchorNode && el === focusNode) { + pos.start += anchorOffset; + pos.end += focusOffset; + pos.dir = anchorOffset <= focusOffset ? '->' : '<-'; + return 'stop'; + } + if (el === anchorNode) { + pos.start += anchorOffset; + if (!pos.dir) { + pos.dir = '->'; + } + else { + return 'stop'; + } + } + else if (el === focusNode) { + pos.end += focusOffset; + if (!pos.dir) { + pos.dir = '<-'; + } + else { + return 'stop'; + } + } + if (el.nodeType === Node.TEXT_NODE) { + if (pos.dir != '->') + pos.start += el.nodeValue.length; + if (pos.dir != '<-') + pos.end += el.nodeValue.length; + } + }); + editor.normalize(); // collapse empty text nodes + return pos; + } + function restore(pos) { + const s = getSelection(); + let startNode, startOffset = 0; + let endNode, endOffset = 0; + if (!pos.dir) + pos.dir = '->'; + if (pos.start < 0) + pos.start = 0; + if (pos.end < 0) + pos.end = 0; + // Flip start and end if the direction reversed + if (pos.dir == '<-') { + const { start, end } = pos; + pos.start = end; + pos.end = start; + } + let current = 0; + visit(editor, el => { + if (el.nodeType !== Node.TEXT_NODE) + return; + const len = (el.nodeValue || '').length; + if (current + len > pos.start) { + if (!startNode) { + startNode = el; + startOffset = pos.start - current; + } + if (current + len > pos.end) { + endNode = el; + endOffset = pos.end - current; + return 'stop'; + } + } + current += len; + }); + if (!startNode) + startNode = editor, startOffset = editor.childNodes.length; + if (!endNode) + endNode = editor, endOffset = editor.childNodes.length; + // Flip back the selection + if (pos.dir == '<-') { + [startNode, startOffset, endNode, endOffset] = [endNode, endOffset, startNode, startOffset]; + } + { + // If nodes not editable, create a text node. + const startEl = uneditable(startNode); + if (startEl) { + const node = document.createTextNode(''); + startEl.parentNode?.insertBefore(node, startEl); + startNode = node; + startOffset = 0; + } + const endEl = uneditable(endNode); + if (endEl) { + const node = document.createTextNode(''); + endEl.parentNode?.insertBefore(node, endEl); + endNode = node; + endOffset = 0; + } + } + s.setBaseAndExtent(startNode, startOffset, endNode, endOffset); + editor.normalize(); // collapse empty text nodes + } + function uneditable(node) { + while (node && node !== editor) { + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node; + if (el.getAttribute('contenteditable') == 'false') { + return el; + } + } + node = node.parentNode; + } + } + function beforeCursor() { + const s = getSelection(); + const r0 = s.getRangeAt(0); + const r = document.createRange(); + r.selectNodeContents(editor); + r.setEnd(r0.startContainer, r0.startOffset); + return r.toString(); + } + function afterCursor() { + const s = getSelection(); + const r0 = s.getRangeAt(0); + const r = document.createRange(); + r.selectNodeContents(editor); + r.setStart(r0.endContainer, r0.endOffset); + return r.toString(); + } + function handleNewLine(event) { + if (event.key === 'Enter') { + const before = beforeCursor(); + const after = afterCursor(); + let [padding] = findPadding(before); + let newLinePadding = padding; + // If last symbol is "{" ident new line + if (options.indentOn.test(before)) { + newLinePadding += options.tab; + } + // Preserve padding + if (newLinePadding.length > 0) { + preventDefault(event); + event.stopPropagation(); + insert('\n' + newLinePadding); + } + else { + legacyNewLineFix(event); + } + // Place adjacent "}" on next line + if (newLinePadding !== padding && options.moveToNewLine.test(after)) { + const pos = save(); + insert('\n' + padding); + restore(pos); + } + } + } + function legacyNewLineFix(event) { + // Firefox does not support plaintext-only mode + // and puts

on Enter. Let's help. + if (isLegacy && event.key === 'Enter') { + preventDefault(event); + event.stopPropagation(); + if (afterCursor() == '') { + insert('\n '); + const pos = save(); + pos.start = --pos.end; + restore(pos); + } + else { + insert('\n'); + } + } + } + function handleSelfClosingCharacters(event) { + const open = `([{'"`; + const close = `)]}'"`; + if (open.includes(event.key)) { + preventDefault(event); + const pos = save(); + const wrapText = pos.start == pos.end ? '' : getSelection().toString(); + const text = event.key + wrapText + close[open.indexOf(event.key)]; + insert(text); + pos.start++; + pos.end++; + restore(pos); + } + } + function handleTabCharacters(event) { + if (event.key === 'Tab') { + preventDefault(event); + if (event.shiftKey) { + const before = beforeCursor(); + let [padding, start] = findPadding(before); + if (padding.length > 0) { + const pos = save(); + // Remove full length tab or just remaining padding + const len = Math.min(options.tab.length, padding.length); + restore({ start, end: start + len }); + document.execCommand('delete'); + pos.start -= len; + pos.end -= len; + restore(pos); + } + } + else { + insert(options.tab); + } + } + } + function handleUndoRedo(event) { + if (isUndo(event)) { + preventDefault(event); + at--; + const record = history[at]; + if (record) { + editor.innerHTML = record.html; + restore(record.pos); + } + if (at < 0) + at = 0; + } + if (isRedo(event)) { + preventDefault(event); + at++; + const record = history[at]; + if (record) { + editor.innerHTML = record.html; + restore(record.pos); + } + if (at >= history.length) + at--; + } + } + function recordHistory() { + if (!focus) + return; + const html = editor.innerHTML; + const pos = save(); + const lastRecord = history[at]; + if (lastRecord) { + if (lastRecord.html === html + && lastRecord.pos.start === pos.start + && lastRecord.pos.end === pos.end) + return; + } + at++; + history[at] = { html, pos }; + history.splice(at + 1); + const maxHistory = 300; + if (at > maxHistory) { + at = maxHistory; + history.splice(0, 1); + } + } + function handlePaste(event) { + if (event.defaultPrevented) + return; + preventDefault(event); + const originalEvent = event.originalEvent ?? event; + const text = originalEvent.clipboardData.getData('text/plain').replace(/\r\n?/g, '\n'); + const pos = save(); + insert(text); + doHighlight(editor); + restore({ + start: Math.min(pos.start, pos.end) + text.length, + end: Math.min(pos.start, pos.end) + text.length, + dir: '<-', + }); + } + function handleCut(event) { + const pos = save(); + const selection = getSelection(); + const originalEvent = event.originalEvent ?? event; + originalEvent.clipboardData.setData('text/plain', selection.toString()); + document.execCommand('delete'); + doHighlight(editor); + restore({ + start: Math.min(pos.start, pos.end), + end: Math.min(pos.start, pos.end), + dir: '<-', + }); + preventDefault(event); + } + function visit(editor, visitor) { + const queue = []; + if (editor.firstChild) + queue.push(editor.firstChild); + let el = queue.pop(); + while (el) { + if (visitor(el) === 'stop') + break; + if (el.nextSibling) + queue.push(el.nextSibling); + if (el.firstChild) + queue.push(el.firstChild); + el = queue.pop(); + } + } + function isCtrl(event) { + return event.metaKey || event.ctrlKey; + } + function isUndo(event) { + return isCtrl(event) && !event.shiftKey && getKeyCode(event) === 'Z'; + } + function isRedo(event) { + return isCtrl(event) && event.shiftKey && getKeyCode(event) === 'Z'; + } + function isCopy(event) { + return isCtrl(event) && getKeyCode(event) === 'C'; + } + function getKeyCode(event) { + let key = event.key || event.keyCode || event.which; + if (!key) + return undefined; + return (typeof key === 'string' ? key : String.fromCharCode(key)).toUpperCase(); + } + function insert(text) { + text = text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + document.execCommand('insertHTML', false, text); + } + function debounce(cb, wait) { + let timeout = 0; + return (...args) => { + clearTimeout(timeout); + timeout = window.setTimeout(() => cb(...args), wait); + }; + } + function findPadding(text) { + // Find beginning of previous line. + let i = text.length - 1; + while (i >= 0 && text[i] !== '\n') + i--; + i++; + // Find padding of the line. + let j = i; + while (j < text.length && /[ \t]/.test(text[j])) + j++; + return [text.substring(i, j) || '', i, j]; + } + function toString() { + return editor.textContent || ''; + } + function preventDefault(event) { + event.preventDefault(); + } + function getSelection() { + if (editor.parentNode?.nodeType == Node.DOCUMENT_FRAGMENT_NODE) { + return editor.parentNode.getSelection(); + } + return window.getSelection(); + } + return { + updateOptions(newOptions) { + Object.assign(options, newOptions); + }, + updateCode(code) { + editor.textContent = code; + doHighlight(editor); + onUpdate(code); + }, + onUpdate(callback) { + onUpdate = callback; + }, + toString, + save, + restore, + recordHistory, + destroy() { + for (let [type, fn] of listeners) { + editor.removeEventListener(type, fn); + } + }, + }; +} diff --git a/static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+height40.png b/static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+height40.png deleted file mode 100644 index 69c12bbdaf98ae2b6dd763811a39eb76a47641e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^4M5Dn!3-qXnAv@R6k~CayA#8@b22Z1oTvbw5LY1m z|Nnn3b`=H{Sq2+ThEOwxZb!CLUiEK+4*l+9c=eyP?*oIy!VHGoRSdbWg}r|J`Q0`! zn8UzOARutshv9z%*Z;%v|39lpDOf6Lx)|BSdW4itTeko5otI}`DMtWJmn#YK3kKPN z8+@^lG6$+m_H=O!vFN?qcaZmh0SB|>VWYeC=gdSknp*r{tgbsMwl8AcGLKcCrt+oQ zJKwEZSS&N6rYHQqyu;y^`OEdc)mR>w~BOF<{jF?_u^~o%OrjW+XXkp gRYVy=8uI?L1u0}sy_fv-1<*PMPgg&ebxsLQ0G_LaJpcdz diff --git a/static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+width640.png b/static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+width640.png index 3a12f659541d1e73c7d7b4887cc1efc7442edbc5..98ad4f61376c8f38ad9e5430c2ea22fa635ffe1c 100644 GIT binary patch delta 125 zcmV-@0D}Ly0=EK?!FFLuL_t(IjopsH4ZtuA0zC%8-G9zDm8~dF{GzY&3dmfrOB6|W zzVe~!XXr<7OilueU*p~D0~na)=joe0Uk=a!e;9yA;~t3D0>~7heSp?CmWwk0{Q$9V fC~5%c0BZjRNg!#FT{62200000NkvXXu0mjfC(<@f delta 127 zcmV-_0D%9u0=WW^!FXdyL_t(IjopsH4Ztu60~y0|y8oO_L{Li-Uhb=YVG9o1C5mia z9BMvgKf{;F-WZ$!E0jLXz h_l89U02M&JzX6*7X}Q`O&Yb`N002ovPDHLkV1lijHDLe% diff --git a/static/pic/01HPQ97GRP075P4WYZPQKMNRH1-tilemap-heavy-metal-48+pixel+width160.png b/static/pic/01HPQ97GRP075P4WYZPQKMNRH1-tilemap-heavy-metal-48+pixel+width160.png new file mode 100644 index 0000000000000000000000000000000000000000..76d53ae09b75a4e59c129459e400064654609e83 GIT binary patch literal 554 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`LuitETkNxJa z^Iqze|Er6C)=pT@U;Xpey{9i^Js5vk&*JC1*J@Yzx}M?7F1dZ&(hI)ZGp)LFik;!c zp9is)&zV_D;w2JJv7fxiQ?TUsyv8kQ@*ch1i~pVc?O^xfZyv)X`$~DP{LK8-I`Stf zSQ&oB7Jp&C%RGVm`|h+)?t9o3v=b)u{uStByUDPU|I3e?PP}iknP%J*%|5}mVwE{l zOul^oO$MFQ;zt+NOlNEgTC{zGw)mI$!(ZeXVy{2ozCZ2iGJEEroeyGkvMV2Ts?S?c a!Tck}Jb?f7lbgVpVDNPHb6Mw<&;$UAzWWaV literal 0 HcmV?d00001