Extract generic grid component and refactor uses
Add a reusable GenericGridComponent (canvas + input handling) and migrate Conway Game of Life and Pathfinding pages to use it. New files: shared/components/generic-grid/{html,scss,ts} implement canvas rendering, resizing, input listeners and a callback API (createNodeFn, getNodeColorFn, applySelectionFn, initializationFn) plus gridChange events. Updated templates to replace raw <canvas> with <app-generic-grid> and switched grid size inputs to ngModelChange bindings. Conway and Pathfinding components: remove direct canvas/mouse handling, wire the generic grid callbacks, keep algorithm-specific logic (node creation, coloring, selection, scenarios and animations) but delegate drawing and interaction to GenericGridComponent. This centralizes grid rendering/interaction and simplifies per-algorithm components.
This commit is contained in:
1 parent
3795090cea
commit
930f0110b0
7 files changed
+581
-685
No files matched your search
@@ -0,0 +1 @@
|
||||
<canvas #gridCanvas></canvas>
|
||||
Whitespace-only changes.
@@ -0,0 +1,213 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
export interface GridPos { row: number; col: number }
|
||||
|
||||
@Component({
|
||||
selector: 'app-generic-grid',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './generic-grid.html',
|
||||
styleUrl: './generic-grid.scss',
|
||||
})
|
||||
export class GenericGridComponent implements AfterViewInit {
|
||||
@ViewChild('gridCanvas', { static: true })
|
||||
canvas!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@Input() gridRows: number = 10;
|
||||
@Input() gridCols: number = 10;
|
||||
@Input() nodeSize: number = 10; // Default node size, can be overridden by computeNodeSize
|
||||
@Input() maxGridPx: number = 500; // Max pixels for grid dimension
|
||||
@Input() minGridSize: number = 5;
|
||||
@Input() maxGridSize: number = 50;
|
||||
@Input() drawNodeBorderColor: string = '#ccc';
|
||||
|
||||
// Callbacks from parent component
|
||||
@Input() createNodeFn!: (row: number, col: number) => any;
|
||||
@Input() getNodeColorFn!: (node: any) => string;
|
||||
@Input() applySelectionFn!: (pos: GridPos, grid: any[][]) => void;
|
||||
@Input() initializationFn!: (grid: any[][]) => void;
|
||||
|
||||
@Output() gridChange = new EventEmitter<any[][]>();
|
||||
@Output() nodeClick = new EventEmitter<GridPos>();
|
||||
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
grid: any[][] = [];
|
||||
|
||||
isDrawing = false;
|
||||
private lastCell: GridPos | null = null;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ctx = this.getContextOrThrow();
|
||||
this.setupCanvasListeners();
|
||||
this.applyGridSize();
|
||||
}
|
||||
|
||||
setupCanvasListeners(): void {
|
||||
const el = this.canvas.nativeElement;
|
||||
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
||||
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
el.addEventListener('mouseup', () => this.onMouseUp());
|
||||
el.addEventListener('mouseleave', () => this.onMouseUp());
|
||||
|
||||
el.addEventListener('touchstart', (e) => {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
this.onMouseDown(e as never);
|
||||
}, { passive: false });
|
||||
|
||||
el.addEventListener('touchmove', (e) => {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
this.onMouseMove(e as never);
|
||||
}, { passive: false });
|
||||
|
||||
el.addEventListener('touchend', () => {
|
||||
this.onMouseUp();
|
||||
});
|
||||
}
|
||||
|
||||
applyGridSize(): void {
|
||||
this.gridRows = this.clampGridSize(this.gridRows);
|
||||
this.gridCols = this.clampGridSize(this.gridCols);
|
||||
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
||||
this.resizeCanvas();
|
||||
if (this.gridRows === this.grid.length && this.gridCols === this.grid[0]?.length) {
|
||||
this.drawGrid();
|
||||
return;
|
||||
}
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
initializeGrid(): void {
|
||||
this.grid = this.createEmptyGrid();
|
||||
if (this.initializationFn) {
|
||||
this.initializationFn(this.grid);
|
||||
}
|
||||
this.drawGrid();
|
||||
this.gridChange.emit(this.grid);
|
||||
}
|
||||
|
||||
createEmptyGrid(): any[][] {
|
||||
const grid: any[][] = [];
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
const currentRow: any[] = [];
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
currentRow.push(this.createNodeFn(row, col));
|
||||
}
|
||||
grid.push(currentRow);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
drawGrid(): void {
|
||||
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
this.drawNode(this.grid[row][col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawNode(node: any): void {
|
||||
this.ctx.fillStyle = this.getNodeColorFn(node);
|
||||
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
this.ctx.strokeStyle = this.drawNodeBorderColor;
|
||||
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
}
|
||||
|
||||
private getContextOrThrow(): CanvasRenderingContext2D {
|
||||
const ctx = this.canvas.nativeElement.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('CanvasRenderingContext2D not available.');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private clampGridSize(value: number): number {
|
||||
const parsed = Math.floor(Number(value));
|
||||
const safe = Number.isFinite(parsed) ? parsed : this.minGridSize; // Use minGridSize as fallback
|
||||
return Math.min(Math.max(this.minGridSize, safe), this.maxGridSize);
|
||||
}
|
||||
|
||||
private computeNodeSize(rows: number, cols: number): number {
|
||||
const sizeByWidth = Math.floor(this.maxGridPx / cols);
|
||||
const sizeByHeight = Math.floor(this.maxGridPx / rows);
|
||||
return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
|
||||
}
|
||||
|
||||
private resizeCanvas(): void {
|
||||
const el = this.canvas.nativeElement;
|
||||
el.width = this.gridCols * this.nodeSize;
|
||||
el.height = this.gridRows * this.nodeSize;
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent | TouchEvent): void {
|
||||
this.isDrawing = true;
|
||||
this.lastCell = null;
|
||||
const pos = this.getGridPosition(event);
|
||||
if (pos) {
|
||||
this.handleInteraction(pos);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent | TouchEvent): void {
|
||||
if (!this.isDrawing) {
|
||||
return;
|
||||
}
|
||||
const pos = this.getGridPosition(event);
|
||||
if (pos && !this.isSameCell(pos, this.lastCell)) {
|
||||
this.handleInteraction(pos);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp(): void {
|
||||
this.isDrawing = false;
|
||||
this.lastCell = null;
|
||||
}
|
||||
|
||||
private handleInteraction(pos: GridPos): void {
|
||||
this.applySelectionFn(pos, this.grid);
|
||||
this.drawNode(this.grid[pos.row][pos.col]);
|
||||
this.lastCell = pos;
|
||||
this.nodeClick.emit(pos);
|
||||
this.gridChange.emit(this.grid);
|
||||
}
|
||||
|
||||
private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
|
||||
const canvas = this.canvas.nativeElement;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
let clientX, clientY;
|
||||
if (event instanceof MouseEvent) {
|
||||
clientX = event.clientX;
|
||||
clientY = event.clientY;
|
||||
} else if (event instanceof TouchEvent && event.touches.length > 0) {
|
||||
clientX = event.touches[0].clientX;
|
||||
clientY = event.touches[0].clientY;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
|
||||
const x = (clientX - rect.left) * scaleX;
|
||||
const y = (clientY - rect.top) * scaleY;
|
||||
|
||||
const col = Math.floor(x / this.nodeSize);
|
||||
const row = Math.floor(y / this.nodeSize);
|
||||
|
||||
if (!this.isValidPosition(row, col)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { row, col };
|
||||
}
|
||||
|
||||
private isValidPosition(row: number, col: number): boolean {
|
||||
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
|
||||
}
|
||||
|
||||
private isSameCell(a: GridPos, b: GridPos | null): boolean {
|
||||
return !!b && a.row === b.row && a.col === b.col;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user