141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
import { renderers, type NullRenderer, type Renderer } from "./renderer";
|
|
import { exp, trunc } from "./utils";
|
|
import type World from "./world";
|
|
import { type Tile } from "./world";
|
|
|
|
const initialTileSize = 32;
|
|
|
|
export default class Graphics {
|
|
private context: CanvasRenderingContext2D;
|
|
private tileSize = initialTileSize;
|
|
private offset: Point = [0, 0];
|
|
private highlighted: Point = [0, 0];
|
|
|
|
constructor(private canvas: HTMLCanvasElement) {
|
|
this.context = canvas.getContext('2d')!;
|
|
this.resetView();
|
|
}
|
|
|
|
get width() {
|
|
return this.canvas.width;
|
|
}
|
|
|
|
get height() {
|
|
return this.canvas.height;
|
|
}
|
|
|
|
get size(): Point {
|
|
return [this.canvas.width, this.canvas.height];
|
|
}
|
|
|
|
applyScale(scale: number, point: Point) {
|
|
const newTileSize = Math.min(Math.max(this.tileSize * scale, 16), this.width / 2, this.height / 2);
|
|
const realScale = newTileSize / this.tileSize;
|
|
this.tileSize = newTileSize;
|
|
|
|
this.offset = exp`((${this.offset} - ${point}) * ${realScale}) + ${point}`;
|
|
}
|
|
|
|
pan(amount: Point) {
|
|
this.offset = exp`${this.offset} + ${amount}`;
|
|
}
|
|
|
|
resetView() {
|
|
this.tileSize = initialTileSize;
|
|
this.offset = exp`(${this.size} - ${this.tileSize}) / ${2}`;
|
|
}
|
|
|
|
highlight(screenPoint: Point) {
|
|
this.highlighted = this.screenToWorld(screenPoint);
|
|
}
|
|
|
|
clear() {
|
|
this.context.clearRect(0, 0, this.width, this.height);
|
|
}
|
|
|
|
debug() {
|
|
// const p00 = this.worldToScreen([0, 0]);
|
|
// const p11 = exp`${this.worldToScreen([1, 1])} - ${p00}`;
|
|
|
|
// this.context.fillStyle = 'red';
|
|
// this.context.fillRect(...p00, ...p11);
|
|
}
|
|
|
|
drawGrid() {
|
|
this.context.beginPath();
|
|
this.context.strokeStyle = 'gray';
|
|
let [x0, y0, x1, y1] = this.visibleWorld;
|
|
[x0, y0] = this.worldToScreen([x0, y0]);
|
|
[x1, y1] = this.worldToScreen([x1, y1]);
|
|
|
|
for (let x = x0; x < x1; x += this.tileSize) {
|
|
this.context.moveTo(x, 0);
|
|
this.context.lineTo(x, this.height);
|
|
}
|
|
for (let y = y0; y < y1; y += this.tileSize) {
|
|
this.context.moveTo(0, y);
|
|
this.context.lineTo(this.width, y);
|
|
}
|
|
this.context.stroke();
|
|
}
|
|
|
|
drawHighlight() {
|
|
this.drawTile(this.highlighted, (ctx) => {
|
|
ctx.fillStyle = getComputedStyle(this.canvas).getPropertyValue("--color-bg-select");
|
|
ctx.fillRect(0, 0, 1, 1);
|
|
});
|
|
}
|
|
|
|
drawTile<T extends Tile>(position: Point, renderer: Renderer<T>, tile: T): void;
|
|
drawTile<T extends null>(position: Point, renderer: NullRenderer): void;
|
|
drawTile<T extends Tile>(position: Point, renderer: Renderer<T> | NullRenderer, tile?: T): void {
|
|
this.context.save();
|
|
|
|
// TODO skip drawing if outside screen
|
|
const screenPosition = this.worldToScreen(trunc(position));
|
|
this.context.translate(screenPosition[0], screenPosition[1]);
|
|
this.context.scale(this.tileSize, this.tileSize);
|
|
|
|
if (tile) {
|
|
renderer(this.context, tile);
|
|
} else {
|
|
(renderer as NullRenderer)(this.context);
|
|
}
|
|
|
|
this.context.restore();
|
|
}
|
|
|
|
drawWorld(world: World) {
|
|
this.context.font = 'bold 0.4px sans-serif';
|
|
this.context.textRendering = 'optimizeSpeed';
|
|
this.context.textAlign = 'center';
|
|
const [x0, y0, x1, y1] = this.visibleWorld;
|
|
for (let y = y0; y <= y1; y++) {
|
|
for (let x = x0; x <= x1; x++) {
|
|
const tile = world.getTile([x, y]);
|
|
if (tile) {
|
|
const renderer = renderers[tile.type] as Renderer<Tile>;
|
|
this.drawTile([x, y], renderer, tile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get visibleWorld(): Rect {
|
|
const [x0, y0] = this.screenToWorld([0, 0]);
|
|
const [x1, y1] = this.screenToWorld([this.width, this.height]);
|
|
|
|
return [Math.floor(x0), Math.floor(y0), Math.ceil(x1), Math.ceil(y1)];
|
|
}
|
|
|
|
worldToScreen(point: Point): Point {
|
|
return [
|
|
point[0] * this.tileSize + this.offset[0],
|
|
point[1] * this.tileSize + this.offset[1],
|
|
];
|
|
}
|
|
|
|
screenToWorld(point: Point): Point {
|
|
return exp`(${point} - ${this.offset}) / ${this.tileSize}`;
|
|
}
|
|
} |