2024-02-14 23:31:39 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-02-17 21:03:45 +01:00
|
|
|
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);
|
|
|
|
|
2024-02-14 23:31:39 +01:00
|
|
|
export class TileEditor extends Frame {
|
2024-02-11 23:05:08 +01:00
|
|
|
constructor() {
|
|
|
|
super();
|
2024-02-14 23:31:39 +01:00
|
|
|
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];
|
2024-02-17 21:03:45 +01:00
|
|
|
this.tilesets47 = [null];
|
2024-02-14 23:31:39 +01:00
|
|
|
|
|
|
|
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;
|
2024-02-17 21:03:45 +01:00
|
|
|
} else if (image.hasAttribute("data-tairu-tileset-47")) {
|
|
|
|
let tilesetIndex = parseInt(image.getAttribute("data-tairu-tileset-47"));
|
|
|
|
this.tilesets47[tilesetIndex] = image;
|
2024-02-14 23:31:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2024-02-17 21:03:45 +01:00
|
|
|
return this.tilesets.length > 1 || this.tilesets47.length > 1;
|
2024-02-14 23:31:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-02-11 23:05:08 +01:00
|
|
|
|
2024-02-14 23:31:39 +01:00
|
|
|
drawTexturedTiles() {
|
|
|
|
this.ctx.imageSmoothingEnabled = false;
|
2024-02-11 23:05:08 +01:00
|
|
|
|
2024-02-14 23:31:39 +01:00
|
|
|
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) {
|
2024-02-17 21:03:45 +01:00
|
|
|
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;
|
|
|
|
}
|
2024-02-14 23:31:39 +01:00
|
|
|
|
|
|
|
let tilesetTileSize = tileset.height;
|
|
|
|
let tilesetX = tileIndex * tilesetTileSize;
|
|
|
|
let tilesetY = 0;
|
|
|
|
this.ctx.drawImage(
|
2024-02-17 21:03:45 +01:00
|
|
|
tileset,
|
2024-02-14 23:31:39 +01:00
|
|
|
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);
|
|
|
|
}
|
2024-02-11 23:05:08 +01:00
|
|
|
}
|
|
|
|
}
|
2024-02-14 23:31:39 +01:00
|
|
|
defineFrame("tairu-editor", TileEditor);
|