diff --git a/src/app.ts b/src/app.ts index 04907da..1a86f61 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,12 +1,12 @@ -import {fetchPalette, Palette} from "./sprite"; +import {fetchPalette} from "./sprite"; +import {Chunk} from "./chunk"; export class App { private canvas: HTMLCanvasElement; private readonly context: CanvasRenderingContext2D; private lastTime: DOMHighResTimeStamp = 0; private isRunning: boolean = false; - - private palette?: Palette; + private chunk?: Chunk; public constructor(canvas: HTMLCanvasElement) { this.canvas = canvas; @@ -19,7 +19,7 @@ export class App { this.updateSize(window.innerWidth, window.innerHeight); window.onresize = () => this.updateSize(window.innerWidth, window.innerHeight); - fetchPalette("").then(p => this.palette = p); + fetchPalette("").then(palette => this.chunk = new Chunk(palette)); } public run() { @@ -33,14 +33,8 @@ export class App { private draw(time: DOMHighResTimeStamp) { this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); - if (this.palette) { - let x = 0; - for (let s of this.palette.sprites) { - for (let y = 0; y < 4; y++) { - s.draw(this.context, y * 128, x, y); - } - x += 128; - } + if (this.chunk) { + this.chunk.render(this.context); } this.lastTime = time; @@ -56,4 +50,5 @@ export class App { } const app = new App(document.getElementById("render") as HTMLCanvasElement); +(window as any)._app = app; app.run(); diff --git a/src/chunk.ts b/src/chunk.ts new file mode 100644 index 0000000..0d55be6 --- /dev/null +++ b/src/chunk.ts @@ -0,0 +1,161 @@ +import { IRender } from "./render"; +import { Direction, Palette, Sprite } from "./sprite"; + +/** Either a tile (if collapsed), or a list of possible tiles (if not) */ +type MaybeTile = Tile | Tile[]; + +/** Number of tiles per row and column of a chunk */ +const CHUNK_DIM = 16 >>> 0; + +export class Chunk implements IRender { + private palette: Palette; + private grid: MaybeTile[] = []; + private complete: boolean = false; + + public constructor(palette: Palette) { + this.palette = palette; + + // pregenerate an array with all possible tiles + let allTileVariants = []; + for (let s of palette.sprites) { + for (let dir = 0; dir < 4; dir++) { + allTileVariants.push(new Tile(s, dir)); + } + } + + // initialize the grid with perfect entropy + for (let i = 0; i < CHUNK_DIM ** 2; i++) { + this.grid.push(allTileVariants); + } + + // collapse a single tile to generate an initial disturbance in entropy + this.collapseAndUpdate(0, 0); + + window.onmousedown = () => this.tick(); + } + + public render(context: CanvasRenderingContext2D) { + const res = this.palette.resolution; + this.grid.forEach((tile, index) => { + const y = Math.floor(index / CHUNK_DIM); + const x = index % CHUNK_DIM; + if (!Array.isArray(tile)) { + tile.draw(context, x * res, y * res); + } + }); + + this.tick(); + } + + private tick() { + if (this.complete) { + return; + } + + let lowest: number = Number.MAX_SAFE_INTEGER; + let lowestCoords: number[][] = []; + this.grid.forEach((tile, index) => { + if (Array.isArray(tile)) { + if (tile.length <= lowest) { + const x = index % CHUNK_DIM; + const y = Math.floor(index / CHUNK_DIM); + if (tile.length < lowest) { + lowest = tile.length; + lowestCoords = [[x, y]]; + } else { + lowestCoords.push([x, y]); + } + } + } + }); + if (lowestCoords.length > 0) { + let coords = pickRandom(lowestCoords); + this.collapseAndUpdate(coords[0], coords[1]); + } else { + this.complete = true; + } + } + + /** Collapse a tile and update entropy on surrounding tiles */ + private collapseAndUpdate(x: number, y: number) { + const index = y * CHUNK_DIM + x; + const candidates = this.grid[index]; + if (!Array.isArray(candidates)) { + return; // tile is already collapsed (or out of bounds) + } + + this.grid[index] = pickRandom(candidates); + + if (y > 0) { + this.updateEntropy(x, y - 1); // north + } + if (x < CHUNK_DIM - 1) { + this.updateEntropy(x + 1, y); // east + } + if (y < CHUNK_DIM - 1) { + this.updateEntropy(x, y + 1); // south + } + if (x > 0) { + this.updateEntropy(x - 1, y); // west + } + } + + /** Remove all candidates that are no longer possible */ + private updateEntropy(x: number, y: number) { + const i = y * CHUNK_DIM + x; + const candidates = this.grid[i]; + if (!Array.isArray(candidates)) { + return; // tile is already collapsed (or out of bounds) + } + + const filterTile = (tile: MaybeTile) => Array.isArray(tile) ? null : tile; + const neighs: (Tile | null)[] = [null, null, null, null]; + if (y > 0) { + neighs[Direction.NORTH] = filterTile(this.grid[i - CHUNK_DIM]); + } + if (x < CHUNK_DIM - 1) { + neighs[Direction.EAST] = filterTile(this.grid[i + 1]); + } + if (y < CHUNK_DIM - 1) { + neighs[Direction.SOUTH] = filterTile(this.grid[i + CHUNK_DIM]); + } + if (x > 0) { + neighs[Direction.WEST] = filterTile(this.grid[i - 1]); + } + + // we have to be careful to create an entirely new array here + // because the constructor inserts a reference to the same array + // of MaybeTiles for every cell, so manipulating the array in-place + // would break everything + this.grid[i] = candidates.filter(c => !neighs.some((n, dir) => n && c.conflicts(dir, n))); + } +} + +export class Tile { + private readonly sprite: Sprite; + private readonly orientation: Direction; + + public constructor(sprite: Sprite, orientation: Direction) { + this.sprite = sprite; + this.orientation = orientation; + } + + public matches(ownEdge: Direction, neighbor: Tile): boolean { + const ownEdgeAbs = (ownEdge + 4 - this.orientation) % 4; + const neighEdge = (ownEdge + 2) % 4; + const neighEdgeAbs = (neighEdge + 4 - neighbor.orientation) % 4; + return this.sprite.matches(ownEdgeAbs, neighbor.sprite, neighEdgeAbs); + } + + public conflicts(ownEdge: Direction, neighbor: Tile): boolean { + return !this.matches(ownEdge, neighbor); + } + + public draw(context: CanvasRenderingContext2D, x: number, y: number) { + this.sprite.draw(context, x, y, this.orientation); + } +} + +function pickRandom(a: T[]): T { + return a[Math.round(Math.random() * (a.length - 1))]; +} diff --git a/src/render.ts b/src/render.ts new file mode 100644 index 0000000..e306c5c --- /dev/null +++ b/src/render.ts @@ -0,0 +1,3 @@ +export interface IRender { + render(context: CanvasRenderingContext2D): void; +}