This commit is contained in:
liquidex 2024-02-12 19:56:06 +01:00
parent 81018eeafe
commit f2e9a5f66e
11 changed files with 126 additions and 16 deletions

View file

@ -20,7 +20,9 @@
id = "programming/opinions" id = "programming/opinions"
+ ### opinions + ### opinions
% id = "01HPD4XQQ5GPQ20C6BPA8G670F"
- ### blog - ### blog
% content.link = "programming/blog/tairu" % content.link = "programming/blog/tairu"
id = "01HPD4XQQ5WM0APCAX014HM43V"
+ tairu - an interactive exploration of 2D autotiling techniques + tairu - an interactive exploration of 2D autotiling techniques

View file

@ -1,86 +1,131 @@
%% title = "tairu - an interactive exploration of 2D autotiling techniques" %% title = "tairu - an interactive exploration of 2D autotiling techniques"
scripts = ["tairu/tairu.js"] scripts = ["tairu/tiling-demo.js", "tairu/tairu.js"]
+ I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain. % id = "01HPD4XQPWM8ECT2QM6AT9YRWB"
- I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain.
% id = "01HPD4XQPWPDBH6QQAZER7A05G"
- in Terraria, you can fully modify the terrain however you want, and the tiles will connect to each other seamlessly. - in Terraria, you can fully modify the terrain however you want, and the tiles will connect to each other seamlessly.
% id = "01HPD4XQPW8HE7681P7H686X4N"
- TODO: short videos demoing this here - 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*
+ this technique involves assigning the cardinal directions (north, south, east, west) to a bitset. % 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
% 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 then for each tile you look at which adjacent tiles should be connected to
% 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?" - 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?"
% id = "01HPD4XQPWAANYFBYX681787D1"
- for example, "is the tile to the left a dirt tile?" - for example, "is the tile to the left a dirt tile?"
% id = "01HPD4XQPWES5K2V2AKB7H0EHK"
- and then you use this bitset to index into a lookup table of tiles - and then you use this bitset to index into a lookup table of tiles
% id = "01HPD4XQPWD00GDZ0N5H1DRH2P"
- for example, say we have the following grid of tiles:\ - for example, say we have the following grid of tiles:\
TODO editable grid on javascript TODO editable grid on javascript
for each tile, we can assign a bitset of cardinal directions like so:\ 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 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: - in JavaScript it would look something like this:
```javascript ```javascript
// TODO code example // TODO code example
``` ```
% 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
- my (very messy) LÖVE-based game engine `lovejam`, powering my game jam games from 2018, had an autotiling feature in its level editor based on this % 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
- TODO video of Planet Overgamma editor doing the magic after issuing the `at` command (or whatever it was called, i forgor :skull:) TODO video of some Planet Overgamma gameplay showing the autotiling in action
- every iteration of Planet Overgamma since then has been using this exact technique for procedurally generated, editable terrain [Planet Overgamma]: https://liquidev.itch.io/planet-overgamma-classic
+ but one day I found a really cool project called Tilekit (TODO link) - 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].
[autotiling source code]: https://github.com/liquidev/planet-overgamma/blob/classic/jam/map.lua#L209
% id = "01HPD4XQPWPN6HNA6M6EH507C6"
+ 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 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` 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, 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.* - 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. 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. while tinkering with your editor is something really cool, in my experience it's only cool up to a point.
% id = "01HPD4XQPWV1BAPA27SNDFR93B"
- the cool thing with Tilekit is that it's *more* than just your average bitwise autotiling - of course it *can* do basic autotiling, but it can also do so much more - the cool thing with Tilekit is that it's *more* than just your average bitwise autotiling - of course it *can* do basic autotiling, but it can also do so much more
% id = "01HPD4XQPWM1JSAPXVT6NBHKYY"
classes.branch_children = "branch-quote"
- if I had to describe it, it's basically something of a *shader langauge for tilesets.* this makes it really powerful, as you can do little programs like - if I had to describe it, it's basically something of a *shader langauge for tilesets.* this makes it really powerful, as you can do little programs like
% id = "01HPD4XQPWE7ZVR0SS67DHTGHQ"
- autotile using this base tileset - autotile using this base tileset
% id = "01HPD4XQPW2BFZYQQ920SYHM9M"
- if the tile above is empty AND with a 50% chance - if the tile above is empty AND with a 50% chance
% id = "01HPD4XQPWJB7V67TS1M3HFCYE"
- then grass - then grass
% id = "01HPD4XQPWF7K85Z0CEK4WDDBZ"
- if the tile above is solid AND with a 10% chance - if the tile above is solid AND with a 10% chance
% id = "01HPD4XQPW5J3N6MVT9Z2W00S9"
- then vines - then vines
% id = "01HPD4XQPWGCMCEAR5Z9EETSGP"
- if the tile above is vines AND with a 50% chance - if the tile above is vines AND with a 50% chance
% id = "01HPD4XQPWP847T0EAM0FJ88T4"
- then vines - then vines
- 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 = "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 = "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 - 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. - and what you're reading is the result of that.
+ so let's get going! first, we'll build a basic tile editor using JavaScript. % id = "01HPD4XQPW1EP8YHACRJVMA0GM"
<script type="module" src="{{ config.site }}/static/js/tairu/tairu.js"></script> - 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. + 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. - 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. 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. - I'll be using Web Components (in particular, custom elements) combined with canvas to add stuff onto the page.

View file

@ -203,6 +203,7 @@ impl Generator {
pub title: String, pub title: String,
pub thumbnail: Option<Thumbnail>, pub thumbnail: Option<Thumbnail>,
pub scripts: Vec<String>, pub scripts: Vec<String>,
pub styles: Vec<String>,
pub breadcrumbs: String, pub breadcrumbs: String,
pub tree_path: Option<String>, pub tree_path: Option<String>,
pub tree: String, pub tree: String,
@ -240,6 +241,7 @@ impl Generator {
alt: thumbnail.alt.clone(), alt: thumbnail.alt.clone(),
}), }),
scripts: roots.attributes.scripts.clone(), scripts: roots.attributes.scripts.clone(),
styles: roots.attributes.styles.clone(),
breadcrumbs, breadcrumbs,
tree_path: treehouse tree_path: treehouse
.tree_path(parsed_tree.file_id) .tree_path(parsed_tree.file_id)

View file

@ -158,7 +158,16 @@ pub fn branch_to_html(
if has_children { if has_children {
s.push_str("</summary>"); s.push_str("</summary>");
{ {
s.push_str("<ul>"); s.push_str("<ul");
if !branch.attributes.classes.branch_children.is_empty() {
write!(
s,
" class=\"{}\"",
EscapeAttribute(&branch.attributes.classes.branch_children)
)
.unwrap();
}
s.push('>');
let num_children = branch.children.len(); let num_children = branch.children.len();
for i in 0..num_children { for i in 0..num_children {
let child_id = treehouse.tree.branch(branch_id).children[i]; let child_id = treehouse.tree.branch(branch_id).children[i];

View file

@ -21,6 +21,11 @@ pub struct RootAttributes {
/// These are relative to the /static/js directory. /// These are relative to the /static/js directory.
#[serde(default)] #[serde(default)]
pub scripts: Vec<String>, pub scripts: Vec<String>,
/// Additional styles to load into to the page.
/// These are relative to the /static/css directory.
#[serde(default)]
pub styles: Vec<String>,
} }
/// A picture reference. /// A picture reference.
@ -51,6 +56,10 @@ pub struct Attributes {
/// Do not persist the branch in localStorage. /// Do not persist the branch in localStorage.
#[serde(default)] #[serde(default)]
pub do_not_persist: bool, pub do_not_persist: bool,
/// Strings of extra CSS class names to include in the generated HTML.
#[serde(default)]
pub classes: Classes,
} }
/// Controls for block content presentation. /// Controls for block content presentation.
@ -76,3 +85,10 @@ pub enum Content {
/// children, an `attribute`-type error is raised. /// children, an `attribute`-type error is raised.
Link(String), Link(String),
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
pub struct Classes {
/// Classes to append to the branch's <ul> element containing its children.
#[serde(default)]
pub branch_children: String,
}

1
static/css/tairu.css Normal file
View file

@ -0,0 +1 @@

View file

@ -346,4 +346,12 @@ th-bb .branch-date {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 0% 50%; background-position: 0% 50%;
opacity: 80%; opacity: 80%;
} }
/* branch-quote class for "air quote branches"; used to separate a subtree from a parent tree
stylistically such that it's interpretable as a form of block quote. */
ul.branch-quote {
padding: 8px;
border: 1px solid var(--border-1);
border-radius: 8px;
}

View file

@ -0,0 +1,13 @@
// A frameworking class assigning some CSS classes to the canvas to make it integrate nicer with CSS.
class Frame extends HTMLCanvasElement {
constructor() {
super();
this.style.cssText = `
`;
}
// Override this!
draw() { }
}

View file

View file

@ -109,6 +109,11 @@ class LinkedBranch extends Branch {
let linkedDocument = parser.parseFromString(text, "text/html"); let linkedDocument = parser.parseFromString(text, "text/html");
let main = linkedDocument.getElementsByTagName("main")[0]; let main = linkedDocument.getElementsByTagName("main")[0];
let ul = main.getElementsByTagName("ul")[0]; let ul = main.getElementsByTagName("ul")[0];
let styles = main.getElementsByTagName("link");
let scripts = main.getElementsByTagName("script");
this.append(...styles);
this.append(...scripts);
this.loadingText.remove(); this.loadingText.remove();
this.innerUL.innerHTML = ul.innerHTML; this.innerUL.innerHTML = ul.innerHTML;

View file

@ -26,6 +26,7 @@
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css"> <link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css"> <link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
<script>const TREEHOUSE_SITE = `{{ config.site }}`;</script> <script>const TREEHOUSE_SITE = `{{ config.site }}`;</script>
<script type="module" src="{{ config.site }}/navmap.js"></script> <script type="module" src="{{ config.site }}/navmap.js"></script>
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script> <script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
@ -33,10 +34,6 @@
<script type="module" src="{{ config.site }}/static/js/tree.js"></script> <script type="module" src="{{ config.site }}/static/js/tree.js"></script>
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script> <script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
<script type="module" src="{{ config.site }}/static/js/thanks-webkit.js"></script> <script type="module" src="{{ config.site }}/static/js/thanks-webkit.js"></script>
{{#each page.scripts}}
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
{{/each}}
</head> </head>
<body> <body>
@ -92,6 +89,18 @@
</div> </div>
<main class="tree"> <main class="tree">
{{!-- Append page styles and scripts into the main content, such that they can be inlined
into linked branches when those are loaded in. Putting them in the page's head would make
extracting them way more painful than it needs to be. --}}
{{#each page.styles}}
<link rel="stylesheet" src="{{ ../config.site }}/static/css/{{ this }}">
{{/each}}
{{#each page.scripts}}
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}" defer></script>
{{/each}}
{{{ page.tree }}} {{{ page.tree }}}
</main> </main>