big commit
This commit is contained in:
		
							parent
							
								
									aff885cf17
								
							
						
					
					
						commit
						b506f5a219
					
				
					 22 changed files with 692 additions and 556 deletions
				
			
		| 
						 | 
				
			
			@ -67,6 +67,13 @@
 | 
			
		|||
        % id = "01H8W7VEVPSHDWDH58HFKBMGD6"
 | 
			
		||||
        - but I don't wanna replace it because it's just too good
 | 
			
		||||
 | 
			
		||||
- ### random places
 | 
			
		||||
 | 
			
		||||
    % id = "emoji/ahyes"
 | 
			
		||||
    - :ahyes: - ah, yes
 | 
			
		||||
 | 
			
		||||
        - smuggest expression for the smuggest of moments, and some tea with it.
 | 
			
		||||
 | 
			
		||||
% id = "01HA4HJKQ7FKV8JJ70Q2CY9R86"
 | 
			
		||||
- ### [Noto Color Emoji](https://github.com/googlefonts/noto-emoji/)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,6 @@
 | 
			
		|||
%% 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"]
 | 
			
		||||
| 
						 | 
				
			
			@ -20,8 +15,9 @@ styles = ["tairu.css"]
 | 
			
		|||
        - TODO: short videos demoing this here
 | 
			
		||||
 | 
			
		||||
% id = "01HPD4XQPWJBTJ4DWAQE3J87C9"
 | 
			
		||||
- once upon a time I stumbled upon a technique called...\
 | 
			
		||||
**bitwise autotiling**
 | 
			
		||||
- once upon a time I stumbled upon a technique called...
 | 
			
		||||
 | 
			
		||||
- ### bitwise autotiling
 | 
			
		||||
 | 
			
		||||
    % id = "01HPD4XQPW6VK3FDW5QRCE6HSS"
 | 
			
		||||
    + 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
 | 
			
		||||
