You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

173 lines
5.1 KiB
TypeScript

import { Schema as JSONSchema, validate as validateJSON } from "./json";
/**
* JSON schema for a single sprite.
*/
interface ISpriteData {
/** URL to this tile image, appended to `IPaletteData.baseurl`. */
url: string,
/**
* All edge sections and their material IDs.
* Edges are divided into `IPaletteData.sections` sections,
* and their individual materials are stored here in clockwise order.
*
* The illustration below shows the indices of material IDs for
* 3-section edges.
*
* ```
* 0 1 2
* + - - - +
* 11 | | 3
* 10 | tile | 4
* 9 | | 5
* + - - - +
* 8 7 6
* ```
*/
edges: number[],
}
/**
* JSON schema for `/dist/sprites.json`
*/
export interface IPaletteData {
/**
* Absolute base URL prepended to all tile URLs.
* Must not end with a trailing slash.
*/
baseurl: string,
/**
* Number of sections per edge.
* The `ISprite.edges` array must be exactly 4 times as long as this value
* (as in the four edges of a quadratic tile).
*/
sections: number,
/** horizontal and vertical resolution of all sprites, in pixels */
resolution: number,
/** Array of all tile prototypes. */
sprites: ISpriteData[],
}
/**
* Stores and renders a single type of tile.
* Actual `Tile`s refer to instances of this class for their data.
*/
export class Sprite {
private readonly sections: number;
private readonly edges: number[];
private bitmap?: ImageBitmap;
public constructor(sections: number, baseurl: string, proto: ISpriteData) {
this.sections = sections;
if (proto.edges.length !== sections * 4) {
throw new TypeError("Invalid edge count");
}
this.edges = proto.edges;
this.fetchBitmapAsync(baseurl + proto.url)
.then(i => this.bitmap = i)
.catch(e => console.error(e));
}
public matches(ownEdge: Direction, other: Sprite, otherEdge: Direction): boolean {
const sections = this.sections;
if (other.sections !== sections) {
return false;
}
const getStart = (edge: Direction) => {
let start = edge * sections;
if (edge >= 2) {
start += sections - 1;
}
return start;
};
let ownPos = getStart(ownEdge);
let otherPos = getStart(otherEdge);
const ownDir = ownEdge < 2 ? 1 : -1;
const otherDir = otherEdge < 2 ? 1 : -1;
for (let n = 0; n < sections; n++) {
if (this.edges[ownPos] !== other.edges[otherPos]) {
return false;
}
ownPos += ownDir;
otherPos += otherDir;
}
return true;
}
/**
* Draw this sprite to the screen at the specified position and rotation.
*
* @param context The context to draw the sprite in
* @param dx horizontal offset in pixels
* @param dy vertical offset in pixels
* @param orientation If specified, rotate the sprite such that the north
* facing edge points to the specified direction.
*/
public draw(context: CanvasRenderingContext2D, dx: number, dy: number, orientation?: Direction) {
if (this.bitmap !== undefined) {
if (orientation === undefined || orientation === Direction.NORTH) {
context.drawImage(this.bitmap, dx, dy);
} else {
context.save();
const halfWidth = this.bitmap.width / 2;
const halfHeight = this.bitmap.height / 2;
context.translate(dx + halfWidth, dy + halfHeight);
context.rotate(Math.PI * (orientation / 2));
context.drawImage(this.bitmap, -halfWidth, -halfHeight);
context.restore();
}
}
}
private async fetchBitmapAsync(url: string): Promise<ImageBitmap> {
console.debug(`fetching bitmap ${url}`);
const response = await fetch(url);
const blob = await response.blob();
const image = await createImageBitmap(blob);
console.debug(`fetched and parsed bitmap ${url}`);
return image;
}
}
export const enum Direction {
NORTH = 0,
EAST = 1,
SOUTH = 2,
WEST = 3,
}
export class Palette {
private readonly sections: number;
public readonly sprites: Sprite[];
public readonly resolution: number;
public constructor(proto: IPaletteData) {
this.sections = proto.sections;
const sprites = [];
for (let s of proto.sprites) {
sprites.push(new Sprite(proto.sections, proto.baseurl, s));
}
this.sprites = sprites;
this.resolution = proto.resolution;
}
}
const PALETTE_SCHEMA: JSONSchema = {
baseurl: "string",
sections: "number",
resolution: "number",
sprites: [{
url: "string",
edges: ["number"],
}],
};
export async function fetchPalette(rootURL: string): Promise<Palette> {
const response = await fetch(rootURL + "/palette.json");
const json = await response.json();
return new Palette(validateJSON(json, PALETTE_SCHEMA));
}