diff --git a/content/programming/blog/tairu.tree b/content/programming/blog/tairu.tree index 54050c7..d26e484 100644 --- a/content/programming/blog/tairu.tree +++ b/content/programming/blog/tairu.tree @@ -1,5 +1,11 @@ %% title = "tairu - an interactive exploration of 2D autotiling techniques" - scripts = ["tairu/tiling-demo.js", "tairu/tairu.js"] +scripts = [ + "tairu/cardinal-directions.js", + "tairu/framework.js", + "tairu/tairu.js", + "tairu/tilemap-registry.js", +] +styles = ["tairu.css"] % id = "01HPD4XQPWM8ECT2QM6AT9YRWB" - I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain. @@ -11,7 +17,8 @@ - TODO: short videos demoing this here % id = "01HPD4XQPWJBTJ4DWAQE3J87C9" -- once upon a time I heard of a technique called *bitwise autotiling* +- once upon a time I heard of 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 @@ -19,32 +26,183 @@ % id = "01HPD4XQPWJ1CE9ZVRW98X7HE6" - Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile: - % id = "01HPD4XQPWHNFQPRHX13MYW8GT" - - this technique involves assigning the cardinal directions (north, south, east, west) to a bitset. - then for each tile you look at which adjacent tiles should be connected to + % id = "01HPJ8GHDET8ZGNN0AH3FWA8HX" + - let's begin with a tilemap. say we have the following grid of tiles: (the examples are interactive, try editing it!) - % id = "01HPD4XQPWS2JS8RJH2P5TKPAB" - - this connection condition can be whatever you want - in most cases it's just "is the adjacent tile of the same type as the current tile?" + + Your browser does not support <canvas>. + - % id = "01HPD4XQPWAANYFBYX681787D1" - - for example, "is the tile to the left a dirt tile?" + % id = "01HPJ8GHDEC0Z334M04MTNADV9" + - for each tile, we can assign a bitset of cardinal directions like so: - % id = "01HPD4XQPWES5K2V2AKB7H0EHK" - - and then you use this bitset to index into a lookup table of tiles + + Your browser does not support <canvas>. + - % id = "01HPD4XQPWD00GDZ0N5H1DRH2P" - - for example, say we have the following grid of tiles:\ - TODO editable grid on javascript + % template = true + id = "01HPJ8GHDE9QKQ4QFZK1Z1KQD4" + classes.branch = "tileset-cardinal-directions-demo" + + now given a tileset, such as the one below that I drew a while ago, we can assign each tile to a set of cardinal directions. + I'll indicate where there's a connection between individual tiles with the letters **N**, **E**, **S**, **W**, standing for the cardinal directions **N**orth, **E**ast, **S**outh, and **W**est. - for each tile, we can assign a bitset of cardinal directions like so:\ - TODO grid linked with the other grid to show which adjacent tiles each tile connects to + - % id = "01HPD4XQPWM0AAE6F162EZTFQY" - - in JavaScript it would look something like this: + % id = "01HPMVT9BM65YD5AXWPT4Z67H5" + - (it's frustratingly hard to center individual letters like this in CSS. please forgive me for how crooked these are!) + + % id = "01HPMVT9BM5V4BP8K80X0C1HJZ" + - note that the state of connection for a given cardinal direction can be represented using two values: **connected**, and **not connected**. + two values make one bit, so we can pack these four connection states into four bits, and use that as an array index! + + % classes.branch = "tileset-cardinal-directions-demo" + id = "01HPMVT9BM4AXG2Z1D2QBH828G" + + for that to work though, we need to rearrange our tilemap somewhat such that we can index into it easily using our integer. + assuming we pack our bits as `NWSE` (bit 0 is east, each next bit we go clockwise), + therefore the final arrangement is this: + +
+ + E + S + ES + W + EW + SW + ESW + N + EN + SN + ESN + WN + EWN + SWN + ESWN +
+ + packing that into a single tilesheet, or rather tile *strip*, we get this image: + + ![horizontal tile strip of 16 8x8 pixel metal tiles][pic:01HPMMR6DGKYTPZ9CK0WQWKNX5] + + % id = "01HPMVT9BMMEM4HT4ANZ40992P" + - in JavaScript, drawing on a `` using bitwise autotiling would look like this: ```javascript - // TODO code example + for (let y = 0; y < tilemap.height; ++y) { + for (let x = 0; x < tilemap.width; ++x) { + // Assume `tilemap.at` is a function which returns the type of tile + // stored at coordinates (x, y). + let tile = tilemap.at(x, y); + + // We need to treat *some* tile as an empty (fully transparent) tile. + // In our case that'll be 0. + if (tile != 0) { + let tileset = tilesets[tile]; + + // Now it's time to represent the tile connections as bits. + // For each cardinal direction we produce a different bit value, or 0 if there is + // no connection: + let connectedWithEast = shouldConnect(tile, tilemap.at(x + 1, y)) ? 0b0001 : 0; + let connectedWithSouth = shouldConnect(tile, tilemap.at(x, y + 1)) ? 0b0010 : 0; + let connectedWithWest = shouldConnect(tile, tilemap.at(x - 1, y)) ? 0b0100 : 0; + let connectedWithNorth = shouldConnect(tile, tilemap.at(x, y - 1)) ? 0b1000 : 0; + // Then we OR them together into one integer. + let tileIndex = connectedWithNorth + | connectedWithWest + | connectedWithSouth + | connectedWithEast; + + // With that, we can draw the correct tile. + // Our strip is a single horizontal line, so we can assume + let tilesetTileSize = tileset.height; + let tilesetX = tileIndex * tilesetTileSize; + let tilesetY = 0; + ctx.drawImage( + tilesets[tile], + tilesetX, tilesetY, tilesetTileSize, tilesetTileSize, + x * tileSize, y * tileSize, tileSize, tileSize, + ); + } + } + } ``` + % template = true + id = "01HPMVT9BM9CS9375MX4H9WKW8" + - and that gives us this result: + + + Your browser does not support <canvas>. + + + + % id = "01HPMVT9BM3WR0BNZFHP2BPZ8A" + - but if you play around with it (or have *already* played around with it, and are therefore left with a non-default tilemap) + + ...something seems awful about it doesn't it? + + % template = true + id = "01HPMVT9BMPA89037VPWPPWX8V" + - something's off about the corners. let me give you a fresh example to illustrate what I mean: + + + Your browser does not support <canvas>. + + + + % id = "01HPMVT9BM16EF3TV5J1K19JAM" + + see that tile in the bottom left corner of the `L` shape? it's missing a corner. + the top-right corner, to be exact, which makes it visually disjoint from the tiles to the north and the east. + + % id = "01HPMVT9BM5VWJSMDNPK2SRNZV" + - (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! + + % id = "01HPMVT9BMQK8N1H68YV3J4CFQ" + - see what I did there? + + % id = "01HPMVT9BMJTG3KD3K5EJ3BC93" + - the solution here is to introduce more tiles to handle these edge cases. + + TODO Explain + % id = "01HPD4XQPWT9N8X9BD9GKWD78F" - bitwise autotiling is a really cool technique that I've used in plenty of games in the past @@ -55,6 +213,7 @@ [Planet Overgamma]: https://liquidev.itch.io/planet-overgamma-classic + % 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]. @@ -111,6 +270,9 @@ % 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 diff --git a/crates/treehouse/src/cli/generate.rs b/crates/treehouse/src/cli/generate.rs index 533580b..7af2fec 100644 --- a/crates/treehouse/src/cli/generate.rs +++ b/crates/treehouse/src/cli/generate.rs @@ -229,15 +229,7 @@ impl Generator { .thumbnail .as_ref() .map(|thumbnail| Thumbnail { - url: format!( - "{}/static/pic/{}", - config.site, - config - .pics - .get(&thumbnail.id) - .map(|x| &**x) - .unwrap_or("404.png") - ), + url: config.pic_url(&thumbnail.id), alt: thumbnail.alt.clone(), }), scripts: roots.attributes.scripts.clone(), diff --git a/crates/treehouse/src/config.rs b/crates/treehouse/src/config.rs index f37a52c..ff06789 100644 --- a/crates/treehouse/src/config.rs +++ b/crates/treehouse/src/config.rs @@ -99,4 +99,12 @@ impl Config { } Ok(()) } + + pub fn pic_url(&self, id: &str) -> String { + format!( + "{}/static/pic/{}", + self.site, + self.pics.get(id).map(|x| &**x).unwrap_or("404.png") + ) + } } diff --git a/crates/treehouse/src/html/tree.rs b/crates/treehouse/src/html/tree.rs index d6e7c52..1bb2423 100644 --- a/crates/treehouse/src/html/tree.rs +++ b/crates/treehouse/src/html/tree.rs @@ -7,7 +7,7 @@ use crate::{ config::Config, html::EscapeAttribute, state::{FileId, Treehouse}, - tree::{attributes::Content, SemaBranchId}, + tree::{attributes::Content, mini_template, SemaBranchId}, }; use super::{markdown, EscapeHtml}; @@ -26,6 +26,12 @@ pub fn branch_to_html( !branch.children.is_empty() || matches!(branch.attributes.content, Content::Link(_)); let class = if has_children { "branch" } else { "leaf" }; + let mut class = String::from(class); + if !branch.attributes.classes.branch.is_empty() { + class.push(' '); + class.push_str(&branch.attributes.classes.branch); + } + let component = if let Content::Link(_) = branch.attributes.content { "th-b-linked" } else { @@ -64,7 +70,7 @@ pub fn branch_to_html( s.push_str(""); let raw_block_content = &source.input()[branch.content.clone()]; - let mut unindented_block_content = String::with_capacity(raw_block_content.len()); + let mut final_markdown = String::with_capacity(raw_block_content.len()); for line in raw_block_content.lines() { // Bit of a jank way to remove at most branch.indent_level spaces from the front. let mut space_count = 0; @@ -76,8 +82,8 @@ pub fn branch_to_html( } } - unindented_block_content.push_str(&line[space_count..]); - unindented_block_content.push('\n'); + final_markdown.push_str(&line[space_count..]); + final_markdown.push('\n'); } let broken_link_callback = &mut |broken_link: BrokenLink<'_>| { @@ -112,8 +118,11 @@ pub fn branch_to_html( None } }; + if branch.attributes.template { + final_markdown = mini_template::render(config, treehouse, &final_markdown); + } let markdown_parser = pulldown_cmark::Parser::new_with_broken_link_callback( - &unindented_block_content, + &final_markdown, { use pulldown_cmark::Options; Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES diff --git a/crates/treehouse/src/tree/attributes.rs b/crates/treehouse/src/tree/attributes.rs index 4c3b169..ea12138 100644 --- a/crates/treehouse/src/tree/attributes.rs +++ b/crates/treehouse/src/tree/attributes.rs @@ -60,6 +60,10 @@ pub struct Attributes { /// Strings of extra CSS class names to include in the generated HTML. #[serde(default)] pub classes: Classes, + + /// Enable `mini_template` templating in this branch. + #[serde(default)] + pub template: bool, } /// Controls for block content presentation. @@ -88,6 +92,10 @@ pub enum Content { #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] pub struct Classes { + /// Classes to append to the branch itself (
  • ). + #[serde(default)] + pub branch: String, + /// Classes to append to the branch's