| 
						 | 
				
			
			@ -29,37 +25,194 @@ styles = ["tairu.css"]
 | 
			
		|||
        % id = "01HPD4XQPWJ1CE9ZVRW98X7HE6"
 | 
			
		||||
        - Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile:
 | 
			
		||||
 | 
			
		||||
    % id = "01HPJ8GHDET8ZGNN0AH3FWA8HX"
 | 
			
		||||
    - let's begin with a tilemap. say we have the following grid of tiles: (the examples are interactive, try editing it!)
 | 
			
		||||
    - so to help us learn, I made a little tile editor so that we can experiment with rendering tiles! have a look:
 | 
			
		||||
 | 
			
		||||
    <canvas
 | 
			
		||||
        is="tairu-editor"
 | 
			
		||||
        data-tilemap-id="bitwiseAutotiling"
 | 
			
		||||
        data-tile-size="40">
 | 
			
		||||
        Your browser does not support <canvas>.
 | 
			
		||||
    </canvas>
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    import { Tilemap } from "tairu/tilemap.js";
 | 
			
		||||
    import { TileEditor } from "tairu/editor.js";
 | 
			
		||||
 | 
			
		||||
    % id = "01HPJ8GHDEC0Z334M04MTNADV9"
 | 
			
		||||
    - for each tile we can assign a bitset of cardinal directions, based on which tiles it should connect to - like so:
 | 
			
		||||
    export const tilemapSquare = Tilemap.parse(" x", [
 | 
			
		||||
        "         ",
 | 
			
		||||
        "   xxx   ",
 | 
			
		||||
        "   xxx   ",
 | 
			
		||||
        "   xxx   ",
 | 
			
		||||
        "         ",
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    <canvas
 | 
			
		||||
        is="tairu-editor-cardinal-directions"
 | 
			
		||||
        data-tilemap-id="bitwiseAutotiling"
 | 
			
		||||
        data-tile-size="40">
 | 
			
		||||
        Your browser does not support <canvas>.
 | 
			
		||||
    </canvas>
 | 
			
		||||
    new TileEditor({
 | 
			
		||||
        tilemap: tilemapSquare,
 | 
			
		||||
        tileSize: 40,
 | 
			
		||||
    });
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    ```output tairu
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
        - `Tilemap` is a class wrapping a flat [`Uint8Array`] with a `width` and a `height`, so that we can index it using (x, y) coordinates.
 | 
			
		||||
 | 
			
		||||
        ```javascript tairu
 | 
			
		||||
        console.log(tilemapSquare.at(0, 0));
 | 
			
		||||
        console.log(tilemapSquare.at(3, 1));
 | 
			
		||||
        ```
 | 
			
		||||
        ```output tairu
 | 
			
		||||
        ```
 | 
			
		||||
 | 
			
		||||
        [`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
 | 
			
		||||
 | 
			
		||||
            - `at` has a `setAt` counterpart which sets tiles instead of getting them.
 | 
			
		||||
 | 
			
		||||
        - `TileEditor` provides a graphical editor for a `Tilemap` based on a `<canvas>`.
 | 
			
		||||
 | 
			
		||||
            - this editor is _Certified Battery Efficient™_, so it won't redraw unless it needs to!\
 | 
			
		||||
            we'll need to keep this in mind for later when we try to draw images, which may not be loaded during the initial draw.
 | 
			
		||||
 | 
			
		||||
    - to kick this off, let's set off a goal. I would like the tiles in our little renderer to connect together, like this:
 | 
			
		||||
 | 
			
		||||
    ![red rectangle with a black outline, made out of 3x3 tiles][pic:01HPYW5SNTY0Z0ENDE5K3XWMTH]
 | 
			
		||||
 | 
			
		||||
    - let's break this down into smaller steps. drawing a border around the rectangle will involve:
 | 
			
		||||
 | 
			
		||||
        - determining *on which tiles* to draw it,
 | 
			
		||||
 | 
			
		||||
        - determining *where in these tiles* to draw it,
 | 
			
		||||
 | 
			
		||||
        - and actually drawing it!
 | 
			
		||||
 | 
			
		||||
    - so let's zoom in a bit and look at the tiles one by one. in particular, let's focus on *these* two tiles:
 | 
			
		||||
 | 
			
		||||
    ![the same red rectangle, now with a focus on the northern tile at its center][pic:01HPYWPJB1P0GK53BSJFJFRAGR]
 | 
			
		||||
 | 
			
		||||
    - notice how the two highlighted tiles are *different.* therefore, we can infer we should probably connect together any tiles that are *the same*.
 | 
			
		||||
 | 
			
		||||
    - knowing that, we can extract the logic to a function:
 | 
			
		||||
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    export function shouldConnect(a, b) {
 | 
			
		||||
        return a == b;
 | 
			
		||||
    }
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    + now, also note that the border around this particular tile is only drawn on its *northern* edge -
 | 
			
		||||
    therefore we can infer that borders should only be drawn on edges for whom `shouldConnect(thisTile, adjacentTile)` is **`false`** (not `true`!).
 | 
			
		||||
    a tile generally has four edges - east, south, west, north - so we need to perform this check for all of them, and draw our border accordingly.
 | 
			
		||||
 | 
			
		||||
        - you might be wondering why I'm using this particular order for cardinal directions - why not [north, south, east, west]? or [north, east, south, west]?
 | 
			
		||||
 | 
			
		||||
            - the reason comes from math - `[cos(0) sin(0)]` is a vector pointing rightwards, not upwards!
 | 
			
		||||
            and I chose clockwise order, because that's how the vector rotates as we increase the angle, in a coordinate space where +Y points downward - such as the `<canvas>` coordinate space.
 | 
			
		||||
 | 
			
		||||
            - this choice yields some nice orderliness in the code that handles fetching tiles for connections - first you check `+X`, then `+Y`, then `-X`, and then `-Y` -
 | 
			
		||||
            which my pedantic mind really appreciates :ahyes:\
 | 
			
		||||
            as `X` is first alphabetically, so checking `Y` first would feel wrong.
 | 
			
		||||
 | 
			
		||||
    - to do that, I'm gonna override the tile editor's `drawTilemap` function - as this is where the actual tilemap rendering happens!
 | 
			
		||||
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    import { TileEditor } from "tairu/editor.js";
 | 
			
		||||
 | 
			
		||||
    export class TileEditorWithBorders extends TileEditor {
 | 
			
		||||
        constructor({ borderWidth, ...options }) {
 | 
			
		||||
            super(options);
 | 
			
		||||
 | 
			
		||||
            this.borderWidth = borderWidth;
 | 
			
		||||
            this.colorScheme.borderColor = "#000000";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        drawTilemap() {
 | 
			
		||||
            // Let the base class render out the infill, we'll just handle the borders.
 | 
			
		||||
            super.drawTilemap();
 | 
			
		||||
 | 
			
		||||
            this.ctx.fillStyle = this.colorScheme.borderColor;
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
                    // We only want to draw non-empty tiles, so skip tile 0.
 | 
			
		||||
                    if (tile == 0) {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Check which of this tile's neighbors should *not* connect to it.
 | 
			
		||||
                    let disjointWithEast = !shouldConnect(tile, this.tilemap.at(x + 1, y));
 | 
			
		||||
                    let disjointWithSouth = !shouldConnect(tile, this.tilemap.at(x, y + 1));
 | 
			
		||||
                    let disjointWithWest = !shouldConnect(tile, this.tilemap.at(x - 1, y));
 | 
			
		||||
                    let disjointWithNorth = !shouldConnect(tile, this.tilemap.at(x, y - 1));
 | 
			
		||||
 | 
			
		||||
                    let { borderWidth, tileSize } = this;
 | 
			
		||||
                    let tx = x * tileSize;
 | 
			
		||||
                    let ty = y * tileSize;
 | 
			
		||||
 | 
			
		||||
                    // For each disjoint neighbor, we want to draw a border between us and them.
 | 
			
		||||
                    if (disjointWithEast) {
 | 
			
		||||
                        this.ctx.fillRect(tx + tileSize - borderWidth, ty, borderWidth, tileSize);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (disjointWithSouth) {
 | 
			
		||||
                        this.ctx.fillRect(tx, ty + tileSize - borderWidth, tileSize, borderWidth);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (disjointWithWest) {
 | 
			
		||||
                        this.ctx.fillRect(tx, ty, borderWidth, tileSize);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (disjointWithNorth) {
 | 
			
		||||
                        this.ctx.fillRect(tx, ty, tileSize, borderWidth);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    and here's the result:
 | 
			
		||||
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    new TileEditorWithBorders({
 | 
			
		||||
        tilemap: tilemapSquare,
 | 
			
		||||
        tileSize: 40,
 | 
			
		||||
        borderWidth: 4,
 | 
			
		||||
    });
 | 
			
		||||
    ```
 | 
			
		||||
    ```output tairu
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    - this looks pretty perfect - maybe sans corners, which I'll conveniently skip for now - because most games don't actually render graphics in a vectorial way like this!
 | 
			
		||||
    instead, the more common way is to use a tileset - a big texture with a bunch of sprites to use for rendering each tile.
 | 
			
		||||
 | 
			
		||||
        - not only does this have the advantage of allowing for richer graphics, but it is also a lot easier to modify by artists, because you no longer need knowledge of graphics APIs to draw tiles.
 | 
			
		||||
 | 
			
		||||
    % 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 example, here's a tileset I drew for the 3rd iteration of my game [Planet Overgamma] - though tweaked a bit because I had never used it before writing this post :hueh:
 | 
			
		||||
 | 
			
		||||
    <ul class="tileset-demo">
 | 
			
		||||
    <li class="full-image">
 | 
			
		||||
    <img alt="a 16-tile tileset of 8x8 pixel metal" src="{% pic 01HPHVDRV0F0251MD0A2EG66C4 %}">
 | 
			
		||||
    </li>
 | 
			
		||||
    <li class="tileset-pieces">
 | 
			
		||||
    ![heavy metal sheet tileset from Planet Overgamma, made out of 16 tiles. it looks like heavy embossed sheets of metal, resembling steel in its heavyness][pic:01HPHVDRV0F0251MD0A2EG66C4]
 | 
			
		||||
 | 
			
		||||
    [Planet Overgamma]: https://github.com/liquidev/planet-overgamma
 | 
			
		||||
 | 
			
		||||
    % classes.branch = "tileset-cardinal-directions-demo"
 | 
			
		||||
    - we can split this tileset up into 16 individual tiles, each one 8 × 8 pixels; people choose various resolutions, I chose a fairly low one to hide my lack of artistic skill.
 | 
			
		||||
 | 
			
		||||
    <div class="horizontal-tile-strip">
 | 
			
		||||
    <span class="metal x-0 y-0"></span>
 | 
			
		||||
    <span class="metal x-1 y-0"></span>
 | 
			
		||||
    <span class="metal x-2 y-0"></span>
 | 
			
		||||
    <span class="metal x-3 y-0"></span>
 | 
			
		||||
    <span class="metal x-0 y-1"></span>
 | 
			
		||||
    <span class="metal x-1 y-1"></span>
 | 
			
		||||
    <span class="metal x-2 y-1"></span>
 | 
			
		||||
    <span class="metal x-3 y-1"></span>
 | 
			
		||||
    <span class="metal x-0 y-2"></span>
 | 
			
		||||
    <span class="metal x-1 y-2"></span>
 | 
			
		||||
    <span class="metal x-2 y-2"></span>
 | 
			
		||||
    <span class="metal x-3 y-2"></span>
 | 
			
		||||
    <span class="metal x-0 y-3"></span>
 | 
			
		||||
    <span class="metal x-1 y-3"></span>
 | 
			
		||||
    <span class="metal x-2 y-3"></span>
 | 
			
		||||
    <span class="metal x-3 y-3"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    % classes.branch = "tileset-cardinal-directions-demo"
 | 
			
		||||
    - the keen eyed among you have probably noticed that this is very similar to the case we had before with drawing procedural borders -
 | 
			
		||||
    except that instead of determining which borders to draw based on a tile's neighbors, this time we'll determine which *whole tile* to draw based on its neighbors!
 | 
			
		||||
 | 
			
		||||
    <div class="horizontal-tile-strip">
 | 
			
		||||
    <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>
 | 
			
		||||
| 
						 | 
				
			
			@ -76,21 +229,28 @@ styles = ["tairu.css"]
 | 
			
		|||
    <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>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
        % id = "01HPMVT9BM65YD5AXWPT4Z67H5"
 | 
			
		||||
        - (it's frustratingly hard to center individual letters like this in CSS. please forgive me for how crooked these are!)
 | 
			
		||||
    - previously we represented which single border to draw with a single boolean.
 | 
			
		||||
    now we will represent which single tile to draw with *four* booleans, because each tile can connect to four different directions.
 | 
			
		||||
 | 
			
		||||
    % 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!
 | 
			
		||||
        - four booleans like this can easily be packed into a single integer using some bitwise operations, hence we get ***bitwise autotiling*** - autotiling using bitwise operations!
 | 
			
		||||
 | 
			
		||||
    - now the clever part of bitwise autotiling is that we can use this packed integer *as an array index* - therefore selecting which tile to draw can be determined using just a single lookup table! neat, huh?
 | 
			
		||||
 | 
			
		||||
        - but because I'm lazy, and CPU time is valuable, instead of using an array I'll just rearrange the tileset texture a bit to be able to slice it in place using this index.
 | 
			
		||||
 | 
			
		||||
        - say we arrange our bits like this:
 | 
			
		||||
 | 
			
		||||
        ```javascript tairu
 | 
			
		||||
        export const E = 0b0001;
 | 
			
		||||
        export const S = 0b0010;
 | 
			
		||||
        export const W = 0b0100;
 | 
			
		||||
        export const N = 0b1000;
 | 
			
		||||
        ```
 | 
			
		||||
 | 
			
		||||
        % 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:
 | 
			
		||||
        - that means we'll need to arrange our tiles like so, where the leftmost tile is at index 0 (`0b0000`) and the rightmost tile is at index 15 (`0b1111`):
 | 
			
		||||
 | 
			
		||||
        <div class="horizontal-tile-strip">
 | 
			
		||||
        <span class="metal x-3 y-3"></span>
 | 
			
		||||
| 
						 | 
				
			
			@ -111,57 +271,129 @@ styles = ["tairu.css"]
 | 
			
		|||
        <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:
 | 
			
		||||
        - packing that into a single tileset, or rather this time, a *tile strip*, we get this image:
 | 
			
		||||
 | 
			
		||||
        ![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.)
 | 
			
		||||
    - now it's time to actually implement it as code! I'll start by defining a *tile index* function as a general way of looking up tiles in a tileset.
 | 
			
		||||
 | 
			
		||||
    % id = "01HPMVT9BMMEM4HT4ANZ40992P"
 | 
			
		||||
    - in JavaScript, drawing on a `<canvas>` using bitwise autotiling would look like this:
 | 
			
		||||
    ```javascript
 | 
			
		||||
    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);
 | 
			
		||||
        - I want to make the tile renderer a bit more general, so being able to attach a different tile lookup function to each tileset sounds like a great feature.
 | 
			
		||||
 | 
			
		||||
            // 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];
 | 
			
		||||
            - just imagine some game where glass connects to metal, but metal doesn't connect to glass - I bet that would look pretty great!
 | 
			
		||||
 | 
			
		||||
                // 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;
 | 
			
		||||
    - …but anyways, here's the basic bitwise magic function:
 | 
			
		||||
 | 
			
		||||
                // 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,
 | 
			
		||||
                );
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    export function tileIndexInBitwiseTileset(tilemap, x, y) {
 | 
			
		||||
        let tile = tilemap.at(x, y);
 | 
			
		||||
 | 
			
		||||
        let tileIndex = 0;
 | 
			
		||||
        tileIndex |= shouldConnect(tile, tilemap.at(x + 1, y)) ? E : 0;
 | 
			
		||||
        tileIndex |= shouldConnect(tile, tilemap.at(x, y + 1)) ? S : 0;
 | 
			
		||||
        tileIndex |= shouldConnect(tile, tilemap.at(x - 1, y)) ? W : 0;
 | 
			
		||||
        tileIndex |= shouldConnect(tile, tilemap.at(x, y - 1)) ? N : 0;
 | 
			
		||||
 | 
			
		||||
        return tileIndex;
 | 
			
		||||
    }
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    % template = true
 | 
			
		||||
    - we'll define our tilesets by their texture, tile size, and a tile indexing function. so let's create an object that will hold our tileset data:
 | 
			
		||||
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    // You'll probably want to host the assets on your own website rather than
 | 
			
		||||
    // hotlinking to others. It helps longevity!
 | 
			
		||||
    const tilesetImage = new Image();
 | 
			
		||||
    tilesetImage.src = "{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}";
 | 
			
		||||
 | 
			
		||||
    export const heavyMetalTileset = {
 | 
			
		||||
        image: tilesetImage,
 | 
			
		||||
        tileSize: 8,
 | 
			
		||||
        tileIndex: tileIndexInBitwiseTileset,
 | 
			
		||||
    };
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    - with all that, we should now be able to write a tile renderer which can handle textures! so let's try it:
 | 
			
		||||
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    import { TileEditor } from "tairu/editor.js";
 | 
			
		||||
 | 
			
		||||
    export class TilesetTileEditor extends TileEditor {
 | 
			
		||||
        constructor({ tilesets, ...options }) {
 | 
			
		||||
            super(options);
 | 
			
		||||
            this.tilesets = tilesets;
 | 
			
		||||
 | 
			
		||||
            // The image may not be loaded once the editor is first drawn, so we need to request a
 | 
			
		||||
            // redraw for each image that gets loaded in.
 | 
			
		||||
            for (let tileset of this.tilesets) {
 | 
			
		||||
                tileset.image.addEventListener("load", () => this.draw());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        drawTilemap() {
 | 
			
		||||
            // We're dealing with pixel tiles so we want our images to be pixelated,
 | 
			
		||||
            // not interpolated.
 | 
			
		||||
            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) {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Subtract one from the tile because tile 0 is always empty.
 | 
			
		||||
                    // Having to specify a null entry at array index 0 would be pretty annoying.
 | 
			
		||||
                    let tileset = this.tilesets[tile - 1];
 | 
			
		||||
                    if (tileset != null) {
 | 
			
		||||
                        let { tileSize } = this;
 | 
			
		||||
                        let tileIndex = tileset.tileIndex(this.tilemap, x, y);
 | 
			
		||||
                        this.ctx.drawImage(
 | 
			
		||||
                            tileset.image,
 | 
			
		||||
                            tileIndex * tileset.tileSize, 0, tileset.tileSize, tileset.tileSize,
 | 
			
		||||
                            x * tileSize, y * tileSize, tileSize, tileSize,
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    TODO this should be literate code
 | 
			
		||||
    - drum roll please...
 | 
			
		||||
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    new TilesetTileEditor({
 | 
			
		||||
        tilemap: tilemapSquare,
 | 
			
		||||
        tileSize: 40,
 | 
			
		||||
        tilesets: [heavyMetalTileset],
 | 
			
		||||
    });
 | 
			
		||||
    ```
 | 
			
		||||
    ```output tairu
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    - it works! buuuut if you play around with it you'll quickly start noticing some problems:
 | 
			
		||||
 | 
			
		||||
    ```javascript tairu
 | 
			
		||||
    import { Tilemap } from "tairu/tilemap.js";
 | 
			
		||||
 | 
			
		||||
    export const tilemapEdgeCase = Tilemap.parse(" x", [
 | 
			
		||||
        "         ",
 | 
			
		||||
        "   xxx   ",
 | 
			
		||||
        "   x x   ",
 | 
			
		||||
        "   xxx   ",
 | 
			
		||||
        "         ",
 | 
			
		||||
    ]);
 | 
			
		||||
    new TilesetTileEditor({
 | 
			
		||||
        tilemap: tilemapEdgeCase,
 | 
			
		||||
        tileSize: 40,
 | 
			
		||||
        tilesets: [heavyMetalTileset],
 | 
			
		||||
    });
 | 
			
		||||
    ```
 | 
			
		||||
    ```output tairu
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
    - where did our nice seamless connections go!?
 | 
			
		||||
 | 
			
		||||
    % template = true
 | 
			
		||||
      id = "01HPMVT9BM9CS9375MX4H9WKW8"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								jsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								jsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
{
 | 
			
		||||
    "compilerOptions": {
 | 
			
		||||
        "baseUrl": "./static/js",
 | 
			
		||||
        "paths": {
 | 
			
		||||
            "treehouse/*": [
 | 
			
		||||
                "./*"
 | 
			
		||||
            ],
 | 
			
		||||
            "tairu/*": [
 | 
			
		||||
                "./components/tairu/*"
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -255,14 +255,12 @@ th-literate-program {
 | 
			
		|||
    .tree summary:hover {
 | 
			
		||||
 | 
			
		||||
        & pre,
 | 
			
		||||
        & th-literate-program {
 | 
			
		||||
        & th-literate-program:not([data-mode="output"]) {
 | 
			
		||||
            background-color: var(--shaded-against-background-twice);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pre>code,
 | 
			
		||||
th-literate-program>code {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -276,7 +274,8 @@ th-literate-program {
 | 
			
		|||
 | 
			
		||||
/* And don't let code examples fly off and overflow the window */
 | 
			
		||||
 | 
			
		||||
pre {
 | 
			
		||||
pre,
 | 
			
		||||
th-literate-program {
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    width: auto;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
| 
						 | 
				
			
			@ -554,30 +553,6 @@ th-literate-program[data-mode="input"] {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
th-literate-program[data-mode="output"] {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    & code {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & code.error {
 | 
			
		||||
        color: var(--error-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::after {
 | 
			
		||||
        content: 'Output';
 | 
			
		||||
 | 
			
		||||
        padding: 8px;
 | 
			
		||||
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        top: 0;
 | 
			
		||||
 | 
			
		||||
        opacity: 50%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
th-literate-program[data-mode="graphics"] {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
| 
						 | 
				
			
			@ -593,6 +568,10 @@ th-literate-program[data-mode="graphics"] {
 | 
			
		|||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & pre>code {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & pre.error {
 | 
			
		||||
        color: var(--error-color);
 | 
			
		||||
        position: relative;
 | 
			
		||||
| 
						 | 
				
			
			@ -614,6 +593,27 @@ th-literate-program[data-mode="graphics"] {
 | 
			
		|||
            opacity: 50%;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & pre.console {
 | 
			
		||||
        position: relative;
 | 
			
		||||
 | 
			
		||||
        &:empty {
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &::after {
 | 
			
		||||
            content: 'Console';
 | 
			
		||||
 | 
			
		||||
            padding: 8px;
 | 
			
		||||
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            right: 0;
 | 
			
		||||
            top: 0;
 | 
			
		||||
 | 
			
		||||
            color: var(--text-color);
 | 
			
		||||
            opacity: 50%;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Syntax highlighting */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,28 +1,4 @@
 | 
			
		|||
.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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -218,8 +218,8 @@ th-bc {
 | 
			
		|||
    flex-grow: 1;
 | 
			
		||||
 | 
			
		||||
    /* Bit of a hack to make <pre>s in <th-bc> have scrollbars proper. */
 | 
			
		||||
    &:has(pre) {
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
    &:has(pre, th-literate-program) {
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								static/emoji/ahyes.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/emoji/ahyes.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 23 KiB  | 
| 
						 | 
				
			
			@ -12,10 +12,8 @@ function getLiterateProgram(name) {
 | 
			
		|||
            outputCount: 0,
 | 
			
		||||
 | 
			
		||||
            nextOutputIndex() {
 | 
			
		||||
                let index = this.outputCount;
 | 
			
		||||
                ++this.outputCount;
 | 
			
		||||
                return index;
 | 
			
		||||
            }
 | 
			
		||||
                return this.outputCount++;
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    return literatePrograms.get(name);
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +26,7 @@ function getLiterateProgramWorkerCommands(name) {
 | 
			
		|||
        if (frame.mode == "input") {
 | 
			
		||||
            commands.push({ kind: "module", source: frame.textContent });
 | 
			
		||||
        } else if (frame.mode == "output") {
 | 
			
		||||
            commands.push({ kind: "output", expected: frame.textContent });
 | 
			
		||||
            commands.push({ kind: "output" });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return commands;
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +49,7 @@ class InputMode {
 | 
			
		|||
            { regex: /"(\\"|[^"])*"/, as: "string" },
 | 
			
		||||
            { regex: /`(\\`|[^"])*`/, as: "string" },
 | 
			
		||||
            // TODO: RegExp literals?
 | 
			
		||||
            { regex: /[+=/*^%<>!~|&\.-]+/, as: "operator" },
 | 
			
		||||
            { regex: /[+=/*^%<>!~|&\.?:-]+/, as: "operator" },
 | 
			
		||||
            { regex: /[,;]/, as: "punct" },
 | 
			
		||||
        ],
 | 
			
		||||
        keywords: new Map([
 | 
			
		||||
| 
						 | 
				
			
			@ -132,54 +130,84 @@ class InputMode {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function messageOutputArrayToString(output) {
 | 
			
		||||
    return output
 | 
			
		||||
        .map(x => {
 | 
			
		||||
            if (typeof x === "object") return JSON.stringify(x);
 | 
			
		||||
            else return x + "";
 | 
			
		||||
        })
 | 
			
		||||
        .join(" ");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class OutputMode {
 | 
			
		||||
    constructor(frame) {
 | 
			
		||||
        this.clearResultsOnNextOutput = false;
 | 
			
		||||
 | 
			
		||||
        this.frame = frame;
 | 
			
		||||
 | 
			
		||||
        this.frame.program.onChanged.push(_ => this.evaluate());
 | 
			
		||||
        this.outputIndex = this.frame.program.nextOutputIndex();
 | 
			
		||||
 | 
			
		||||
        this.evaluate();
 | 
			
		||||
    }
 | 
			
		||||
        this.console = document.createElement("pre");
 | 
			
		||||
        this.console.classList.add("console");
 | 
			
		||||
        this.frame.appendChild(this.console);
 | 
			
		||||
        this.clearConsoleOnNextOutput = false;
 | 
			
		||||
 | 
			
		||||
    evaluate() {
 | 
			
		||||
        // This is a small bit of debouncing. If we cleared the output right away, the page would
 | 
			
		||||
        // jitter around irritatingly.
 | 
			
		||||
        this.clearResultsOnNextOutput = true;
 | 
			
		||||
        this.error = document.createElement("pre");
 | 
			
		||||
        this.error.classList.add("error");
 | 
			
		||||
        this.frame.appendChild(this.error);
 | 
			
		||||
 | 
			
		||||
        if (this.worker != null) {
 | 
			
		||||
            this.worker.terminate();
 | 
			
		||||
        }
 | 
			
		||||
        this.worker = new Worker(import.meta.resolve("./literate-programming/worker.js"), {
 | 
			
		||||
            type: "module",
 | 
			
		||||
            name: `evaluate LiterateOutput ${this.frame.programName}`
 | 
			
		||||
        });
 | 
			
		||||
        this.iframe = document.createElement("iframe");
 | 
			
		||||
        this.iframe.classList.add("hidden");
 | 
			
		||||
        this.iframe.src = `${TREEHOUSE_SITE}/sandbox`;
 | 
			
		||||
        this.frame.appendChild(this.iframe);
 | 
			
		||||
 | 
			
		||||
        this.worker.addEventListener("message", event => {
 | 
			
		||||
        this.iframe.contentWindow.treehouseSandboxInternals = { outputIndex: this.outputIndex };
 | 
			
		||||
 | 
			
		||||
        this.iframe.contentWindow.addEventListener("message", event => {
 | 
			
		||||
            let message = event.data;
 | 
			
		||||
            if (message.kind == "evalComplete") {
 | 
			
		||||
                this.worker.terminate();
 | 
			
		||||
            if (message.kind == "ready") {
 | 
			
		||||
                this.evaluate();
 | 
			
		||||
            } else if (message.kind == "resize" && message.outputIndex == this.outputIndex) {
 | 
			
		||||
                this.resize();
 | 
			
		||||
            } else if (message.kind == "output" && message.outputIndex == this.outputIndex) {
 | 
			
		||||
                this.addOutput(message.output);
 | 
			
		||||
                if (message.output.kind == "error") {
 | 
			
		||||
                    this.error.textContent = messageOutputArrayToString(message.output.message);
 | 
			
		||||
                    this.iframe.classList.add("hidden");
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.addOutput(message.output);
 | 
			
		||||
                }
 | 
			
		||||
            } else if (message.kind == "evalComplete") {
 | 
			
		||||
                this.error.textContent = "";
 | 
			
		||||
                this.flushConsoleClear();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.worker.postMessage({
 | 
			
		||||
        this.frame.program.onChanged.push(_ => this.evaluate());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    evaluate() {
 | 
			
		||||
        this.requestConsoleClear();
 | 
			
		||||
        this.iframe.contentWindow.postMessage({
 | 
			
		||||
            action: "eval",
 | 
			
		||||
            input: getLiterateProgramWorkerCommands(this.frame.programName),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addOutput(output) {
 | 
			
		||||
        if (this.clearResultsOnNextOutput) {
 | 
			
		||||
            this.clearResultsOnNextOutput = false;
 | 
			
		||||
            this.clearResults();
 | 
			
		||||
        }
 | 
			
		||||
    clearConsole() {
 | 
			
		||||
        this.console.replaceChildren();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        // Don't show anything if the function didn't return a value.
 | 
			
		||||
        if (output.kind == "result" && output.message[0] === undefined) return;
 | 
			
		||||
    requestConsoleClear() {
 | 
			
		||||
        this.clearConsoleOnNextOutput = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    flushConsoleClear() {
 | 
			
		||||
        if (this.clearConsoleOnNextOutput) {
 | 
			
		||||
            this.clearConsole();
 | 
			
		||||
            this.clearConsoleOnNextOutput = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addOutput(output) {
 | 
			
		||||
        this.flushConsoleClear();
 | 
			
		||||
 | 
			
		||||
        let line = document.createElement("code");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -194,65 +222,22 @@ class OutputMode {
 | 
			
		|||
            })
 | 
			
		||||
            .join(" ");
 | 
			
		||||
 | 
			
		||||
        this.frame.appendChild(line);
 | 
			
		||||
        this.console.appendChild(line);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clearResults() {
 | 
			
		||||
        this.frame.replaceChildren();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static messageOutputArrayToString(output) {
 | 
			
		||||
        return output
 | 
			
		||||
            .map(x => {
 | 
			
		||||
                if (typeof x === "object") return JSON.stringify(x);
 | 
			
		||||
                else return x + "";
 | 
			
		||||
            })
 | 
			
		||||
            .join(" ");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class GraphicsMode {
 | 
			
		||||
    constructor(frame) {
 | 
			
		||||
        this.frame = frame;
 | 
			
		||||
 | 
			
		||||
        this.error = document.createElement("pre");
 | 
			
		||||
        this.error.classList.add("error");
 | 
			
		||||
        this.frame.appendChild(this.error);
 | 
			
		||||
 | 
			
		||||
        this.iframe = document.createElement("iframe");
 | 
			
		||||
        this.iframe.classList.add("hidden");
 | 
			
		||||
        this.iframe.src = import.meta.resolve("../../html/sandbox.html");
 | 
			
		||||
        this.frame.appendChild(this.iframe);
 | 
			
		||||
 | 
			
		||||
        this.iframe.contentWindow.addEventListener("message", event => {
 | 
			
		||||
            let message = event.data;
 | 
			
		||||
            if (message.kind == "ready") {
 | 
			
		||||
                this.evaluate();
 | 
			
		||||
            }
 | 
			
		||||
            else if (message.kind == "resize") {
 | 
			
		||||
                this.resize(message);
 | 
			
		||||
            } else if (message.kind == "output" && message.output.kind == "error") {
 | 
			
		||||
                this.error.textContent = OutputMode.messageOutputArrayToString(message.output.message);
 | 
			
		||||
                this.iframe.classList.add("hidden");
 | 
			
		||||
            } else if (message.kind == "evalComplete") {
 | 
			
		||||
                this.error.textContent = "";
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.frame.program.onChanged.push(_ => this.evaluate());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    evaluate() {
 | 
			
		||||
        this.iframe.contentWindow.postMessage({
 | 
			
		||||
            action: "eval",
 | 
			
		||||
            input: getLiterateProgramWorkerCommands(this.frame.programName),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resize(message) {
 | 
			
		||||
        this.iframe.width = message.width;
 | 
			
		||||
        this.iframe.height = message.height;
 | 
			
		||||
    resize() {
 | 
			
		||||
        // iframe cannot be `display: none` to get its scrollWidth/scrollHeight.
 | 
			
		||||
        this.iframe.classList.remove("hidden");
 | 
			
		||||
 | 
			
		||||
        let width = this.iframe.contentDocument.body.scrollWidth;
 | 
			
		||||
        let height = this.iframe.contentDocument.body.scrollHeight;
 | 
			
		||||
 | 
			
		||||
        if (width == 0 || height == 0) {
 | 
			
		||||
            this.iframe.classList.add("hidden");
 | 
			
		||||
        } else {
 | 
			
		||||
            this.iframe.width = width;
 | 
			
		||||
            this.iframe.height = height;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -266,8 +251,6 @@ class LiterateProgram extends HTMLElement {
 | 
			
		|||
            this.modeImpl = new InputMode(this);
 | 
			
		||||
        } else if (this.mode == "output") {
 | 
			
		||||
            this.modeImpl = new OutputMode(this);
 | 
			
		||||
        } else if (this.mode == "graphics") {
 | 
			
		||||
            this.modeImpl = new GraphicsMode(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,21 @@
 | 
			
		|||
let outputIndex = 0;
 | 
			
		||||
 | 
			
		||||
export function getOutputIndex() {
 | 
			
		||||
    return outputIndex;
 | 
			
		||||
}
 | 
			
		||||
export const jsConsole = console;
 | 
			
		||||
 | 
			
		||||
// Overwrite globalThis.console with domConsole to redirect output to the DOM console.
 | 
			
		||||
// To always output to the JavaScript console regardless, use jsConsole.
 | 
			
		||||
export const domConsole = {
 | 
			
		||||
    log(...message) {
 | 
			
		||||
        postMessage({
 | 
			
		||||
            kind: "output",
 | 
			
		||||
            output: {
 | 
			
		||||
                kind: "console.log",
 | 
			
		||||
                message: [...message],
 | 
			
		||||
            },
 | 
			
		||||
            outputIndex,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function withTemporaryGlobalScope(callback) {
 | 
			
		||||
    let state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +26,7 @@ async function withTemporaryGlobalScope(callback) {
 | 
			
		|||
        }
 | 
			
		||||
    };
 | 
			
		||||
    await callback(state);
 | 
			
		||||
    jsConsole.trace(state.oldValues, "bringing back old state");
 | 
			
		||||
    for (let key in state.oldValues) {
 | 
			
		||||
        globalThis[key] = state.oldValues[key];
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -20,15 +34,11 @@ async function withTemporaryGlobalScope(callback) {
 | 
			
		|||
 | 
			
		||||
let evaluationComplete = null;
 | 
			
		||||
 | 
			
		||||
export async function evaluate(commands, { start, success, error }) {
 | 
			
		||||
export async function evaluate(commands, { error, newOutput }) {
 | 
			
		||||
    if (evaluationComplete != null) {
 | 
			
		||||
        await evaluationComplete;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (start != null) {
 | 
			
		||||
        start();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let signalEvaluationComplete;
 | 
			
		||||
    evaluationComplete = new Promise((resolve, _reject) => {
 | 
			
		||||
        signalEvaluationComplete = resolve;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,21 +46,19 @@ export async function evaluate(commands, { start, success, error }) {
 | 
			
		|||
 | 
			
		||||
    outputIndex = 0;
 | 
			
		||||
    try {
 | 
			
		||||
        await withTemporaryGlobalScope(async scope => {
 | 
			
		||||
            for (let command of commands) {
 | 
			
		||||
                if (command.kind == "module") {
 | 
			
		||||
                    let blobUrl = URL.createObjectURL(new Blob([command.source], { type: "text/javascript" }));
 | 
			
		||||
                    let module = await import(blobUrl);
 | 
			
		||||
                    for (let exportedKey in module) {
 | 
			
		||||
                        scope.set(exportedKey, module[exportedKey]);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (command.kind == "output") {
 | 
			
		||||
                    ++outputIndex;
 | 
			
		||||
        for (let command of commands) {
 | 
			
		||||
            if (command.kind == "module") {
 | 
			
		||||
                let blobUrl = URL.createObjectURL(new Blob([command.source], { type: "text/javascript" }));
 | 
			
		||||
                let module = await import(blobUrl);
 | 
			
		||||
                for (let exportedKey in module) {
 | 
			
		||||
                    globalThis[exportedKey] = module[exportedKey];
 | 
			
		||||
                }
 | 
			
		||||
            } else if (command.kind == "output") {
 | 
			
		||||
                if (newOutput != null) {
 | 
			
		||||
                    newOutput(outputIndex);
 | 
			
		||||
                }
 | 
			
		||||
                ++outputIndex;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        if (success != null) {
 | 
			
		||||
            success();
 | 
			
		||||
        }
 | 
			
		||||
        postMessage({
 | 
			
		||||
            kind: "evalComplete",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +0,0 @@
 | 
			
		|||
import { evaluate, getOutputIndex } from "./eval.js";
 | 
			
		||||
 | 
			
		||||
let debugLog = console.log;
 | 
			
		||||
 | 
			
		||||
globalThis.console = {
 | 
			
		||||
    log(...message) {
 | 
			
		||||
        postMessage({
 | 
			
		||||
            kind: "output",
 | 
			
		||||
            output: {
 | 
			
		||||
                kind: "log",
 | 
			
		||||
                message: [...message],
 | 
			
		||||
            },
 | 
			
		||||
            outputIndex: getOutputIndex(),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
addEventListener("message", async event => {
 | 
			
		||||
    let message = event.data;
 | 
			
		||||
    if (message.action == "eval") {
 | 
			
		||||
        evaluate(message.input, {});
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										0
									
								
								static/js/components/tairu/cardinal-directions.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								static/js/components/tairu/cardinal-directions.js
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										134
									
								
								static/js/components/tairu/editor.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								static/js/components/tairu/editor.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,134 @@
 | 
			
		|||
import { Sketch } from "treehouse/sandbox.js";
 | 
			
		||||
 | 
			
		||||
export class TileEditor extends Sketch {
 | 
			
		||||
    constructor({ tilemap, tileSize }) {
 | 
			
		||||
        super(tilemap.width * tileSize, tilemap.height * tileSize);
 | 
			
		||||
 | 
			
		||||
        this.colorScheme = {
 | 
			
		||||
            background: "#F7F7F7",
 | 
			
		||||
            grid: "#00000011",
 | 
			
		||||
            tileCursor: "#222222",
 | 
			
		||||
            tiles: [
 | 
			
		||||
                "transparent", // never actually drawn to the screen with the default renderer!
 | 
			
		||||
                "#eb134a",
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.tilemap = tilemap;
 | 
			
		||||
        this.tileSize = tileSize;
 | 
			
		||||
 | 
			
		||||
        this.hasFocus = false;
 | 
			
		||||
        this.paintingTile = null;
 | 
			
		||||
        this.tileCursor = { x: 0, y: 0 };
 | 
			
		||||
 | 
			
		||||
        this.canvas.addEventListener("mousemove", event => this.mouseMoved(event));
 | 
			
		||||
        this.canvas.addEventListener("mousedown", event => this.mousePressed(event));
 | 
			
		||||
        this.canvas.addEventListener("mouseup", event => this.mouseReleased(event));
 | 
			
		||||
 | 
			
		||||
        this.canvas.addEventListener("mouseenter", _ => this.mouseEnter());
 | 
			
		||||
        this.canvas.addEventListener("mouseleave", _ => this.mouseLeave());
 | 
			
		||||
 | 
			
		||||
        this.canvas.addEventListener("contextmenu", event => event.preventDefault());
 | 
			
		||||
 | 
			
		||||
        // Only draw first frame after the constructor already runs.
 | 
			
		||||
        // That way we can modify the color scheme however much we want without causing additional
 | 
			
		||||
        // redraws.
 | 
			
		||||
        requestAnimationFrame(() => this.draw());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    draw() {
 | 
			
		||||
        this.drawBackground();
 | 
			
		||||
        this.drawTilemap();
 | 
			
		||||
        this.drawGrid();
 | 
			
		||||
        if (this.hasFocus) {
 | 
			
		||||
            this.drawTileCursor();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    drawBackground() {
 | 
			
		||||
        this.ctx.fillStyle = this.colorScheme.background;
 | 
			
		||||
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    drawTilemap() {
 | 
			
		||||
        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);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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.canvas.height);
 | 
			
		||||
        }
 | 
			
		||||
        for (let y = 0; y < this.tilemap.width; ++y) {
 | 
			
		||||
            this.ctx.moveTo(0, y * this.tileSize);
 | 
			
		||||
            this.ctx.lineTo(this.canvas.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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    mouseMoved(event) {
 | 
			
		||||
        this.tileCursor.x = Math.floor(event.offsetX / this.tileSize);
 | 
			
		||||
        this.tileCursor.y = Math.floor(event.offsetY / this.tileSize);
 | 
			
		||||
        this.paintTileUnderCursor();
 | 
			
		||||
 | 
			
		||||
        this.draw();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mousePressed(event) {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
 | 
			
		||||
        if (event.button == 0) {
 | 
			
		||||
            this.paintingTile = 1;
 | 
			
		||||
        } else if (event.button == 2) {
 | 
			
		||||
            this.paintingTile = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.paintTileUnderCursor();
 | 
			
		||||
 | 
			
		||||
        this.draw();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mouseReleased(_event) {
 | 
			
		||||
        this.stopPainting();
 | 
			
		||||
        this.draw();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mouseEnter() {
 | 
			
		||||
        this.hasFocus = true;
 | 
			
		||||
        this.draw();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mouseLeave() {
 | 
			
		||||
        this.hasFocus = false;
 | 
			
		||||
        this.stopPainting();
 | 
			
		||||
        this.draw();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    paintTileUnderCursor() {
 | 
			
		||||
        if (this.paintingTile != null) {
 | 
			
		||||
            this.tilemap.setAt(this.tileCursor.x, this.tileCursor.y, this.paintingTile);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stopPainting() {
 | 
			
		||||
        this.paintingTile = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,45 @@
 | 
			
		|||
import { defineFrame, Frame } from './framework.js';
 | 
			
		||||
import { TileEditor, canConnect, shouldConnect } from './tairu.js';
 | 
			
		||||
import { TileEditor } from 'tairu/editor.js';
 | 
			
		||||
 | 
			
		||||
class CardinalDirectionsEditor extends TileEditor {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
export function alignTextInRectangle(ctx, text, x, y, width, height, hAlign, vAlign) {
 | 
			
		||||
    let measurements = 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 };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function shouldConnect(a, b) {
 | 
			
		||||
    return a == b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class TileEditorWithCardinalDirections extends TileEditor {
 | 
			
		||||
    constructor(options) {
 | 
			
		||||
        super(options);
 | 
			
		||||
        this.colorScheme.tiles[1] = "#f96565";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -38,4 +74,3 @@ class CardinalDirectionsEditor extends TileEditor {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
defineFrame("tairu-editor-cardinal-directions", CardinalDirectionsEditor);
 | 
			
		||||
| 
						 | 
				
			
			@ -27,4 +27,16 @@ export class Tilemap {
 | 
			
		|||
            this.tiles[this.tileIndex(x, y)] = tile;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static parse(alphabet, 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,9 @@
 | 
			
		|||
export const internals = {
 | 
			
		||||
    body: document.createElement("body"),
 | 
			
		||||
 | 
			
		||||
    resetBody() {
 | 
			
		||||
        this.body.replaceChildren();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function body() {
 | 
			
		||||
| 
						 | 
				
			
			@ -19,4 +23,13 @@ export class Sketch {
 | 
			
		|||
 | 
			
		||||
        addElement(this.canvas);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    animate(draw) {
 | 
			
		||||
        let animationCallback;
 | 
			
		||||
        animationCallback = () => {
 | 
			
		||||
            draw();
 | 
			
		||||
            requestAnimationFrame(animationCallback);
 | 
			
		||||
        };
 | 
			
		||||
        animationCallback();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,251 +0,0 @@
 | 
			
		|||
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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const dirs47 = {
 | 
			
		||||
    E: 0b0000_0001,
 | 
			
		||||
    SE: 0b0000_0010,
 | 
			
		||||
    S: 0b0000_0100,
 | 
			
		||||
    SW: 0b0000_1000,
 | 
			
		||||
    W: 0b0001_0000,
 | 
			
		||||
    NW: 0b0010_0000,
 | 
			
		||||
    N: 0b0100_0000,
 | 
			
		||||
    NE: 0b1000_0000,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function isSet(integer, bit) {
 | 
			
		||||
    return (integer & bit) == bit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeRedundancies(t) {
 | 
			
		||||
    if (isSet(t, dirs47.SE) && (!isSet(t, dirs47.S) || !isSet(t, dirs47.E))) {
 | 
			
		||||
        t &= ~dirs47.SE;
 | 
			
		||||
    }
 | 
			
		||||
    if (isSet(t, dirs47.SW) && (!isSet(t, dirs47.S) || !isSet(t, dirs47.W))) {
 | 
			
		||||
        t &= ~dirs47.SW;
 | 
			
		||||
    }
 | 
			
		||||
    if (isSet(t, dirs47.NW) && (!isSet(t, dirs47.N) || !isSet(t, dirs47.W))) {
 | 
			
		||||
        t &= ~dirs47.NW;
 | 
			
		||||
    }
 | 
			
		||||
    if (isSet(t, dirs47.NE) && (!isSet(t, dirs47.N) || !isSet(t, dirs47.E))) {
 | 
			
		||||
        t &= ~dirs47.NE;
 | 
			
		||||
    }
 | 
			
		||||
    return t;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ordinalDirections() {
 | 
			
		||||
    let unique = new Set();
 | 
			
		||||
    for (let i = 0; i <= 0b1111_1111; ++i) {
 | 
			
		||||
        unique.add(removeRedundancies(i));
 | 
			
		||||
    }
 | 
			
		||||
    return Array.from(unique).sort((a, b) => a - b);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let xToConnectionBitSet = ordinalDirections();
 | 
			
		||||
let connectionBitSetToX = new Uint8Array(256);
 | 
			
		||||
for (let i = 0; i < xToConnectionBitSet.length; ++i) {
 | 
			
		||||
    connectionBitSetToX[xToConnectionBitSet[i]] = i;
 | 
			
		||||
}
 | 
			
		||||
console.log(connectionBitSetToX);
 | 
			
		||||
 | 
			
		||||
export class TileEditor extends Frame {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        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");
 | 
			
		||||
        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];
 | 
			
		||||
        this.tilesets47 = [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;
 | 
			
		||||
            } else if (image.hasAttribute("data-tairu-tileset-47")) {
 | 
			
		||||
                let tilesetIndex = parseInt(image.getAttribute("data-tairu-tileset-47"));
 | 
			
		||||
                this.tilesets47[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 || this.tilesets47.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 tileset16 = this.tilesets[tile];
 | 
			
		||||
                    let tileset47 = this.tilesets47[tile];
 | 
			
		||||
                    let tileset = tileset47 != null ? tileset47 : tileset16;
 | 
			
		||||
 | 
			
		||||
                    let tileIndex = 0;
 | 
			
		||||
                    if (tileset47 != null) {
 | 
			
		||||
                        let rawTileIndex = 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x + 1, y)) ? dirs47.E : 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x + 1, y + 1)) ? dirs47.SE : 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x, y + 1)) ? dirs47.S : 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x - 1, y + 1)) ? dirs47.SW : 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x - 1, y)) ? dirs47.W : 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x - 1, y - 1)) ? dirs47.NW : 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x, y - 1)) ? dirs47.N : 0;
 | 
			
		||||
                        rawTileIndex |= shouldConnect(tile, this.tilemap.at(x + 1, y - 1)) ? dirs47.NE : 0;
 | 
			
		||||
                        tileIndex = connectionBitSetToX[removeRedundancies(rawTileIndex)];
 | 
			
		||||
                    } else {
 | 
			
		||||
                        tileIndex |= shouldConnect(tile, this.tilemap.at(x + 1, y)) ? 0b0001 : 0;
 | 
			
		||||
                        tileIndex |= shouldConnect(tile, this.tilemap.at(x, y + 1)) ? 0b0010 : 0;
 | 
			
		||||
                        tileIndex |= shouldConnect(tile, this.tilemap.at(x - 1, y)) ? 0b0100 : 0;
 | 
			
		||||
                        tileIndex |= shouldConnect(tile, this.tilemap.at(x, y - 1)) ? 0b1000 : 0;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    let tilesetTileSize = tileset.height;
 | 
			
		||||
                    let tilesetX = tileIndex * tilesetTileSize;
 | 
			
		||||
                    let tilesetY = 0;
 | 
			
		||||
                    this.ctx.drawImage(
 | 
			
		||||
                        tileset,
 | 
			
		||||
                        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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
defineFrame("tairu-editor", TileEditor);
 | 
			
		||||
							
								
								
									
										7
									
								
								static/js/vendor/codejar.js
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								static/js/vendor/codejar.js
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -23,9 +23,10 @@ export function CodeJar(editor, highlight, opt = {}) {
 | 
			
		|||
    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';
 | 
			
		||||
    // PATCH(liquidex): I think I know better how I want to handle overflow.
 | 
			
		||||
    // editor.style.overflowWrap = 'break-word';
 | 
			
		||||
    // editor.style.overflowY = 'auto';
 | 
			
		||||
    editor.style.whiteSpace = 'pre';
 | 
			
		||||
    const doHighlight = (editor, pos) => {
 | 
			
		||||
        highlight(editor, pos);
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								static/pic/01HPYW5SNTY0Z0ENDE5K3XWMTH-goal.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/pic/01HPYW5SNTY0Z0ENDE5K3XWMTH-goal.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								static/pic/01HPYWPJB1P0GK53BSJFJFRAGR-goal2.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/pic/01HPYWPJB1P0GK53BSJFJFRAGR-goal2.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
						 | 
				
			
			@ -9,6 +9,8 @@
 | 
			
		|||
        body {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            width: fit-content;
 | 
			
		||||
            height: fit-content;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        canvas {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,43 +18,37 @@
 | 
			
		|||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <script type="importmap">{ "imports": { "treehouse/": "{{ config.site }}/static/js/" } }</script>
 | 
			
		||||
    <script type="importmap">{
 | 
			
		||||
        "imports": {
 | 
			
		||||
            "treehouse/": "{{ config.site }}/static/js/",
 | 
			
		||||
            "tairu/": "{{ config.site }}/static/js/components/tairu/"
 | 
			
		||||
        }
 | 
			
		||||
    }</script>
 | 
			
		||||
 | 
			
		||||
    <script type="module">
 | 
			
		||||
        import { evaluate } from "treehouse/components/literate-programming/eval.js";
 | 
			
		||||
        import { evaluate, domConsole, jsConsole } from "treehouse/components/literate-programming/eval.js";
 | 
			
		||||
        import { internals as sandboxInternals } from "treehouse/sandbox.js";
 | 
			
		||||
 | 
			
		||||
        // I'm aware there's also ResizeObserver but it didn't seem to fire off any events when a
 | 
			
		||||
        // canvas was added, rendering it pretty much useless.
 | 
			
		||||
        let mutationObserver = new MutationObserver(() => {
 | 
			
		||||
            postMessage({
 | 
			
		||||
                kind: "resize",
 | 
			
		||||
                width: document.body.scrollWidth,
 | 
			
		||||
                height: document.body.scrollHeight,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        mutationObserver.observe(document.body, { subtree: true, childList: true });
 | 
			
		||||
        globalThis.console = domConsole;
 | 
			
		||||
 | 
			
		||||
        addEventListener("message", async event => {
 | 
			
		||||
            let message = event.data;
 | 
			
		||||
            if (message.action == "eval") {
 | 
			
		||||
                evaluate(message.input, {
 | 
			
		||||
                    success() {
 | 
			
		||||
                        // A double buffered approach for flickerless code modifications.
 | 
			
		||||
                        document.body.replaceChildren(...sandboxInternals.body.childNodes);
 | 
			
		||||
                        sandboxInternals.body.replaceChildren();
 | 
			
		||||
                        requestAnimationFrame(() => {
 | 
			
		||||
                            postMessage({
 | 
			
		||||
                                kind: "resize",
 | 
			
		||||
                                width: document.body.scrollWidth,
 | 
			
		||||
                                height: document.body.scrollHeight,
 | 
			
		||||
                            });
 | 
			
		||||
                        });
 | 
			
		||||
                    },
 | 
			
		||||
 | 
			
		||||
                    error() {
 | 
			
		||||
                        sandboxInternals.body.replaceChildren();
 | 
			
		||||
                    },
 | 
			
		||||
 | 
			
		||||
                    newOutput(currentOutputIndex) {
 | 
			
		||||
                        if (currentOutputIndex == window.treehouseSandboxInternals.outputIndex) {
 | 
			
		||||
                            document.body.replaceChildren(...sandboxInternals.body.childNodes);
 | 
			
		||||
                            postMessage({
 | 
			
		||||
                                kind: "resize",
 | 
			
		||||
                                outputIndex: currentOutputIndex,
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                        sandboxInternals.resetBody();
 | 
			
		||||
                    },
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue