prime-map-generator/src/main/js/Painter.ts

212 lines
5.3 KiB
TypeScript

import {Model} from "./Model";
/**
* Paints prime numbers on a grid according to some `Model`.
*
* A `Painter` keeps track of the HTML side and determines which part of the `Model` is currently visible to the user.
*/
export class Painter {
/**
* The canvas to draw on.
*
* @private
*/
private readonly canvas: HTMLCanvasElement;
/**
* The context of `canvas` to draw on.
*
* @private
*/
private readonly ctx: CanvasRenderingContext2D;
/**
* `true` if and only if this painter is currently in a draw loop.
*
* @see #startDrawLoop
* @private
*/
private isDrawing: boolean = false;
/**
* The model that determines where primes are painted.
*/
public model: Model | undefined;
/**
* The scale to draw prime numbers at.
*/
private _scale: number = 15;
public get scale(): number {
return this._scale;
}
public set scale(scale: number) {
this._scale = Math.max(scale, 1);
}
/**
* The vertical position of the viewport.
*/
private _scrollX: number = 0;
public get scrollX(): number {
return this._scrollX;
}
public set scrollX(scrollX: number) {
const min = this.model?.minX;
const max = this.model?.maxX;
this._scrollX = min != null && scrollX < min ? min : (max != null && scrollX > max ? max : scrollX);
}
/**
* The horizontal position of the viewport.
*/
private _scrollY: number = 0;
public get scrollY(): number {
return this._scrollY;
}
public set scrollY(scrollY: number) {
const min = this.model?.minY;
const max = this.model?.maxY;
this._scrollY = min != null && scrollY < min ? min : (max != null && scrollY > max ? max : scrollY);
}
/**
* Constructs a new `Painter`.
*
* @param canvas the canvas to be used by this painter
*/
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d")!;
}
/**
* The width of this painter in terms of grid squares.
*
* @private
*/
private get width(): number {
return Math.floor(this.ctx.canvas.clientWidth / this.scale) - 1;
}
/**
* The height of this painter in terms of grid squares.
*
* @private
*/
private get height(): number {
return Math.floor(this.ctx.canvas.clientHeight / this.scale) - 1;
}
/**
* Starts repeatedly invoking `draw` onto the `canvas`.
*/
public startDrawLoop(): void {
if (this.isDrawing) return;
const cb = () => {
if (!this.isDrawing) return;
this.draw();
window.requestAnimationFrame(cb);
};
this.isDrawing = true;
window.requestAnimationFrame(cb);
}
/**
* Stops the draw loop started by `startDrawLoop`.
*/
public stopDrawLoop(): void {
this.isDrawing = false;
}
/**
* Draws on the canvas once.
*/
draw(): void {
this.clearCanvas();
this.ctx.save();
this.ctx.scale(this.scale, this.scale);
this.ctx.translate(0.2, 0.2);
this.drawBoundaries();
this.drawPrimes();
this.ctx.restore();
}
/**
* Clears the entire canvas.
*
* @private
*/
private clearCanvas(): void {
this.ctx.save();
this.ctx.fillStyle = "#ffffff";
this.ctx.fillRect(0, 0, this.canvas.clientWidth, this.canvas.clientHeight);
this.ctx.restore();
}
private drawBoundaries(): void {
this.ctx.save();
this.ctx.beginPath();
this.ctx.strokeStyle = "#000000";
this.ctx.lineWidth = 0.2;
const scrolledMinX = (this.model?.minX ?? 0) - this.scrollX;
const scrolledMaxX = this.model?.maxX == null ? this.width : this.model.maxX - this.scrollX;
const scrolledMinY = (this.model?.minY ?? 0) - this.scrollY;
const scrolledMaxY = this.model?.maxY == null ? this.height : this.model.maxY - this.scrollY;
if (this.model?.minX != null) {
this.ctx.moveTo(scrolledMinX, scrolledMinY);
this.ctx.lineTo(scrolledMinX, scrolledMaxY);
}
if (this.model?.maxX != null) {
this.ctx.moveTo(scrolledMaxX, scrolledMinY);
this.ctx.lineTo(scrolledMaxX, scrolledMaxY);
}
if (this.model?.minY != null) {
this.ctx.moveTo(scrolledMinX, scrolledMinY);
this.ctx.lineTo(scrolledMaxX, scrolledMinY);
}
if (this.model?.maxY != null) {
this.ctx.moveTo(scrolledMinX, scrolledMaxY);
this.ctx.lineTo(scrolledMaxX, scrolledMaxY);
}
this.ctx.stroke();
this.ctx.restore();
}
/**
* Draws prime numbers on the canvas.
*
* @private
*/
private drawPrimes(): void {
this.ctx.save();
this.ctx.fillStyle = "#0033cc"; // TODO: Get color programmatically
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
if (this.model?.isPrime(this.scrollX + x, this.scrollY + y)) {
this.ctx.fillRect(x, y, 1, 1);
}
}
}
this.ctx.restore();
}
}