From 59148db2959314dd0e9bd3425197000da6f17d4d Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Fri, 6 Feb 2026 14:40:49 +0100 Subject: [PATCH] Enhance Conway's Game of Life UI & interaction Add interactive controls and drawing support for Conway's Game of Life: introduce Node.alive, Scenario enum, spawn/speed/time constants, random/empty generation, and mouse/touch drawing (click-drag/touch to toggle cells). Update template to include control buttons, speed input, legend, and expose Scenario constants. Implement grid initialization, random seeding, grid position mapping, and optimized node drawing/color logic. Also update i18n (de/en) with GOL strings and move GRID label keys to ALGORITHM, switch some label usages accordingly. Move generic container/legend styles into global styles.scss (adjust canvas border color), and simplify component SCSS files. Change CONWAYS_WIKI URL to German wiki and remove now-unused UrlConstants references from components. --- src/app/constants/UrlConstants.ts | 2 +- .../algorithms/conway-gol/conway-gol.html | 92 +++++++---- .../conway-gol/conway-gol.models.ts | 12 ++ .../pages/algorithms/conway-gol/conway-gol.ts | 150 +++++++++++++++++- .../algorithms/information/information.ts | 2 - .../pathfinding/pathfinding.component.html | 4 +- .../pathfinding/pathfinding.component.scss | 33 ---- .../pathfinding/pathfinding.component.ts | 2 - .../algorithms/sorting/sorting.component.html | 2 +- .../algorithms/sorting/sorting.component.scss | 9 -- .../algorithms/sorting/sorting.component.ts | 2 - src/assets/i18n/de.json | 13 +- src/assets/i18n/en.json | 13 +- src/styles.scss | 38 ++++- 14 files changed, 276 insertions(+), 98 deletions(-) diff --git a/src/app/constants/UrlConstants.ts b/src/app/constants/UrlConstants.ts index 8f43b6a..b71c228 100644 --- a/src/app/constants/UrlConstants.ts +++ b/src/app/constants/UrlConstants.ts @@ -7,5 +7,5 @@ static readonly QUICK_SORT_WIKI = 'https://de.wikipedia.org/wiki/Quicksort' static readonly HEAP_SORT_WIKI = 'https://de.wikipedia.org/wiki/Heapsort' static readonly SHAKE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Shakersort' - static readonly CONWAYS_WIKI = 'https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life' + static readonly CONWAYS_WIKI = 'https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens' } diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html index 228e8f7..c94f1cf 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -4,36 +4,68 @@ -
- -
-
- - {{ 'PATHFINDING.GRID_HEIGHT' | translate }} - - - - {{ 'PATHFINDING.GRID_WIDTH' | translate }} - - +
+
+ + + + +
+
+ +
+
+ + {{ 'ALGORITHM.GRID_HEIGHT' | translate }} + + + + {{ 'ALGORITHM.GRID_WIDTH' | translate }} + + + + {{ 'GOL.SPEED' | translate }} + + +
+
+ {{ 'GOL.ALIVE' | translate }} + {{ 'GOL.DEAD' | translate }} +
diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.models.ts b/src/app/pages/algorithms/conway-gol/conway-gol.models.ts index a3300a6..f2854d6 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.models.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.models.ts @@ -1,6 +1,12 @@ export interface Node { row: number; col: number; + alive: boolean; +} + +export enum Scenario { + RANDOM = 0, + EMPTY } export const DEFAULT_GRID_ROWS = 100; @@ -8,4 +14,10 @@ export const DEFAULT_GRID_COLS = 100; export const MIN_GRID_SIZE = 20; export const MAX_GRID_SIZE = 200; +export const DEFAULT_TIME_PER_GENERATION = 50; + +export const MIN_TIME_PER_GENERATION = 20; +export const MAX_TIME_PER_GENERATION = 200; + export const MAX_GRID_PX = 1000; +export const LIVE_SPAWN_PROBABILITY = 0.37; diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index a80abc4..05f956f 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -8,7 +8,9 @@ import {MatButton} from '@angular/material/button'; import {MatIcon} from '@angular/material/icon'; import {MatFormField, MatInput, MatLabel} from '@angular/material/input'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_SIZE, MIN_GRID_SIZE, MAX_GRID_PX, Node} from './conway-gol.models'; +import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_SIZE, MIN_GRID_SIZE, MAX_GRID_PX, Node, LIVE_SPAWN_PROBABILITY, Scenario, MAX_TIME_PER_GENERATION, MIN_TIME_PER_GENERATION, DEFAULT_TIME_PER_GENERATION} from './conway-gol.models'; + +interface GridPos { row: number; col: number } @Component({ selector: 'app-conway-gol', @@ -45,20 +47,57 @@ export class ConwayGol implements AfterViewInit { disclaimerBottom: '', disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4'] }; + protected gridCols = DEFAULT_GRID_COLS; protected gridRows = DEFAULT_GRID_ROWS; + protected lifeSpeed = DEFAULT_TIME_PER_GENERATION; protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE; protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE; nodeSize = 10; grid: Node[][] = []; + currentScenario: Scenario = 0; @ViewChild('gridCanvas', { static: true }) canvas!: ElementRef; private ctx!: CanvasRenderingContext2D; + private lastCell: GridPos | null = null; + isDrawing = false; ngAfterViewInit(): void { this.ctx = this.getContextOrThrow(); this.applyGridSize(); + 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(); + }); + } + + generate(scene: Scenario): void { + this.currentScenario = scene; + this.initializeGrid(); + } + + setupRandomLives(): void { + for (let row = 0; row < this.gridRows; row++) { + for (let col = 0; col < this.gridCols; col++) { + this.grid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY; + } + } } applyGridSize(): void { @@ -73,11 +112,19 @@ export class ConwayGol implements AfterViewInit { return; } this.initializeGrid(); - } + applySpeed(): void { + this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION); + } + + private initializeGrid(): void { this.grid = this.createEmptyGrid(); + if (this.currentScenario === Scenario.RANDOM) { + this.setupRandomLives(); + } + this.drawGrid(); } @@ -87,7 +134,7 @@ export class ConwayGol implements AfterViewInit { for (let row = 0; row < this.gridRows; row++) { const currentRow: Node[] = []; for (let col = 0; col < this.gridCols; col++) { - currentRow.push(this.createNode(row, col)); + currentRow.push(this.createNode(row, col, false)); } grid.push(currentRow); } @@ -95,10 +142,11 @@ export class ConwayGol implements AfterViewInit { return grid; } - private createNode(row: number, col: number): Node { + private createNode(row: number, col: number, alive: boolean): Node { return { row, - col + col, + alive }; } @@ -113,14 +161,18 @@ export class ConwayGol implements AfterViewInit { } private drawNode(node: Node): void { - this.ctx.fillStyle = this.getNodeColor(); + this.ctx.fillStyle = this.getNodeColor(node); this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize); this.ctx.strokeStyle = '#ccc'; this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize); } - private getNodeColor(): string { + private getNodeColor(node: Node): string { + if (node.alive) + { + return 'black'; + } return 'lightgray'; } @@ -150,4 +202,88 @@ export class ConwayGol implements AfterViewInit { el.height = this.gridRows * this.nodeSize; } + //mouse listener + private onMouseDown(event: MouseEvent): void { + const pos = this.getGridPosition(event); + if (!pos) { + return; + } + + this.isDrawing = true; + this.lastCell = null; + this.applySelectionAt(pos); + } + + private onMouseMove(event: MouseEvent): void { + if (!this.isDrawing) { + return; + } + + const pos = this.getGridPosition(event); + if (!pos) { + return; + } + + if (this.isSameCell(pos, this.lastCell)) { + return; + } + + this.applySelectionAt(pos); + } + + private onMouseUp(): void { + this.isDrawing = false; + this.lastCell = null; + } + + // Mouse -> grid cell + 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; + } + + private applySelectionAt(pos: GridPos): void { + const node = this.grid[pos.row][pos.col]; + node.alive = !node.alive; + this.lastCell = pos; + this.drawNode(node); + } + + protected readonly Scenario = Scenario; + protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION; + protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION; } diff --git a/src/app/pages/algorithms/information/information.ts b/src/app/pages/algorithms/information/information.ts index c48af5a..3b3d589 100644 --- a/src/app/pages/algorithms/information/information.ts +++ b/src/app/pages/algorithms/information/information.ts @@ -14,6 +14,4 @@ import {AlgorithmInformation} from './information.models'; export class Information { @Input({ required: true }) algorithmInformation!: AlgorithmInformation; - - protected readonly UrlConstants = UrlConstants; } diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index 6f3a695..42b19ba 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -29,7 +29,7 @@
- {{ 'PATHFINDING.GRID_HEIGHT' | translate }} + {{ 'ALGORITHM.GRID_HEIGHT' | translate }} - {{ 'PATHFINDING.GRID_WIDTH' | translate }} + {{ 'ALGORITHM.GRID_WIDTH' | translate }} +
{{ 'SORTING.TITLE' | translate }} diff --git a/src/app/pages/algorithms/sorting/sorting.component.scss b/src/app/pages/algorithms/sorting/sorting.component.scss index b911eac..ce0e7a6 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.scss +++ b/src/app/pages/algorithms/sorting/sorting.component.scss @@ -1,11 +1,3 @@ -.sorting-container { - display: flex; - justify-content: center; - align-items: flex-start; - padding: 20px; - height: 100%; - box-sizing: border-box; - .sorting-card { width: 100%; max-width: 1200px; @@ -59,4 +51,3 @@ color: #FFFFFF; } } -} diff --git a/src/app/pages/algorithms/sorting/sorting.component.ts b/src/app/pages/algorithms/sorting/sorting.component.ts index 6fdebaa..569c037 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.ts +++ b/src/app/pages/algorithms/sorting/sorting.component.ts @@ -171,6 +171,4 @@ export class SortingComponent implements OnInit { this.stopAnimations(); this.resetSortState(); } - - protected readonly UrlConstants = UrlConstants; } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 3dacb72..7e03d1a 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -317,9 +317,7 @@ }, "ALERT": { "START_END_NODES": "Bitte wählen Sie einen Start- und Endknoten aus, bevor Sie den Algorithmus starten." - }, - "GRID_HEIGHT": "Höhe", - "GRID_WIDTH": "Beite" + } }, "SORTING": { "TITLE": "Sortieralgorithmen", @@ -345,6 +343,11 @@ "GOL": { "TITLE": "Conway's Spiel des Lebens", "START": "Starten", + "RANDOM_SCENE": "Zufällig", + "EMPTY_SCENE": "Leer", + "ALIVE": "Lebend", + "DEAD": "Leer", + "SPEED": "Zeit pro Generation", "EXPLANATION": { "TITLE": "Erklärung", "EXPLANATION" : "Das Spiel läuft schrittweise ab. Zunächst wird eine Anfangsgeneration von lebenden Zellen auf dem Spielfeld definiert. Aus der vorliegenden Generation (dem Gesamtbild des Spielfeldes) wird die Folgegeneration ermittelt. Der Zustand jeder einzelnen Zelle in der Folgegeneration ergibt sich dabei nach einfachen Regeln aus ihrem aktuellen Zustand sowie den aktuellen Zuständen ihrer acht Nachbarzellen (Moore-Nachbarschaft).", @@ -369,6 +372,8 @@ "TITLE": "Conway's Game of Life", "DESCRIPTION": "Das 'Spiel des Lebens' ist ein vom Mathematiker John Horton Conway 1970 entworfenes Spiel." }, - "NOTE": "HINWEIS" + "NOTE": "HINWEIS", + "GRID_HEIGHT": "Höhe", + "GRID_WIDTH": "Beite" } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b560660..8754e04 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -317,9 +317,7 @@ }, "ALERT": { "START_END_NODES": "Please select a start and end node before running the algorithm." - }, - "GRID_HEIGHT": "Height", - "GRID_WIDTH": "Width" + } }, "SORTING": { "TITLE": "Sorting Algorithms", @@ -344,6 +342,11 @@ "GOL": { "TITLE": "Conway's Game of Life", "START": "Start", + "RANDOM_SCENE": "Random", + "EMPTY_SCENE": "Empty", + "ALIVE": "Alive", + "DEAD": "Empty", + "SPEED": "Time per Generation", "EXPLANATION": { "TITLE": "Erklärung", "EXPLANATION" : "Das Spiel läuft schrittweise ab. Zunächst wird eine Anfangsgeneration von lebenden Zellen auf dem Spielfeld definiert. Aus der vorliegenden Generation (dem Gesamtbild des Spielfeldes) wird die Folgegeneration ermittelt. Der Zustand jeder einzelnen Zelle in der Folgegeneration ergibt sich dabei nach einfachen Regeln aus ihrem aktuellen Zustand sowie den aktuellen Zuständen ihrer acht Nachbarzellen (Moore-Nachbarschaft).", @@ -368,6 +371,8 @@ "TITLE:": "Conway's Game of Life", "DESCRIPTION": "The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970." }, - "NOTE": "Note" + "NOTE": "Note", + "GRID_HEIGHT": "Height", + "GRID_WIDTH": "Width" } } diff --git a/src/styles.scss b/src/styles.scss index 3321d1d..c01f329 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -213,6 +213,11 @@ a { } // algos + +.container { + padding: 2rem; +} + .algo-info { margin: 0 0 1rem 0; padding: 0.75rem 1rem; @@ -248,7 +253,38 @@ a { } canvas { - border: 1px solid #ccc; + border: 1px solid lightgray; display: block; max-width: 100%; } + +.legend { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + font-size: 0.9em; + + .legend-color { + display: inline-block; + width: 15px; + height: 15px; + border: 1px solid lightgray; + vertical-align: middle; + margin-right: 5px; + + &.start { background-color: green; } + &.end { background-color: red; } + &.wall { background-color: black; } + &.visited { background-color: skyblue; } + &.path { background-color: gold; } + &.empty { background-color: lightgray; } + &.alive { background-color: black; } + } +} + +.controls-container { + display: flex; + flex-direction: column; + margin-bottom: 1rem; +}