wip!
This commit is contained in:
parent
1305ffbb16
commit
1013c53975
|
@ -1,5 +1,11 @@
|
||||||
%% title = "tairu - an interactive exploration of 2D autotiling techniques"
|
%% 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"
|
% id = "01HPD4XQPWM8ECT2QM6AT9YRWB"
|
||||||
- I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain.
|
- 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
|
- TODO: short videos demoing this here
|
||||||
|
|
||||||
% id = "01HPD4XQPWJBTJ4DWAQE3J87C9"
|
% 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"
|
% 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 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"
|
% id = "01HPD4XQPWJ1CE9ZVRW98X7HE6"
|
||||||
- Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile:
|
- Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile:
|
||||||
|
|
||||||
% id = "01HPD4XQPWHNFQPRHX13MYW8GT"
|
% id = "01HPJ8GHDET8ZGNN0AH3FWA8HX"
|
||||||
- this technique involves assigning the cardinal directions (north, south, east, west) to a bitset.
|
- let's begin with a tilemap. say we have the following grid of tiles: (the examples are interactive, try editing it!)
|
||||||
then for each tile you look at which adjacent tiles should be connected to
|
|
||||||
|
|
||||||
% id = "01HPD4XQPWS2JS8RJH2P5TKPAB"
|
<canvas
|
||||||
- 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?"
|
is="tairu-editor"
|
||||||
|
data-tilemap-id="bitwiseAutotiling"
|
||||||
|
data-tile-size="40">
|
||||||
|
Your browser does not support <canvas>.
|
||||||
|
</canvas>
|
||||||
|
|
||||||
% id = "01HPD4XQPWAANYFBYX681787D1"
|
% id = "01HPJ8GHDEC0Z334M04MTNADV9"
|
||||||
- for example, "is the tile to the left a dirt tile?"
|
- for each tile, we can assign a bitset of cardinal directions like so:
|
||||||
|
|
||||||
% id = "01HPD4XQPWES5K2V2AKB7H0EHK"
|
<canvas
|
||||||
- and then you use this bitset to index into a lookup table of tiles
|
is="tairu-editor-cardinal-directions"
|
||||||
|
data-tilemap-id="bitwiseAutotiling"
|
||||||
|
data-tile-size="40">
|
||||||
|
Your browser does not support <canvas>.
|
||||||
|
</canvas>
|
||||||
|
|
||||||
% id = "01HPD4XQPWD00GDZ0N5H1DRH2P"
|
% template = true
|
||||||
- for example, say we have the following grid of tiles:\
|
id = "01HPJ8GHDE9QKQ4QFZK1Z1KQD4"
|
||||||
TODO editable grid on javascript
|
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:\
|
<ul class="tileset-demo">
|
||||||
TODO grid linked with the other grid to show which adjacent tiles each tile connects to
|
<li class="full-image">
|
||||||
|
<img alt="a 16-tile tileset of 8x8 pixel metal" src="{% pic 01HPHVDRV0F0251MD0A2EG66C4 %}">
|
||||||
|
</li>
|
||||||
|
<li class="tileset-pieces">
|
||||||
|
<span class="metal x-0 y-0"><span class="east">E</span><span class="south">S</span></span>
|
||||||
|
<span class="metal x-1 y-0"><span class="east">E</span><span class="south">S</span><span class="west">W</span></span>
|
||||||
|
<span class="metal x-2 y-0"><span class="south">S</span><span class="west">W</span></span>
|
||||||
|
<span class="metal x-3 y-0"><span class="south">S</span></span>
|
||||||
|
<span class="metal x-0 y-1"><span class="east">E</span><span class="south">S</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-1 y-1"><span class="east">E</span><span class="south">S</span><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-2 y-1"><span class="south">S</span><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-3 y-1"><span class="south">S</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-0 y-2"><span class="east">E</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-1 y-2"><span class="east">E</span><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-2 y-2"><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-3 y-2"><span class="north">N</span></span>
|
||||||
|
<span class="metal x-0 y-3"><span class="east">E</span></span>
|
||||||
|
<span class="metal x-1 y-3"><span class="east">E</span><span class="west">W</span></span>
|
||||||
|
<span class="metal x-2 y-3"><span class="west">W</span></span>
|
||||||
|
<span class="metal x-3 y-3"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
% id = "01HPD4XQPWM0AAE6F162EZTFQY"
|
% id = "01HPMVT9BM65YD5AXWPT4Z67H5"
|
||||||
- in JavaScript it would look something like this:
|
- (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:
|
||||||
|
|
||||||
|
<div class="horizontal-tile-strip">
|
||||||
|
<span class="metal x-3 y-3"></span>
|
||||||
|
<span class="metal x-0 y-3"><span class="east">E</span></span>
|
||||||
|
<span class="metal x-3 y-0"><span class="south">S</span></span>
|
||||||
|
<span class="metal x-0 y-0"><span class="east">E</span><span class="south">S</span></span>
|
||||||
|
<span class="metal x-2 y-3"><span class="west">W</span></span>
|
||||||
|
<span class="metal x-1 y-3"><span class="east">E</span><span class="west">W</span></span>
|
||||||
|
<span class="metal x-2 y-0"><span class="south">S</span><span class="west">W</span></span>
|
||||||
|
<span class="metal x-1 y-0"><span class="east">E</span><span class="south">S</span><span class="west">W</span></span>
|
||||||
|
<span class="metal x-3 y-2"><span class="north">N</span></span>
|
||||||
|
<span class="metal x-0 y-2"><span class="east">E</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-3 y-1"><span class="south">S</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-0 y-1"><span class="east">E</span><span class="south">S</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-2 y-2"><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-1 y-2"><span class="east">E</span><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-2 y-1"><span class="south">S</span><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
<span class="metal x-1 y-1"><span class="east">E</span><span class="south">S</span><span class="west">W</span><span class="north">N</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
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 `<canvas>` using bitwise autotiling would look like this:
|
||||||
```javascript
|
```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:
|
||||||
|
|
||||||
|
<canvas
|
||||||
|
is="tairu-editor"
|
||||||
|
data-tilemap-id="bitwiseAutotiling"
|
||||||
|
data-tile-size="40"
|
||||||
|
>
|
||||||
|
Your browser does not support <canvas>.
|
||||||
|
<img class="resource" src="{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}" data-tairu-tileset="1">
|
||||||
|
</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:
|
||||||
|
|
||||||
|
<canvas
|
||||||
|
is="tairu-editor"
|
||||||
|
data-tilemap-id="bitwiseAutotilingChapter2"
|
||||||
|
data-tile-size="40"
|
||||||
|
>
|
||||||
|
Your browser does not support <canvas>.
|
||||||
|
<img class="resource" src="{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}" data-tairu-tileset="1">
|
||||||
|
</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"
|
% 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
|
||||||
|
|
||||||
|
@ -55,6 +213,7 @@
|
||||||
|
|
||||||
[Planet Overgamma]: https://liquidev.itch.io/planet-overgamma-classic
|
[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.
|
- 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].
|
he autotiling source code of the one in the video can be found [here][autotiling source code].
|
||||||
|
|
||||||
|
@ -111,6 +270,9 @@
|
||||||
% id = "01HPD4XQPWK58Z63X6962STADR"
|
% 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?
|
- 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"
|
% 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
|
||||||
|
|
||||||
|
|
|
@ -229,15 +229,7 @@ impl Generator {
|
||||||
.thumbnail
|
.thumbnail
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|thumbnail| Thumbnail {
|
.map(|thumbnail| Thumbnail {
|
||||||
url: format!(
|
url: config.pic_url(&thumbnail.id),
|
||||||
"{}/static/pic/{}",
|
|
||||||
config.site,
|
|
||||||
config
|
|
||||||
.pics
|
|
||||||
.get(&thumbnail.id)
|
|
||||||
.map(|x| &**x)
|
|
||||||
.unwrap_or("404.png")
|
|
||||||
),
|
|
||||||
alt: thumbnail.alt.clone(),
|
alt: thumbnail.alt.clone(),
|
||||||
}),
|
}),
|
||||||
scripts: roots.attributes.scripts.clone(),
|
scripts: roots.attributes.scripts.clone(),
|
||||||
|
|
|
@ -99,4 +99,12 @@ impl Config {
|
||||||
}
|
}
|
||||||
Ok(())
|
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")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
html::EscapeAttribute,
|
html::EscapeAttribute,
|
||||||
state::{FileId, Treehouse},
|
state::{FileId, Treehouse},
|
||||||
tree::{attributes::Content, SemaBranchId},
|
tree::{attributes::Content, mini_template, SemaBranchId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{markdown, EscapeHtml};
|
use super::{markdown, EscapeHtml};
|
||||||
|
@ -26,6 +26,12 @@ pub fn branch_to_html(
|
||||||
!branch.children.is_empty() || matches!(branch.attributes.content, Content::Link(_));
|
!branch.children.is_empty() || matches!(branch.attributes.content, Content::Link(_));
|
||||||
|
|
||||||
let class = if has_children { "branch" } else { "leaf" };
|
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 {
|
let component = if let Content::Link(_) = branch.attributes.content {
|
||||||
"th-b-linked"
|
"th-b-linked"
|
||||||
} else {
|
} else {
|
||||||
|
@ -64,7 +70,7 @@ pub fn branch_to_html(
|
||||||
s.push_str("<th-bp></th-bp>");
|
s.push_str("<th-bp></th-bp>");
|
||||||
|
|
||||||
let raw_block_content = &source.input()[branch.content.clone()];
|
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() {
|
for line in raw_block_content.lines() {
|
||||||
// Bit of a jank way to remove at most branch.indent_level spaces from the front.
|
// Bit of a jank way to remove at most branch.indent_level spaces from the front.
|
||||||
let mut space_count = 0;
|
let mut space_count = 0;
|
||||||
|
@ -76,8 +82,8 @@ pub fn branch_to_html(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unindented_block_content.push_str(&line[space_count..]);
|
final_markdown.push_str(&line[space_count..]);
|
||||||
unindented_block_content.push('\n');
|
final_markdown.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
let broken_link_callback = &mut |broken_link: BrokenLink<'_>| {
|
let broken_link_callback = &mut |broken_link: BrokenLink<'_>| {
|
||||||
|
@ -112,8 +118,11 @@ pub fn branch_to_html(
|
||||||
None
|
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(
|
let markdown_parser = pulldown_cmark::Parser::new_with_broken_link_callback(
|
||||||
&unindented_block_content,
|
&final_markdown,
|
||||||
{
|
{
|
||||||
use pulldown_cmark::Options;
|
use pulldown_cmark::Options;
|
||||||
Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES
|
Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES
|
||||||
|
|
|
@ -60,6 +60,10 @@ pub struct Attributes {
|
||||||
/// Strings of extra CSS class names to include in the generated HTML.
|
/// Strings of extra CSS class names to include in the generated HTML.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub classes: Classes,
|
pub classes: Classes,
|
||||||
|
|
||||||
|
/// Enable `mini_template` templating in this branch.
|
||||||
|
#[serde(default)]
|
||||||
|
pub template: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls for block content presentation.
|
/// Controls for block content presentation.
|
||||||
|
@ -88,6 +92,10 @@ pub enum Content {
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
||||||
pub struct Classes {
|
pub struct Classes {
|
||||||
|
/// Classes to append to the branch itself (<li is="th-b">).
|
||||||
|
#[serde(default)]
|
||||||
|
pub branch: String,
|
||||||
|
|
||||||
/// Classes to append to the branch's <ul> element containing its children.
|
/// Classes to append to the branch's <ul> element containing its children.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub branch_children: String,
|
pub branch_children: String,
|
||||||
|
|
213
crates/treehouse/src/tree/mini_template.rs
Normal file
213
crates/treehouse/src/tree/mini_template.rs
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
//! Minimalistic templating engine that integrates with the .tree format and Markdown.
|
||||||
|
//!
|
||||||
|
//! Mostly to avoid pulling in Handlebars everywhere; mini_template, unlike Handlebars, also allows
|
||||||
|
//! for injecting *custom, stateful* context into the renderer, which is important for things like
|
||||||
|
//! the `pic` template to work.
|
||||||
|
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use pulldown_cmark::escape::escape_html;
|
||||||
|
|
||||||
|
use crate::{config::Config, state::Treehouse};
|
||||||
|
|
||||||
|
struct Lexer<'a> {
|
||||||
|
input: &'a str,
|
||||||
|
position: usize,
|
||||||
|
|
||||||
|
// Despite this parser's intentional simplicity, a peekahead buffer needs to be used for
|
||||||
|
// performance because tokens are usually quite long and therefore reparsing them would be
|
||||||
|
// too expensive.
|
||||||
|
peek_buffer: Option<(Token, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum TokenKind {
|
||||||
|
/// Verbatim text, may be inside of a template.
|
||||||
|
Text,
|
||||||
|
Open(EscapingMode), // {%
|
||||||
|
Close, // %}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum EscapingMode {
|
||||||
|
EscapeHtml,
|
||||||
|
NoEscaping,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct Token {
|
||||||
|
kind: TokenKind,
|
||||||
|
range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Lexer<'a> {
|
||||||
|
fn new(input: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
position: 0,
|
||||||
|
peek_buffer: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current(&self) -> Option<char> {
|
||||||
|
self.input[self.position..].chars().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(&mut self) {
|
||||||
|
self.position += self.current().map(|c| c.len_utf8()).unwrap_or(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_token(&self, start: usize, kind: TokenKind) -> Token {
|
||||||
|
Token {
|
||||||
|
kind,
|
||||||
|
range: start..self.position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_inner(&mut self) -> Option<Token> {
|
||||||
|
if let Some((token, after_token)) = self.peek_buffer.take() {
|
||||||
|
self.position = after_token;
|
||||||
|
return Some(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = self.position;
|
||||||
|
match self.current() {
|
||||||
|
Some('{') => {
|
||||||
|
self.advance();
|
||||||
|
if self.current() == Some('%') {
|
||||||
|
self.advance();
|
||||||
|
if self.current() == Some('!') {
|
||||||
|
Some(self.create_token(start, TokenKind::Open(EscapingMode::NoEscaping)))
|
||||||
|
} else {
|
||||||
|
Some(self.create_token(start, TokenKind::Open(EscapingMode::EscapeHtml)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.advance();
|
||||||
|
Some(self.create_token(start, TokenKind::Text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some('%') => {
|
||||||
|
self.advance();
|
||||||
|
if self.current() == Some('}') {
|
||||||
|
self.advance();
|
||||||
|
Some(self.create_token(start, TokenKind::Close))
|
||||||
|
} else {
|
||||||
|
self.advance();
|
||||||
|
Some(self.create_token(start, TokenKind::Text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
while !matches!(self.current(), Some('{' | '%') | None) {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
Some(self.create_token(start, TokenKind::Text))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_inner(&mut self) -> Option<Token> {
|
||||||
|
let position = self.position;
|
||||||
|
let token = self.next();
|
||||||
|
let after_token = self.position;
|
||||||
|
self.position = position;
|
||||||
|
|
||||||
|
if let Some(token) = token.clone() {
|
||||||
|
self.peek_buffer = Some((token, after_token));
|
||||||
|
}
|
||||||
|
|
||||||
|
token
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Token> {
|
||||||
|
self.next_inner().map(|mut token| {
|
||||||
|
// Coalesce multiple Text tokens into one.
|
||||||
|
if token.kind == TokenKind::Text {
|
||||||
|
while let Some(Token {
|
||||||
|
kind: TokenKind::Text,
|
||||||
|
..
|
||||||
|
}) = self.peek_inner()
|
||||||
|
{
|
||||||
|
let next_token = self.next_inner().unwrap();
|
||||||
|
token.range.end = next_token.range.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Renderer<'a> {
|
||||||
|
lexer: Lexer<'a>,
|
||||||
|
output: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InvalidTemplate;
|
||||||
|
|
||||||
|
impl<'a> Renderer<'a> {
|
||||||
|
fn emit_token_verbatim(&mut self, token: &Token) {
|
||||||
|
self.output.push_str(&self.lexer.input[token.range.clone()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, config: &Config, treehouse: &Treehouse) {
|
||||||
|
let kind_of = |token: &Token| token.kind;
|
||||||
|
|
||||||
|
while let Some(token) = self.lexer.next() {
|
||||||
|
match token.kind {
|
||||||
|
TokenKind::Open(escaping) => {
|
||||||
|
let inside = self.lexer.next();
|
||||||
|
let close = self.lexer.next();
|
||||||
|
|
||||||
|
if let Some((TokenKind::Text, TokenKind::Close)) = inside
|
||||||
|
.as_ref()
|
||||||
|
.map(kind_of)
|
||||||
|
.zip(close.as_ref().map(kind_of))
|
||||||
|
{
|
||||||
|
match Self::render_template(
|
||||||
|
config,
|
||||||
|
treehouse,
|
||||||
|
self.lexer.input[inside.as_ref().unwrap().range.clone()].trim(),
|
||||||
|
) {
|
||||||
|
Ok(s) => match escaping {
|
||||||
|
EscapingMode::EscapeHtml => {
|
||||||
|
_ = escape_html(&mut self.output, &s);
|
||||||
|
}
|
||||||
|
EscapingMode::NoEscaping => self.output.push_str(&s),
|
||||||
|
},
|
||||||
|
Err(InvalidTemplate) => {
|
||||||
|
inside.inspect(|token| self.emit_token_verbatim(token));
|
||||||
|
close.inspect(|token| self.emit_token_verbatim(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inside.inspect(|token| self.emit_token_verbatim(token));
|
||||||
|
close.inspect(|token| self.emit_token_verbatim(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self.emit_token_verbatim(&token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_template(
|
||||||
|
config: &Config,
|
||||||
|
_treehouse: &Treehouse,
|
||||||
|
template: &str,
|
||||||
|
) -> Result<String, InvalidTemplate> {
|
||||||
|
let (function, arguments) = template.split_once(' ').unwrap_or((template, ""));
|
||||||
|
match function {
|
||||||
|
"pic" => Ok(config.pic_url(arguments)),
|
||||||
|
"c++" => Ok("<script>alert(1)</script>".into()),
|
||||||
|
_ => Err(InvalidTemplate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(config: &Config, treehouse: &Treehouse, input: &str) -> String {
|
||||||
|
let mut renderer = Renderer {
|
||||||
|
lexer: Lexer::new(input),
|
||||||
|
output: String::new(),
|
||||||
|
};
|
||||||
|
renderer.render(config, treehouse);
|
||||||
|
renderer.output
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod attributes;
|
pub mod attributes;
|
||||||
|
pub mod mini_template;
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,12 @@ body::selection {
|
||||||
src: url('../font/Recursive_VF_1.085.woff2');
|
src: url('../font/Recursive_VF_1.085.woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'RecVarMono';
|
||||||
|
src: url('../font/Recursive_VF_1.085.woff2');
|
||||||
|
font-variation-settings: "MONO" 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
body,
|
body,
|
||||||
pre,
|
pre,
|
||||||
|
@ -256,6 +262,29 @@ img.pic {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Image hints for tweaking rendering */
|
||||||
|
img {
|
||||||
|
&[src*='+pixel'] {
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[src*='+width160'] {
|
||||||
|
width: 160px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[src*='+width640'] {
|
||||||
|
width: 640px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resources for use in JavaScript. */
|
||||||
|
&.resource {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Fix the default blue and ugly purple links normally have */
|
/* Fix the default blue and ugly purple links normally have */
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -1 +1,128 @@
|
||||||
|
.tileset-cardinal-directions-demo th-bc {
|
||||||
|
& ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
& ul.tileset-demo {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& ul.tileset-demo::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
& li.full-image {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& li.tileset-pieces {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .horizontal-tile-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& .metal {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
&>span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .metal {
|
||||||
|
display: inline-block;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin: 8px;
|
||||||
|
background-image: url('../pic/01HPHVDRV0F0251MD0A2EG66C4-tilemap-heavy-metal-16+pixel+width160.png');
|
||||||
|
background-size: 400%;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
& .east,
|
||||||
|
& .south,
|
||||||
|
& .west,
|
||||||
|
& .north {
|
||||||
|
--recursive-wght: 900;
|
||||||
|
--recursive-casl: 0.0;
|
||||||
|
--recursive-slnt: 0.0;
|
||||||
|
--recursive-mono: 1.0;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
position: absolute;
|
||||||
|
color: #d3dce9;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .east {
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .south {
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .west {
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .north {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .off {
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .x-0 {
|
||||||
|
background-position-x: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .x-1 {
|
||||||
|
background-position-x: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .x-2 {
|
||||||
|
background-position-x: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .x-3 {
|
||||||
|
background-position-x: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .y-0 {
|
||||||
|
background-position-y: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .y-1 {
|
||||||
|
background-position-y: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .y-2 {
|
||||||
|
background-position-y: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .y-3 {
|
||||||
|
background-position-y: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -351,7 +351,35 @@ th-bb .branch-date {
|
||||||
/* branch-quote class for "air quote branches"; used to separate a subtree from a parent tree
|
/* 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. */
|
stylistically such that it's interpretable as a form of block quote. */
|
||||||
ul.branch-quote {
|
ul.branch-quote {
|
||||||
padding: 8px;
|
--vertical-margin: 8px;
|
||||||
|
--padding: 8px;
|
||||||
|
|
||||||
|
margin-top: var(--vertical-margin);
|
||||||
|
margin-bottom: var(--vertical-margin);
|
||||||
|
padding-top: var(--padding);
|
||||||
|
padding-bottom: var(--padding);
|
||||||
|
padding-right: var(--padding);
|
||||||
border: 1px solid var(--border-1);
|
border: 1px solid var(--border-1);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
--recursive-wght: 900;
|
||||||
|
--recursive-casl: 0;
|
||||||
|
|
||||||
|
content: '“';
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 1px;
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 50%;
|
||||||
|
|
||||||
|
transition: opacity var(--transition-duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::before {
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
41
static/js/tairu/cardinal-directions.js
Normal file
41
static/js/tairu/cardinal-directions.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { defineFrame, Frame } from './framework.js';
|
||||||
|
import { TileEditor, canConnect, shouldConnect } from './tairu.js';
|
||||||
|
|
||||||
|
class CardinalDirectionsEditor extends TileEditor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.colorScheme.tiles[1] = "#f96565";
|
||||||
|
}
|
||||||
|
|
||||||
|
drawConnectionText(text, enabled, tileX, tileY, hAlign, vAlign) {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.fillStyle = enabled ? "#6c023e" : "#d84161";
|
||||||
|
this.ctx.font = `800 14px ${Frame.monoFontFace}`;
|
||||||
|
const padding = 2;
|
||||||
|
let topLeftX = tileX * this.tileSize + padding;
|
||||||
|
let topLeftY = tileY * this.tileSize + padding;
|
||||||
|
let rectSize = this.tileSize - padding * 2;
|
||||||
|
let { leftX, baselineY } = this.getTextPositionInBox(text, topLeftX, topLeftY, rectSize, rectSize, hAlign, vAlign);
|
||||||
|
this.ctx.fillText(text, leftX, baselineY);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTiles() {
|
||||||
|
super.drawTiles();
|
||||||
|
for (let y = 0; y < this.tilemap.height; ++y) {
|
||||||
|
for (let x = 0; x < this.tilemap.width; ++x) {
|
||||||
|
let tile = this.tilemap.at(x, y);
|
||||||
|
if (canConnect(tile)) {
|
||||||
|
let connectedWithEast = shouldConnect(tile, this.tilemap.at(x + 1, y));
|
||||||
|
let connectedWithSouth = shouldConnect(tile, this.tilemap.at(x, y + 1));
|
||||||
|
let connectedWithNorth = shouldConnect(tile, this.tilemap.at(x, y - 1));
|
||||||
|
let connectedWithWest = shouldConnect(tile, this.tilemap.at(x - 1, y));
|
||||||
|
this.drawConnectionText("E", connectedWithEast, x, y, "right", "center");
|
||||||
|
this.drawConnectionText("S", connectedWithSouth, x, y, "center", "bottom");
|
||||||
|
this.drawConnectionText("N", connectedWithNorth, x, y, "center", "top");
|
||||||
|
this.drawConnectionText("W", connectedWithWest, x, y, "left", "center");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineFrame("tairu-editor-cardinal-directions", CardinalDirectionsEditor);
|
|
@ -1,13 +1,87 @@
|
||||||
// A frameworking class assigning some CSS classes to the canvas to make it integrate nicer with CSS.
|
// A frameworking class assigning some CSS classes to the canvas to make it integrate nicer with CSS.
|
||||||
class Frame extends HTMLCanvasElement {
|
export class Frame extends HTMLCanvasElement {
|
||||||
|
static fontFace = "RecVar";
|
||||||
|
static monoFontFace = "RecVarMono";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
this.style.cssText = `
|
this.style.cssText = `
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
this.ctx = this.getContext("2d");
|
||||||
|
|
||||||
|
requestAnimationFrame(this.#drawLoop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
#drawLoop() {
|
||||||
|
this.ctx.font = "14px RecVar";
|
||||||
|
this.draw();
|
||||||
|
requestAnimationFrame(this.#drawLoop.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override this!
|
// Override this!
|
||||||
draw() { }
|
draw() {
|
||||||
|
throw new ReferenceError("draw() must be overridden");
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextPositionInBox(text, x, y, width, height, hAlign, vAlign) {
|
||||||
|
let measurements = this.ctx.measureText(text);
|
||||||
|
|
||||||
|
let leftX;
|
||||||
|
switch (hAlign) {
|
||||||
|
case "left":
|
||||||
|
leftX = x;
|
||||||
|
break;
|
||||||
|
case "center":
|
||||||
|
leftX = x + width / 2 - measurements.width / 2;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
leftX = x + width - measurements.width;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textHeight = measurements.fontBoundingBoxAscent;
|
||||||
|
let baselineY;
|
||||||
|
switch (vAlign) {
|
||||||
|
case "top":
|
||||||
|
baselineY = y + textHeight;
|
||||||
|
break;
|
||||||
|
case "center":
|
||||||
|
baselineY = y + height / 2 + textHeight / 2;
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
baselineY = y + height;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { leftX, baselineY };
|
||||||
|
}
|
||||||
|
|
||||||
|
get scaleInViewportX() {
|
||||||
|
return this.clientWidth / this.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get scaleInViewportY() {
|
||||||
|
return this.clientHeight / this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMousePositionFromEvent(event) {
|
||||||
|
return {
|
||||||
|
x: event.offsetX / this.scaleInViewportX,
|
||||||
|
y: event.offsetY / this.scaleInViewportY,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function defineFrame(elementName, claß) { // because `class` is a keyword.
|
||||||
|
customElements.define(elementName, claß, { extends: "canvas" });
|
||||||
|
}
|
||||||
|
|
||||||
|
defineFrame("tairu--frame", Frame);
|
||||||
|
|
|
@ -1,8 +1,192 @@
|
||||||
class TileEditor extends HTMLCanvasElement {
|
import { Frame, defineFrame } from "./framework.js";
|
||||||
|
import tilemapRegistry from "./tilemap-registry.js";
|
||||||
|
|
||||||
|
export function canConnect(tile) {
|
||||||
|
return tile == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldConnect(a, b) {
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TileEditor extends Frame {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.tileCursor = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
this.colorScheme = {
|
||||||
|
background: "#F7F7F7",
|
||||||
|
grid: "#00000011",
|
||||||
|
tileCursor: "#222222",
|
||||||
|
tiles: [
|
||||||
|
"transparent",
|
||||||
|
"#eb134a",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tileColorPalette = [
|
||||||
|
"transparent",
|
||||||
|
"#eb134a",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
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 {
|
||||||
|
throw new ReferenceError(`tilemap '${tilemapId}' does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0st element is explicitly null because it represents the empty tile.
|
||||||
|
this.tilesets = [null];
|
||||||
|
|
||||||
|
let attachedImages = this.getElementsByTagName("img");
|
||||||
|
for (let image of attachedImages) {
|
||||||
|
if (image.hasAttribute("data-tairu-tileset")) {
|
||||||
|
let tilesetIndex = parseInt(image.getAttribute("data-tairu-tileset"));
|
||||||
|
this.tilesets[tilesetIndex] = image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.width = this.tilemap.width * this.tileSize;
|
||||||
|
this.height = this.tilemap.height * this.tileSize;
|
||||||
|
|
||||||
|
this.hasFocus = false;
|
||||||
|
this.paintingTile = null;
|
||||||
|
|
||||||
|
this.addEventListener("mousemove", event => this.mouseMoved(event));
|
||||||
|
this.addEventListener("mousedown", event => this.mousePressed(event));
|
||||||
|
this.addEventListener("mouseup", event => this.mouseReleased(event));
|
||||||
|
|
||||||
|
this.addEventListener("mouseenter", _ => this.hasFocus = true);
|
||||||
|
this.addEventListener("mouseleave", _ => this.hasFocus = false);
|
||||||
|
|
||||||
|
this.addEventListener("contextmenu", event => event.preventDefault());
|
||||||
|
|
||||||
|
// TODO: This should also work on mobile.
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
this.ctx.fillStyle = this.colorScheme.background;
|
||||||
|
this.ctx.fillRect(0, 0, this.width, this.height);
|
||||||
|
|
||||||
|
this.drawTiles();
|
||||||
|
this.drawGrid();
|
||||||
|
if (this.hasFocus) {
|
||||||
|
this.drawTileCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid() {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
for (let x = 0; x < this.tilemap.width; ++x) {
|
||||||
|
this.ctx.moveTo(x * this.tileSize, 0);
|
||||||
|
this.ctx.lineTo(x * this.tileSize, this.height);
|
||||||
|
}
|
||||||
|
for (let y = 0; y < this.tilemap.width; ++y) {
|
||||||
|
this.ctx.moveTo(0, y * this.tileSize);
|
||||||
|
this.ctx.lineTo(this.width, y * this.tileSize);
|
||||||
|
}
|
||||||
|
this.ctx.strokeStyle = this.colorScheme.grid;
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTileCursor() {
|
||||||
|
this.ctx.strokeStyle = this.colorScheme.tileCursor;
|
||||||
|
this.ctx.lineWidth = 5;
|
||||||
|
this.ctx.strokeRect(this.tileCursor.x * this.tileSize, this.tileCursor.y * this.tileSize, this.tileSize, this.tileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasTilesets() {
|
||||||
|
// Remember that tile 0 represents emptiness.
|
||||||
|
return this.tilesets.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTiles() {
|
||||||
|
if (this.hasTilesets) {
|
||||||
|
this.drawTexturedTiles();
|
||||||
|
} else {
|
||||||
|
this.drawColoredTiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawColoredTiles() {
|
||||||
|
for (let y = 0; y < this.tilemap.height; ++y) {
|
||||||
|
for (let x = 0; x < this.tilemap.width; ++x) {
|
||||||
|
let tile = this.tilemap.at(x, y);
|
||||||
|
if (tile != 0) {
|
||||||
|
this.ctx.fillStyle = this.colorScheme.tiles[tile];
|
||||||
|
this.ctx.fillRect(x * this.tileSize, y * this.tileSize, this.tileSize, this.tileSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTexturedTiles() {
|
||||||
|
this.ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
for (let y = 0; y < this.tilemap.height; ++y) {
|
||||||
|
for (let x = 0; x < this.tilemap.width; ++x) {
|
||||||
|
let tile = this.tilemap.at(x, y);
|
||||||
|
if (tile != 0) {
|
||||||
|
let tileset = this.tilesets[tile];
|
||||||
|
|
||||||
|
let connectedWithEast = shouldConnect(tile, this.tilemap.at(x + 1, y)) ? 0b0001 : 0;
|
||||||
|
let connectedWithSouth = shouldConnect(tile, this.tilemap.at(x, y + 1)) ? 0b0010 : 0;
|
||||||
|
let connectedWithWest = shouldConnect(tile, this.tilemap.at(x - 1, y)) ? 0b0100 : 0;
|
||||||
|
let connectedWithNorth = shouldConnect(tile, this.tilemap.at(x, y - 1)) ? 0b1000 : 0;
|
||||||
|
let tileIndex = connectedWithNorth
|
||||||
|
| connectedWithWest
|
||||||
|
| connectedWithSouth
|
||||||
|
| connectedWithEast;
|
||||||
|
|
||||||
|
let tilesetTileSize = tileset.height;
|
||||||
|
let tilesetX = tileIndex * tilesetTileSize;
|
||||||
|
let tilesetY = 0;
|
||||||
|
this.ctx.drawImage(
|
||||||
|
this.tilesets[tile],
|
||||||
|
tilesetX, tilesetY, tilesetTileSize, tilesetTileSize,
|
||||||
|
x * this.tileSize, y * this.tileSize, this.tileSize, this.tileSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseMoved(event) {
|
||||||
|
let mouse = this.getMousePositionFromEvent(event);
|
||||||
|
this.tileCursor.x = Math.floor(mouse.x / this.tileSize);
|
||||||
|
this.tileCursor.y = Math.floor(mouse.y / this.tileSize);
|
||||||
|
this.paintTileUnderCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
mousePressed(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.button == 0) {
|
||||||
|
this.paintingTile = 1;
|
||||||
|
} else if (event.button == 2) {
|
||||||
|
this.paintingTile = 0;
|
||||||
|
}
|
||||||
|
this.paintTileUnderCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseReleased() {
|
||||||
|
this.paintingTile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
paintTileUnderCursor() {
|
||||||
|
if (this.paintingTile != null) {
|
||||||
|
this.tilemap.setAt(this.tileCursor.x, this.tileCursor.y, this.paintingTile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define("tairu-tile-editor", TileEditor)
|
defineFrame("tairu-editor", TileEditor);
|
||||||
|
|
||||||
|
console.log("tairu editor loaded");
|
||||||
|
|
33
static/js/tairu/tilemap-registry.js
Normal file
33
static/js/tairu/tilemap-registry.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Tilemap } from './tilemap.js';
|
||||||
|
|
||||||
|
const alphabet = " x";
|
||||||
|
|
||||||
|
function parseTilemap(lineArray) {
|
||||||
|
let tilemap = new Tilemap(lineArray[0].length, lineArray.length);
|
||||||
|
for (let y in lineArray) {
|
||||||
|
let line = lineArray[y];
|
||||||
|
for (let x = 0; x < line.length; ++x) {
|
||||||
|
let char = line.charAt(x);
|
||||||
|
tilemap.setAt(x, y, alphabet.indexOf(char));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tilemap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
bitwiseAutotiling: parseTilemap([
|
||||||
|
" ",
|
||||||
|
" xxx ",
|
||||||
|
" xxx ",
|
||||||
|
" xxx ",
|
||||||
|
" ",
|
||||||
|
]),
|
||||||
|
bitwiseAutotilingChapter2: parseTilemap([
|
||||||
|
" ",
|
||||||
|
" x ",
|
||||||
|
" x ",
|
||||||
|
" xxx ",
|
||||||
|
" ",
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
30
static/js/tairu/tilemap.js
Normal file
30
static/js/tairu/tilemap.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export class Tilemap {
|
||||||
|
constructor(width, height) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.tiles = new Uint8Array(width * height);
|
||||||
|
this.default = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tileIndex(x, y) {
|
||||||
|
return x + y * this.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
inBounds(x, y) {
|
||||||
|
return x >= 0 && y >= 0 && x < this.width && y < this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
at(x, y) {
|
||||||
|
if (this.inBounds(x, y)) {
|
||||||
|
return this.tiles[this.tileIndex(x, y)];
|
||||||
|
} else {
|
||||||
|
return this.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAt(x, y, tile) {
|
||||||
|
if (this.inBounds(x, y)) {
|
||||||
|
this.tiles[this.tileIndex(x, y)] = tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -112,11 +112,17 @@ class LinkedBranch extends Branch {
|
||||||
let styles = main.getElementsByTagName("link");
|
let styles = main.getElementsByTagName("link");
|
||||||
let scripts = main.getElementsByTagName("script");
|
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;
|
||||||
|
|
||||||
|
this.append(...styles);
|
||||||
|
for (let script of scripts) {
|
||||||
|
// No need to await for the import because we don't use the resulting module.
|
||||||
|
// Just fire and forger 💀
|
||||||
|
// and let them run in parallel.
|
||||||
|
import(script.src);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loadingText.innerText = error.toString();
|
this.loadingText.innerText = error.toString();
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 284 B |
Binary file not shown.
After Width: | Height: | Size: 313 B |
Binary file not shown.
After Width: | Height: | Size: 313 B |
|
@ -94,11 +94,11 @@
|
||||||
extracting them way more painful than it needs to be. --}}
|
extracting them way more painful than it needs to be. --}}
|
||||||
|
|
||||||
{{#each page.styles}}
|
{{#each page.styles}}
|
||||||
<link rel="stylesheet" src="{{ ../config.site }}/static/css/{{ this }}">
|
<link rel="stylesheet" href="{{ ../config.site }}/static/css/{{ this }}">
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
{{#each page.scripts}}
|
{{#each page.scripts}}
|
||||||
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}" defer></script>
|
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
{{{ page.tree }}}
|
{{{ page.tree }}}
|
||||||
|
|
Loading…
Reference in a new issue