Implement canvas traversal

This commit is contained in:
Florine W. Dekker 2022-04-07 21:51:09 +02:00
parent 1372ab3cc0
commit 6638faec61
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
5 changed files with 200 additions and 130 deletions

View File

@ -39,11 +39,14 @@
<label for="gridCols">Columns</label>
<input type="number" id="gridCols" min="1" value="100" />
<label for="gridMax">Max</label>
<input type="number" id="gridMax" min="1" value="10000" />
<label for="gridScale">Scale</label>
<input type="number" id="gridScale" min="1" value="15" />
<label for="scrollX">Scroll X</label>
<input type="number" id="scrollX" min="0" value="0" />
<label for="scrollY">Scroll Y</label>
<input type="number" id="scrollY" min="0" value="0" />
</form>
</div>
</section>

View File

@ -1,117 +0,0 @@
import {CachedPrimeMath, PrimeMath} from "./PrimeMath";
export class Display {
private readonly canvas: HTMLCanvasElement;
private _painter: Painter | undefined = undefined;
public set painter(painter: Painter) {
this._painter = painter;
}
private isDrawing: boolean = false;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
}
public startDrawLoop(): void {
if (this.isDrawing) return;
const cb = () => {
if (!this.isDrawing) return;
this._painter?.draw(this.canvas);
window.requestAnimationFrame(cb);
};
this.isDrawing = true;
window.requestAnimationFrame(cb);
}
public stopDrawLoop(): void {
this.isDrawing = false;
}
}
export interface Painter {
getPixelWidth(): number;
getPixelHeight(): number;
draw(canvas: HTMLCanvasElement): void;
}
export class GridPainter implements Painter {
private readonly primeMath: PrimeMath = CachedPrimeMath.getInstance();
private readonly gridScale: number = 1 / 15;
public scale: number = 15;
public cols: number = 100;
public max: number = 10_001;
public get rows(): number {
return Math.ceil(this.max / this.cols);
}
getPixelWidth(): number {
return this.scale * this.cols;
}
getPixelHeight(): number {
return this.scale * this.rows;
}
draw(canvas: HTMLCanvasElement): void {
const ctx = canvas.getContext("2d")!;
this.clearCanvas(ctx);
this.drawPrimes(ctx);
this.drawGrid(ctx);
}
private clearCanvas(ctx: CanvasRenderingContext2D): void {
ctx.save();
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
ctx.restore();
}
private drawGrid(ctx: CanvasRenderingContext2D): void {
// TODO: Don't draw grid for non-filled rows
ctx.save();
ctx.beginPath();
ctx.strokeStyle = "#7b7b7b";
ctx.lineWidth = this.scale * this.gridScale;
for (let x = 0; x <= this.cols; x++) {
ctx.moveTo(x * this.scale, 0);
ctx.lineTo(x * this.scale, this.rows * this.scale);
}
for (let y = 0; y <= this.rows; y++) {
ctx.moveTo(0, y * this.scale);
ctx.lineTo(this.cols * this.scale, y * this.scale);
}
ctx.stroke();
ctx.restore();
}
private drawPrimes(ctx: CanvasRenderingContext2D): void {
ctx.save();
ctx.fillStyle = "#ff0000";
for (let i = 1; i <= this.max; i++) {
if (this.primeMath.isPrime(i))
ctx.fillRect(
((i - 1) % this.cols) * this.scale,
Math.floor((i - 1) / this.cols) * this.scale,
this.scale,
this.scale
);
}
ctx.restore();
}
}

View File

@ -1,7 +1,8 @@
// @ts-ignore
const {$, doAfterLoad, footer, header, nav} = window.fwdekker;
import {Display, GridPainter} from "./Display";
import {GridModel} from "./Model";
import {Painter} from "./Painter";
// Set up template
@ -30,17 +31,20 @@ doAfterLoad(async () => {
window.addEventListener("resize", resize, false);
resize();
const display = new Display(canvas);
const gridPainter = new GridPainter();
display.painter = gridPainter;
display.startDrawLoop();
const painter = new Painter(canvas);
const model = new GridModel();
painter.model = model;
painter.startDrawLoop();
const gridCols = $("#gridCols");
gridCols.addEventListener("change", () => gridPainter.cols = gridCols.value);
const gridMax = $("#gridMax");
gridMax.addEventListener("change", () => gridPainter.max = gridMax.value);
gridCols.addEventListener("change", () => model.cols = parseInt(gridCols.value));
const gridScale = $("#gridScale");
gridScale.addEventListener("change", () => gridPainter.scale = gridScale.value);
gridScale.addEventListener("change", () => painter.scale = parseInt(gridScale.value));
const scrollX = $("#scrollX");
scrollX.addEventListener("change", () => model.scrollX = parseInt(scrollX.value));
const scrollY = $("#scrollY");
scrollY.addEventListener("change", () => model.scrollY = parseInt(scrollY.value));
});

46
src/main/js/Model.ts Normal file
View File

@ -0,0 +1,46 @@
import {CachedPrimeMath, PrimeMath} from "./PrimeMath";
/**
* Represents a particular way in which prime numbers can be arranged and drawn.
*/
export interface Model {
/**
* Returns `true` if and only if the square at coordinates (`x`, `y`) is a prime number.
*
* @param x the horizontal coordinate
* @param y the vertical coordinate
* @return `true` if and only if the square at coordinates (`x`, `y`) is a prime number
*/
isPrime(x: number, y: number): boolean | null;
}
/**
* A `Model` for a simple grid of primes.
*
* Supports scrolling by setting the `scrollX` and `scrollY` values.
*/
export class GridModel implements Model {
private readonly primeMath: PrimeMath = CachedPrimeMath.getInstance();
/**
* The vertical position of the viewport.
*/
public scrollX: number = 0;
/**
* The horizontal position of the viewport.
*/
public scrollY: number = 0;
/**
* The number of columns to draw primes in.
*/
public cols: number = 100;
isPrime(x: number, y: number): boolean | null {
if (this.scrollX + x >= this.cols) return null;
const n = (y + this.scrollY) * this.cols + this.scrollX + x + 1;
return this.primeMath.isPrime(n);
}
}

134
src/main/js/Painter.ts Normal file
View File

@ -0,0 +1,134 @@
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.
*/
public scale: number = 15;
/**
* 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 this.ctx.canvas.clientWidth / this.scale;
}
/**
* The height of this painter in terms of grid squares.
*
* @private
*/
private get height(): number {
return this.ctx.canvas.clientHeight / this.scale;
}
/**
* 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.drawPrimes();
}
/**
* 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();
}
/**
* Draws prime numbers on the canvas.
*
* @private
*/
private drawPrimes(): void {
const width = this.width;
const height = this.height;
this.ctx.save();
this.ctx.fillStyle = "#0033cc"; // TODO: Get color programmatically
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
if (this.model?.isPrime(x, y)) {
this.ctx.fillRect(x * this.scale, y * this.scale, this.scale, this.scale);
}
}
}
this.ctx.restore();
}
}