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(position: Point, renderer: Renderer, tile: T): void; drawTile(position: Point, renderer: NullRenderer): void; drawTile(position: Point, renderer: Renderer | 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; 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}`; } }