From da432138083eee1dea864f4ea0b6e9d0d5227bcc Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Fri, 6 Feb 2026 09:59:12 +0100 Subject: [PATCH 1/6] Created new component and refactored - Created new component to display the game of life algo - created an algo info component to combine the algo header for all algos --- src/app/app.routes.ts | 1 + src/app/constants/RouterConstants.ts | 7 +++ src/app/constants/UrlConstants.ts | 1 + .../algorithms/algorithms.component.scss | 4 +- .../algorithms/conway-gol/conway-gol.html | 8 +++ .../algorithms/conway-gol/conway-gol.scss | 0 .../pages/algorithms/conway-gol/conway-gol.ts | 43 ++++++++++++++ .../algorithms/information/information.html | 35 +++++++++++ .../information/information.models.ts | 14 +++++ .../algorithms/information/information.scss | 0 .../algorithms/information/information.ts | 19 ++++++ .../pathfinding/pathfinding.component.html | 18 +----- .../pathfinding/pathfinding.component.ts | 24 +++++++- .../algorithms/service/algorithms.service.ts | 6 ++ .../algorithms/sorting/sorting.component.html | 40 +------------ .../algorithms/sorting/sorting.component.ts | 58 ++++++++++++++----- src/assets/i18n/de.json | 13 +++-- src/assets/i18n/en.json | 12 +++- 18 files changed, 223 insertions(+), 80 deletions(-) create mode 100644 src/app/pages/algorithms/conway-gol/conway-gol.html create mode 100644 src/app/pages/algorithms/conway-gol/conway-gol.scss create mode 100644 src/app/pages/algorithms/conway-gol/conway-gol.ts create mode 100644 src/app/pages/algorithms/information/information.html create mode 100644 src/app/pages/algorithms/information/information.models.ts create mode 100644 src/app/pages/algorithms/information/information.scss create mode 100644 src/app/pages/algorithms/information/information.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 55aa00e..33423b1 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -10,5 +10,6 @@ export const routes: Routes = [ { path: RouterConstants.PATHFINDING.PATH, component: RouterConstants.PATHFINDING.COMPONENT}, { path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT}, { path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT}, + { path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT} ]; diff --git a/src/app/constants/RouterConstants.ts b/src/app/constants/RouterConstants.ts index ce20694..30122d5 100644 --- a/src/app/constants/RouterConstants.ts +++ b/src/app/constants/RouterConstants.ts @@ -4,6 +4,7 @@ import {ImprintComponent} from '../pages/imprint/imprint.component'; import {AlgorithmsComponent} from '../pages/algorithms/algorithms.component'; import {PathfindingComponent} from '../pages/algorithms/pathfinding/pathfinding.component'; import {SortingComponent} from '../pages/algorithms/sorting/sorting.component'; +import {ConwayGol} from '../pages/algorithms/conway-gol/conway-gol'; export class RouterConstants { @@ -37,6 +38,12 @@ export class RouterConstants { COMPONENT: SortingComponent }; + static readonly GOL = { + PATH: 'algorithms/gol', + LINK: '/algorithms/gol', + COMPONENT: ConwayGol + }; + static readonly IMPRINT = { PATH: 'imprint', LINK: '/imprint', diff --git a/src/app/constants/UrlConstants.ts b/src/app/constants/UrlConstants.ts index 4c8ce37..8f43b6a 100644 --- a/src/app/constants/UrlConstants.ts +++ b/src/app/constants/UrlConstants.ts @@ -7,4 +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' } diff --git a/src/app/pages/algorithms/algorithms.component.scss b/src/app/pages/algorithms/algorithms.component.scss index 825e521..ba0db08 100644 --- a/src/app/pages/algorithms/algorithms.component.scss +++ b/src/app/pages/algorithms/algorithms.component.scss @@ -10,7 +10,7 @@ mat-card { cursor: pointer; - min-width: 300px; - max-width: 400px; + min-width: 450px; + max-width: 450px; } } diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html new file mode 100644 index 0000000..43c212e --- /dev/null +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -0,0 +1,8 @@ + + + {{ 'GOL.TITLE' | translate }} + + + + + diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.scss b/src/app/pages/algorithms/conway-gol/conway-gol.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts new file mode 100644 index 0000000..b0ecc02 --- /dev/null +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; +import {TranslatePipe} from "@ngx-translate/core"; +import {UrlConstants} from '../../../constants/UrlConstants'; +import {Information} from '../information/information'; +import {AlgorithmInformation} from '../information/information.models'; + +@Component({ + selector: 'app-conway-gol', + imports: [ + MatCard, + MatCardContent, + MatCardHeader, + MatCardTitle, + TranslatePipe, + Information + ], + templateUrl: './conway-gol.html', + styleUrl: './conway-gol.scss', +}) +export class ConwayGol { + + protected readonly UrlConstants = UrlConstants; + + algoInformation: AlgorithmInformation = { + title: 'PATHFINDING.EXPLANATION.TITLE', + entries: [ + { + name: 'Dijkstra', + description: 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION', + link: UrlConstants.DIJKSTRA_WIKI + }, + { + name: 'A*', + description: 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION', + link: UrlConstants.ASTAR_WIKI + } + ], + disclaimer: 'PATHFINDING.EXPLANATION.DISCLAIMER', + disclaimerBottom: '', + disclaimerListEntry: [] + }; +} diff --git a/src/app/pages/algorithms/information/information.html b/src/app/pages/algorithms/information/information.html new file mode 100644 index 0000000..a38c736 --- /dev/null +++ b/src/app/pages/algorithms/information/information.html @@ -0,0 +1,35 @@ +
+

{{ algorithmInformation.title | translate }}

+ + @if(algorithmInformation.entries && algorithmInformation.entries.length > 0){ + @for (algo of algorithmInformation.entries; track algo) + { +

+ {{ algo.name }} {{ algo.description | translate }} + Wikipedia +

+ } + } + + @if (algorithmInformation.disclaimer !== '') + { +

+ {{ 'ALGORITHM.NOTE' | translate}} {{ algorithmInformation.disclaimer | translate}} +

+ @if (algorithmInformation.disclaimerListEntry && algorithmInformation.disclaimerListEntry.length > 0) + { + + } + @if (algorithmInformation.disclaimerBottom !== '') + { +

+ {{ algorithmInformation.disclaimerBottom | translate}} +

+ } + } +
diff --git a/src/app/pages/algorithms/information/information.models.ts b/src/app/pages/algorithms/information/information.models.ts new file mode 100644 index 0000000..9cd064b --- /dev/null +++ b/src/app/pages/algorithms/information/information.models.ts @@ -0,0 +1,14 @@ +export interface AlgorithmInformation { + title: string; + entries: AlgorithmEntry[]; + disclaimer: string; + disclaimerBottom: string; + disclaimerListEntry: string[]; +} + +export interface AlgorithmEntry { + name: string; + description: string; + link: string; + +} diff --git a/src/app/pages/algorithms/information/information.scss b/src/app/pages/algorithms/information/information.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/algorithms/information/information.ts b/src/app/pages/algorithms/information/information.ts new file mode 100644 index 0000000..c48af5a --- /dev/null +++ b/src/app/pages/algorithms/information/information.ts @@ -0,0 +1,19 @@ +import {Component, Input} from '@angular/core'; +import {TranslatePipe} from "@ngx-translate/core"; +import {UrlConstants} from "../../../constants/UrlConstants"; +import {AlgorithmInformation} from './information.models'; + +@Component({ + selector: 'app-information', + imports: [ + TranslatePipe + ], + templateUrl: './information.html', + styleUrl: './information.scss', +}) +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 e6866d4..660789a 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -3,23 +3,7 @@ {{ 'PATHFINDING.TITLE' | translate }} -
-

{{ 'PATHFINDING.EXPLANATION.TITLE' | translate }}

- -

- Dijkstra {{ 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION' | translate }} - Wikipedia -

- -

- A* {{ 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION' | translate}} - Wikipedia -

- -

- {{ 'PATHFINDING.EXPLANATION.NOTE' | translate}} {{ 'PATHFINDING.EXPLANATION.DISCLAIMER' | translate}} -

-
+
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts index 9f14bb3..2b30454 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts @@ -13,6 +13,8 @@ import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MAX_RA import {PathfindingService} from './service/pathfinding.service'; import {UrlConstants} from '../../../constants/UrlConstants'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; +import {Information} from '../information/information'; +import {AlgorithmInformation} from '../information/information.models'; enum NodeType { Start = 'start', @@ -37,7 +39,8 @@ interface GridPos { row: number; col: number } MatCard, MatCardHeader, MatCardTitle, - MatCardContent + MatCardContent, + Information ], templateUrl: './pathfinding.component.html', styleUrls: ['./pathfinding.component.scss'] @@ -50,6 +53,25 @@ export class PathfindingComponent implements AfterViewInit { readonly MIN_GRID_SIZE = MIN_GRID_SIZE; readonly MAX_GRID_SIZE = MAX_GRID_SIZE; + algoInformation: AlgorithmInformation = { + title: 'PATHFINDING.EXPLANATION.TITLE', + entries: [ + { + name: 'Dijkstra', + description: 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION', + link: UrlConstants.DIJKSTRA_WIKI + }, + { + name: 'A*', + description: 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION', + link: UrlConstants.ASTAR_WIKI + } + ], + disclaimer: 'PATHFINDING.EXPLANATION.DISCLAIMER', + disclaimerBottom: '', + disclaimerListEntry: [] + }; + @ViewChild('gridCanvas', { static: true }) canvas!: ElementRef; diff --git a/src/app/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/service/algorithms.service.ts index 6fd5b0c..f834c16 100644 --- a/src/app/pages/algorithms/service/algorithms.service.ts +++ b/src/app/pages/algorithms/service/algorithms.service.ts @@ -20,6 +20,12 @@ export class AlgorithmsService { title: 'ALGORITHM.SORTING.TITLE', description: 'ALGORITHM.SORTING.DESCRIPTION', routerLink: RouterConstants.SORTING.LINK + }, + { + id: 'gameOfLife', + title: 'ALGORITHM.GOL.TITLE', + description: 'ALGORITHM.GOL.DESCRIPTION', + routerLink: RouterConstants.GOL.LINK } ]; diff --git a/src/app/pages/algorithms/sorting/sorting.component.html b/src/app/pages/algorithms/sorting/sorting.component.html index 91827b3..506706b 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.html +++ b/src/app/pages/algorithms/sorting/sorting.component.html @@ -4,47 +4,13 @@ {{ 'SORTING.TITLE' | translate }} -
-

{{ 'SORTING.EXPLANATION.TITLE' | translate }}

- -

- Bubble Sort {{ 'SORTING.EXPLANATION.BUBBLE_SORT_EXPLANATION' | translate }} - Wikipedia -

- -

- Cocktail Sort {{ 'SORTING.EXPLANATION.COCKTAIL_SORT_EXPLANATION' | translate}} - Wikipedia -

- -

- Quick Sort {{ 'SORTING.EXPLANATION.QUICK_SORT_EXPLANATION' | translate}} - Wikipedia -

- -

- Heap Sort {{ 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION' | translate}} - Wikipedia -

- -

- {{ 'SORTING.EXPLANATION.NOTE' | translate}} {{ 'SORTING.EXPLANATION.DISCLAIMER' | translate}} -

-
    -
  • {{ 'SORTING.EXPLANATION.DISCLAIMER_1' | translate}}
  • -
  • {{ 'SORTING.EXPLANATION.DISCLAIMER_2' | translate}}
  • -
  • {{ 'SORTING.EXPLANATION.DISCLAIMER_3' | translate}}
  • -
-

- {{ 'SORTING.EXPLANATION.DISCLAIMER_4' | translate}} -

-
+
{{ 'SORTING.ALGORITHM' | translate }} - @for (algo of availableAlgorithms; track algo.value) { - {{ algo.name }} + @for (algo of algoInformation.entries; track algo.name) { + {{ algo.name }} } diff --git a/src/app/pages/algorithms/sorting/sorting.component.ts b/src/app/pages/algorithms/sorting/sorting.component.ts index fac1cff..6fdebaa 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.ts +++ b/src/app/pages/algorithms/sorting/sorting.component.ts @@ -11,11 +11,12 @@ import {SortData, SortSnapshot} from './sorting.models'; import { FormsModule } from '@angular/forms'; import {MatInput} from '@angular/material/input'; import {UrlConstants} from '../../../constants/UrlConstants'; -import {MIN} from '@angular/forms/signals'; +import {AlgorithmInformation} from '../information/information.models'; +import {Information} from '../information/information'; @Component({ selector: 'app-sorting', standalone: true, - imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput], + imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput, Information], templateUrl: './sorting.component.html', styleUrls: ['./sorting.component.scss'] }) @@ -27,21 +28,46 @@ export class SortingComponent implements OnInit { readonly MAX_ARRAY_SIZE: number = 200; readonly MIN_ARRAY_SIZE: number = 20; + algoInformation: AlgorithmInformation = { + title: 'SORTING.EXPLANATION.TITLE', + entries: [ + { + name: 'Bubble Sort', + description: 'SORTING.EXPLANATION.BUBBLE_SORT_EXPLANATION', + link: UrlConstants.BUBBLE_SORT_WIKI + }, + { + name: 'Cocktail Sort', + description: 'SORTING.EXPLANATION.COCKTAIL_SORT_EXPLANATION', + link: UrlConstants.SHAKE_SORT_WIKI + }, + { + name: 'Quick Sort', + description: 'SORTING.EXPLANATION.QUICK_SORT_EXPLANATION', + link: UrlConstants.QUICK_SORT_WIKI + }, + { + name: 'Heap Sort', + description: 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION', + link: UrlConstants.HEAP_SORT_WIKI + } + ], + disclaimer: 'SORTING.EXPLANATION.DISCLAIMER', + disclaimerBottom: 'SORTING.EXPLANATION.DISCLAIMER_4', + disclaimerListEntry: [ + 'SORTING.EXPLANATION.DISCLAIMER_1', + 'SORTING.EXPLANATION.DISCLAIMER_2', + 'SORTING.EXPLANATION.DISCLAIMER_3' + ] + }; + private timeoutIds: number[] = []; sortArray: SortData[] = []; unsortedArrayCopy: SortData[] = []; arraySize = 50; maxArrayValue = 100; animationSpeed = 50; // Milliseconds per step - - // Placeholder for available sorting algorithms - availableAlgorithms: { name: string; value: string }[] = [ - { name: 'Bubble Sort', value: 'bubbleSort' }, - { name: 'Cocktail Sort', value: 'cocktailSort' }, - { name: 'Quick Sort', value: 'quickSort' }, - { name: 'Heap Sort', value: 'heapSort' }, - ]; - selectedAlgorithm: string = this.availableAlgorithms[0].value; + selectedAlgorithm: string = this.algoInformation.entries[0].name; executionTime = 0; ngOnInit(): void { @@ -93,22 +119,22 @@ export class SortingComponent implements OnInit { let snapshots: SortSnapshot[] = []; switch (this.selectedAlgorithm) { - case 'bubbleSort': + case 'Bubble Sort': snapshots = this.sortingService.bubbleSort(this.sortArray); break; - case 'quickSort': + case 'Quick Sort': snapshots = this.sortingService.quickSort(this.sortArray); break; - case 'heapSort': + case 'Heap Sort': snapshots = this.sortingService.heapSort(this.sortArray); break; - case 'cocktailSort': + case 'Cocktail Sort': snapshots = this.sortingService.cocktailSort(this.sortArray); break; } const endTime = performance.now(); - this.executionTime = parseFloat((endTime - startTime).toFixed(4)); + this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4)); console.log(snapshots.length); this.animateSorting(snapshots); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 72d2b97..9c28066 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -313,7 +313,6 @@ "TITLE": "Algorithmen", "DIJKSTRA_EXPLANATION": " findet garantiert den kürzesten Weg, wenn alle Kantenkosten nicht-negativ sind. Vorteil: optimal und ohne Heuristik. Nachteil: besucht oft sehr viele Knoten (kann bei großen Grids langsamer wirken).", "ASTAR_EXPLANATION": " erweitert Dijkstra um eine Heuristik (z.B. Manhattan-Distanz) und kann dadurch wesentlich zielgerichteter suchen. Vorteil: oft deutlich schneller bei guter Heuristik; bei zulässiger Heuristik bleibt der Weg optimal. Nachteil: hängt stark von der Heuristik ab (schlechte Heuristik ≈ Dijkstra).", - "NOTE": "HINWEIS", "DISCLAIMER": "Diese A*-Implementierung ist bewusst einfach gehalten. Es wird nur in vier Richtungen gegangen und jeder Schritt kostet 1. Die Heuristik ist minimal und dient nur dazu, das Prinzip von A* gegenüber Dijkstra zu demonstrieren. Ziel ist nicht ein optimaler oder produktionsreifer A*-Algorithmus, sondern eine anschauliche Visualisierung, wie Heuristiken die Suche beschleunigen können." }, "ALERT": { @@ -336,7 +335,6 @@ "QUICK_SORT_EXPLANATION": "folgt dem \"Teile und Herrsche\"-Prinzip. Ein \"Pivot\"-Element wird gewählt, und das Array wird in zwei Hälften geteilt: Elemente kleiner als das Pivot und Elemente größer als das Pivot. Vorteil: Im Durchschnitt einer der schnellsten Sortieralgorithmen (O(n log n)); benötigt keinen zusätzlichen Speicher (In-Place). Nachteil: Im schlechtesten Fall (Worst Case) langsam (O(n²)), wenn das Pivot ungünstig gewählt wird. Ist nicht stabil (ändert Reihenfolge gleicher Elemente).", "HEAP_SORT_EXPLANATION": "organisiert die Daten zunächst in einer speziellen Baumstruktur (Binary Heap). Das größte Element (die Wurzel) wird entnommen und ans Ende sortiert, dann wird der Baum repariert. Vorteil: Garantiert eine schnelle Laufzeit von O(n log n), selbst im schlechtesten Fall. Benötigt fast keinen zusätzlichen Speicher. Nachteil: In der Praxis oft etwas langsamer als Quick Sort, da die Sprünge im Speicher (Heap-Struktur) den CPU-Cache schlechter nutzen.", "COCKTAIL_SORT_EXPLANATION" : "(auch Shaker Sort) ist eine Erweiterung des Bubble Sort. Statt nur von links nach rechts zu gehen, wechselt er bei jedem Durchlauf die Richtung und schiebt abwechselnd das größte Element nach rechts und das kleinste nach links. Vorteil: Schneller als Bubble Sort, da kleine Elemente am Ende schneller nach vorne wandern (\"Schildkröten-Problem\" gelöst). Nachteil: Bleibt in der Laufzeitklasse O(n²), also für große Datenmengen ineffizient.", - "NOTE": "HINWEIS", "DISCLAIMER": "Die Wahl des \"besten\" Sortieralgorithmus hängt stark von den Daten und den Rahmenbedingungen ab. In der Informatik betrachtet man oft drei Szenarien:", "DISCLAIMER_1": "Best Case: Die Daten sind schon fast sortiert (hier glänzt z.B. Bubble Sort).", "DISCLAIMER_2": "Average Case: Der statistische Normalfall.", @@ -344,6 +342,9 @@ "DISCLAIMER_4": "Zusätzlich gibt es fast immer einen Time-Space Trade-off (Zeit-Speicher-Kompromiss): Algorithmen, die extrem schnell sind (wie Merge Sort), benötigen oft viel zusätzlichen Arbeitsspeicher. Algorithmen, die direkt im vorhandenen Speicher arbeiten (wie Heap Sort), sparen Platz, sind aber manchmal komplexer oder minimal langsamer. Es gibt also keine \"One-Size-Fits-All\"-Lösung." } }, + "GOL": { + "TITLE": "Conway's Spiel des Lebens" + }, "ALGORITHM": { "TITLE": "Algorithmen", "PATHFINDING": { @@ -353,7 +354,11 @@ "SORTING": { "TITLE": "Sortierung", "DESCRIPTION": "Visualisierung verschiedener Sortieralgorithmen." - - } + }, + "GOL": { + "TITLE": "Conway's Game of Life", + "DESCRIPTION": "Das 'Spiel des Lebens' ist ein vom Mathematiker John Horton Conway 1970 entworfenes Spiel." + }, + "NOTE": "HINWEIS" } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 181aecf..feeea1f 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -313,7 +313,6 @@ "TITLE": "Algorithms", "DIJKSTRA_EXPLANATION": " is guaranteed to find the shortest path if all edge costs are non-negative. Advantage: optimal and without heuristics. Disadvantage: often visits a large number of nodes (can be slower for large grids).", "ASTAR_EXPLANATION": " extends Dijkstra with a heuristic (e.g. Manhattan distance) and can therefore search in a much more targeted manner. Advantage: often significantly faster with good heuristics; with permissible heuristics, the path remains optimal. Disadvantage: highly dependent on heuristics (poor heuristics ≈ Dijkstra).", - "NOTE": "Note", "DISCLAIMER": "This A* implementation is deliberately kept simple. It only moves in four directions and each step costs 1. The heuristic is minimal and only serves to demonstrate the principle of A* compared to Dijkstra. The goal is not an optimal or production-ready A* algorithm, but a clear visualisation of how heuristics can speed up the search." }, "ALERT": { @@ -335,7 +334,6 @@ "BUBBLE_SORT_EXPLANATION": "repeatedly compares adjacent elements and swaps them if they are in the wrong order. The largest element \"bubbles\" to the end of the list like an air bubble. Advantage: Extremely simple to understand and implement; detects already sorted lists very quickly. Disadvantage: Very inefficient for large lists (runtime O(n²)). Rarely used in practice.", "QUICK_SORT_EXPLANATION": "follows the \"divide and conquer\" principle. A \"pivot\" element is selected, and the array is divided into two halves: elements smaller than the pivot and elements larger than the pivot. Advantage: On average one of the fastest sorting algorithms (O(n log n)); requires no additional memory (in-place). Disadvantage: Slow in the worst case (O(n²)) if the pivot is chosen poorly. Is not stable (changes order of equal elements).", "HEAP_SORT_EXPLANATION": "organizes the data initially into a special tree structure (Binary Heap). The largest element (the root) is extracted and sorted to the end, then the tree is repaired. Advantage: Guarantees a fast runtime of O(n log n), even in the worst case. Requires almost no additional memory. Disadvantage: Often slightly slower than Quick Sort in practice because the jumps in memory (heap structure) utilize the CPU cache less effectively.", - "NOTE": "NOTE", "DISCLAIMER": "The choice of the \"best\" sorting algorithm depends heavily on the data and the constraints. In computer science, three scenarios are often considered:", "DISCLAIMER_1": "Best Case: The data is already nearly sorted (Bubble Sort shines here, for example).", "DISCLAIMER_2": "Average Case: The statistical norm.", @@ -343,6 +341,9 @@ "DISCLAIMER_4": "Additionally, there is almost always a Time-Space Trade-off: Algorithms that are extremely fast (like Merge Sort) often require a lot of additional working memory. Algorithms that work directly in existing memory (like Heap Sort) save space but are sometimes more complex or slightly slower. Thus, there is no \"one-size-fits-all\" solution." } }, + "GOL": { + "TITLE": "Conway's Game of Life" + }, "ALGORITHM": { "TITLE": "Algorithms", "PATHFINDING": { @@ -352,6 +353,11 @@ "SORTING": { "TITLE": "Sorting", "DESCRIPTION": "Visualizing various sorting algorithms." - } + }, + "GOL": { + "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" } } From a22dd178692a57421daa2993fddb05ef8416930e Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Fri, 6 Feb 2026 11:52:17 +0100 Subject: [PATCH 2/6] Created default build up - next make grid be a own component with a lot of callbacks - after this start the game implementtion --- .../algorithms/conway-gol/conway-gol.html | 32 ++++ .../conway-gol/conway-gol.models.ts | 11 ++ .../pages/algorithms/conway-gol/conway-gol.ts | 142 ++++++++++++++++-- .../pathfinding/pathfinding.component.html | 3 +- .../pathfinding/pathfinding.component.scss | 13 -- src/assets/i18n/de.json | 12 +- src/assets/i18n/en.json | 12 +- src/styles.scss | 21 +++ 8 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 src/app/pages/algorithms/conway-gol/conway-gol.models.ts diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html index 43c212e..228e8f7 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -4,5 +4,37 @@ +
+ +
+
+ + {{ 'PATHFINDING.GRID_HEIGHT' | translate }} + + + + {{ 'PATHFINDING.GRID_WIDTH' | 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 new file mode 100644 index 0000000..a3300a6 --- /dev/null +++ b/src/app/pages/algorithms/conway-gol/conway-gol.models.ts @@ -0,0 +1,11 @@ +export interface Node { + row: number; + col: number; +} + +export const DEFAULT_GRID_ROWS = 100; +export const DEFAULT_GRID_COLS = 100; + +export const MIN_GRID_SIZE = 20; +export const MAX_GRID_SIZE = 200; +export const MAX_GRID_PX = 1000; diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index b0ecc02..a80abc4 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -1,9 +1,14 @@ -import { Component } from '@angular/core'; +import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; import {TranslatePipe} from "@ngx-translate/core"; import {UrlConstants} from '../../../constants/UrlConstants'; import {Information} from '../information/information'; import {AlgorithmInformation} from '../information/information.models'; +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'; @Component({ selector: 'app-conway-gol', @@ -13,31 +18,136 @@ import {AlgorithmInformation} from '../information/information.models'; MatCardHeader, MatCardTitle, TranslatePipe, - Information + Information, + MatButton, + MatIcon, + MatFormField, + MatInput, + MatLabel, + ReactiveFormsModule, + FormsModule ], templateUrl: './conway-gol.html', styleUrl: './conway-gol.scss', }) -export class ConwayGol { - - protected readonly UrlConstants = UrlConstants; +export class ConwayGol implements AfterViewInit { algoInformation: AlgorithmInformation = { - title: 'PATHFINDING.EXPLANATION.TITLE', + title: 'GOL.EXPLANATION.TITLE', entries: [ { - name: 'Dijkstra', - description: 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION', - link: UrlConstants.DIJKSTRA_WIKI - }, - { - name: 'A*', - description: 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION', - link: UrlConstants.ASTAR_WIKI + name: '', + description: 'GOL.EXPLANATION.EXPLANATION', + link: UrlConstants.CONWAYS_WIKI } ], - disclaimer: 'PATHFINDING.EXPLANATION.DISCLAIMER', + disclaimer: 'GOL.EXPLANATION.DISCLAIMER', disclaimerBottom: '', - disclaimerListEntry: [] + 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 readonly MIN_GRID_SIZE = MIN_GRID_SIZE; + protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE; + nodeSize = 10; + grid: Node[][] = []; + + @ViewChild('gridCanvas', { static: true }) + canvas!: ElementRef; + private ctx!: CanvasRenderingContext2D; + + ngAfterViewInit(): void { + this.ctx = this.getContextOrThrow(); + this.applyGridSize(); + } + + applyGridSize(): void { + this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS); + this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS); + 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(); + + } + + private initializeGrid(): void { + this.grid = this.createEmptyGrid(); + this.drawGrid(); + } + + private createEmptyGrid(): Node[][] { + const grid: Node[][] = []; + + 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)); + } + grid.push(currentRow); + } + + return grid; + } + + private createNode(row: number, col: number): Node { + return { + row, + col + }; + } + + private 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]); + } + } + } + + private drawNode(node: Node): void { + this.ctx.fillStyle = this.getNodeColor(); + 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 { + return 'lightgray'; + } + + private getContextOrThrow(): CanvasRenderingContext2D { + const ctx = this.canvas.nativeElement.getContext('2d'); + if (!ctx) { + throw new Error('CanvasRenderingContext2D not available.'); + } + return ctx; + } + + private clampGridSize(value: number, fallback: number): number { + const parsed = Math.floor(Number(value)); + const safe = Number.isFinite(parsed) ? parsed : fallback; + return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE); + } + + private computeNodeSize(rows: number, cols: number): number { + const sizeByWidth = Math.floor(MAX_GRID_PX / cols); + const sizeByHeight = Math.floor(MAX_GRID_PX / 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; + } + } diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index 660789a..6f3a695 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -71,4 +71,5 @@
-
+ + diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.scss b/src/app/pages/algorithms/pathfinding/pathfinding.component.scss index 3e30d18..fdfd40e 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.scss +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.scss @@ -21,13 +21,6 @@ } } -.grid-size { - display: flex; - gap: 0.75rem; - align-items: center; - flex-wrap: wrap; -} - .grid-field { width: 150px; } @@ -54,9 +47,3 @@ &.path { background-color: gold; } } } - -canvas { - border: 1px solid #ccc; - display: block; - max-width: 100%; -} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 9c28066..3dacb72 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -343,7 +343,17 @@ } }, "GOL": { - "TITLE": "Conway's Spiel des Lebens" + "TITLE": "Conway's Spiel des Lebens", + "START": "Starten", + "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).", + "DISCLAIMER": "Nach Conways ursprünglicher Regel lebt eine Zelle in der nächsten Runde, wenn zuvor in ihrer 3x3-Umgebung insgesamt genau drei Zellen leben, wobei sie selbst nur bei Bedarf mitgezählt wird, das heißt:", + "DISCLAIMER_1": "Eine lebende Zelle lebt auch in der Folgegeneration, wenn sie entweder zwei oder drei lebende Nachbarn hat.", + "DISCLAIMER_2": "Eine tote Zelle „wird geboren“ (lebt in der Folgegeneration), wenn sie genau drei lebende Nachbarn hat.", + "DISCLAIMER_3": "Eine lebende Zelle „stirbt“ (ist in der Folgegeneration tot), wenn sie weniger als zwei (Vereinsamung) oder mehr als drei (Übervölkerung) lebende Nachbarn hat.", + "DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat." + } }, "ALGORITHM": { "TITLE": "Algorithmen", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index feeea1f..b560660 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -342,7 +342,17 @@ } }, "GOL": { - "TITLE": "Conway's Game of Life" + "TITLE": "Conway's Game of Life", + "START": "Start", + "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).", + "DISCLAIMER": "Nach Conways ursprünglicher Regel lebt eine Zelle in der nächsten Runde, wenn zuvor in ihrer 3x3-Umgebung insgesamt genau drei Zellen leben, wobei sie selbst nur bei Bedarf mitgezählt wird, das heißt:", + "DISCLAIMER_1": "Eine lebende Zelle lebt auch in der Folgegeneration, wenn sie entweder zwei oder drei lebende Nachbarn hat.", + "DISCLAIMER_2": "Eine tote Zelle „wird geboren“ (lebt in der Folgegeneration), wenn sie genau drei lebende Nachbarn hat.", + "DISCLAIMER_3": "Eine lebende Zelle „stirbt“ (ist in der Folgegeneration tot), wenn sie weniger als zwei (Vereinsamung) oder mehr als drei (Übervölkerung) lebende Nachbarn hat.", + "DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat." + } }, "ALGORITHM": { "TITLE": "Algorithms", diff --git a/src/styles.scss b/src/styles.scss index 3a12a96..3321d1d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -231,3 +231,24 @@ a { margin-left: 0.25rem; } } + +.controls-panel { + display: flex; + gap: 10px; + margin-bottom: 20px; + align-items: center; + flex-wrap: wrap; +} + +.grid-size { + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; +} + +canvas { + border: 1px solid #ccc; + display: block; + max-width: 100%; +} From 59148db2959314dd0e9bd3425197000da6f17d4d Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Fri, 6 Feb 2026 14:40:49 +0100 Subject: [PATCH 3/6] 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; +} From 3795090cea2faf25569f52d8813a87a1e2bb7266 Mon Sep 17 00:00:00 2001 From: Lobo Date: Fri, 6 Feb 2026 15:06:54 +0100 Subject: [PATCH 4/6] Refactor controls UI, styles; bump deps Consolidate and refactor component styling and markup: remove component-specific SCSS for Conway's Game of Life and Pathfinding and drop their styleUrls, rename per-component .controls blocks to .controls-panel in pathfinding and sorting templates, and move the outer container class onto the mat-card. Add global styles in src/styles.scss for .controls-panel, grid/form sizing, container max-width, and sorting visualization (bar states and transitions). Also update package-lock.json with minor/patch dependency bumps for Angular CLI/devkit/schematics/@schematics/angular, @modelcontextprotocol/sdk and several transitive packages. --- package-lock.json | 180 ++++++++++++++---- .../algorithms/conway-gol/conway-gol.scss | 0 .../pages/algorithms/conway-gol/conway-gol.ts | 1 - .../algorithms/information/information.ts | 1 - .../pathfinding/pathfinding.component.html | 18 +- .../pathfinding/pathfinding.component.scss | 16 -- .../pathfinding/pathfinding.component.ts | 1 - .../algorithms/sorting/sorting.component.html | 6 +- src/styles.scss | 56 +++++- 9 files changed, 212 insertions(+), 67 deletions(-) delete mode 100644 src/app/pages/algorithms/conway-gol/conway-gol.scss delete mode 100644 src/app/pages/algorithms/pathfinding/pathfinding.component.scss diff --git a/package-lock.json b/package-lock.json index c110d85..aa38707 100644 --- a/package-lock.json +++ b/package-lock.json @@ -306,13 +306,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.2.tgz", - "integrity": "sha512-PA3gkiFhHUuXd2XuP7yzKg/9N++bjw+uOl473KwIsMuZwMPhncKa4+mUYBaffDoPqaujZvjfo6mjtCBuiBv05w==", + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.3.tgz", + "integrity": "sha512-Ps7bRl5uOcM7WpNJHbSls/jz5/wAI0ldkTlKyiBFA7RtNeQIABAV+hvlw5DJuEb1Lo5hnK0hXj90AyZdOxzY+w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.1.2", + "@angular-devkit/core": "21.1.3", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.0.0", @@ -324,6 +324,34 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-eslint/builder": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz", @@ -565,19 +593,19 @@ } }, "node_modules/@angular/cli": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.2.tgz", - "integrity": "sha512-AHjXCBl2PEilMJct6DX3ih5Fl5PiKpNDIj0ViTyVh1YcfpYjt6NzhVlV2o++8VNPNH/vMcmf2551LZIDProXXA==", + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.3.tgz", + "integrity": "sha512-UPtDcpKyrKZRPfym9gTovcibPzl2O/Woy7B8sm45sAnjDH+jDUCcCvuIak7GpH47shQkC2J4yvnHZbD4c6XxcQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2101.2", - "@angular-devkit/core": "21.1.2", - "@angular-devkit/schematics": "21.1.2", + "@angular-devkit/architect": "0.2101.3", + "@angular-devkit/core": "21.1.3", + "@angular-devkit/schematics": "21.1.3", "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.25.2", - "@schematics/angular": "21.1.2", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.1.3", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.46.2", "ini": "6.0.0", @@ -600,6 +628,53 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.2101.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.3.tgz", + "integrity": "sha512-vKz8aPA62W+e9+pF6ct4CRDG/MjlIH7sWFGYkxPPRst2g46ZQsRkrzfMZAWv/wnt6OZ1OwyRuO3RW83EMhag8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.1.3", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/common": { "version": "21.1.2", "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.1.2.tgz", @@ -2388,13 +2463,13 @@ ] }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "dev": true, "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -2402,14 +2477,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -4019,14 +4095,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.2.tgz", - "integrity": "sha512-kxwxhCIUrj7DfzEtDSs/pi/w+aII/WQLpPfLgoQCWE8/95v60WnTfd1afmsXsFoxikKPxkwoPWtU2YbhSoX9MQ==", + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.3.tgz", + "integrity": "sha512-obJvWBhzRdsYL2msM4+8bQD21vFl3VxaVsuiq6iIfYsxhU5i2Iar2wM9NaRaIIqAYhZ8ehQQ/moB9BEbWvDCTw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.1.2", - "@angular-devkit/schematics": "21.1.2", + "@angular-devkit/core": "21.1.3", + "@angular-devkit/schematics": "21.1.3", "jsonc-parser": "3.3.1" }, "engines": { @@ -4035,6 +4111,34 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@sigstore/bundle": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", @@ -6314,11 +6418,14 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "dev": true, "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, "engines": { "node": ">= 16" }, @@ -6329,6 +6436,16 @@ "express": ">= 4.11" } }, + "node_modules/express-rate-limit/node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6712,12 +6829,11 @@ } }, "node_modules/hono": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", - "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.8.tgz", + "integrity": "sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.scss b/src/app/pages/algorithms/conway-gol/conway-gol.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index 05f956f..4f8d05b 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -30,7 +30,6 @@ interface GridPos { row: number; col: number } FormsModule ], templateUrl: './conway-gol.html', - styleUrl: './conway-gol.scss', }) export class ConwayGol implements AfterViewInit { diff --git a/src/app/pages/algorithms/information/information.ts b/src/app/pages/algorithms/information/information.ts index 3b3d589..f9ee088 100644 --- a/src/app/pages/algorithms/information/information.ts +++ b/src/app/pages/algorithms/information/information.ts @@ -1,6 +1,5 @@ import {Component, Input} from '@angular/core'; import {TranslatePipe} from "@ngx-translate/core"; -import {UrlConstants} from "../../../constants/UrlConstants"; import {AlgorithmInformation} from './information.models'; @Component({ diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index 42b19ba..ed364a0 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -6,18 +6,18 @@
-
+
-
+
-
+
{{ 'PATHFINDING.START_NODE' | translate }} {{ 'PATHFINDING.END_NODE' | translate }} @@ -25,8 +25,7 @@ {{ 'PATHFINDING.CLEAR_NODE' | translate }}
- -
+
{{ 'ALGORITHM.GRID_HEIGHT' | translate }} @@ -63,11 +62,10 @@ {{ 'PATHFINDING.VISITED' | translate }} {{ 'PATHFINDING.PATH' | translate }}
-
- -
-

{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}

-

{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms

+
+

{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}

+

{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms

+
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.scss b/src/app/pages/algorithms/pathfinding/pathfinding.component.scss deleted file mode 100644 index fa0e10c..0000000 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.scss +++ /dev/null @@ -1,16 +0,0 @@ -.controls { - display: flex; - flex-wrap: wrap; - gap: 1rem; - margin-bottom: 1rem; - align-items: center; - - mat-button-toggle-group { - border-radius: 4px; - overflow: hidden; - } -} - -.grid-field { - width: 150px; -} diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts index 6215a72..37837e1 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts @@ -43,7 +43,6 @@ interface GridPos { row: number; col: number } Information ], templateUrl: './pathfinding.component.html', - styleUrls: ['./pathfinding.component.scss'] }) export class PathfindingComponent implements AfterViewInit { private readonly pathfindingService = inject(PathfindingService); diff --git a/src/app/pages/algorithms/sorting/sorting.component.html b/src/app/pages/algorithms/sorting/sorting.component.html index 1a15631..504df1d 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.html +++ b/src/app/pages/algorithms/sorting/sorting.component.html @@ -1,5 +1,4 @@ -
- + {{ 'SORTING.TITLE' | translate }} @@ -40,7 +39,7 @@
-
+

{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms

@@ -56,4 +55,3 @@
-
diff --git a/src/styles.scss b/src/styles.scss index c01f329..fdba66c 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -110,6 +110,12 @@ a { transition: box-shadow 200ms ease, transform 200ms ease; + + &.container { + width: 100%; + max-width: 1200px; + padding: 20px; + } } .mat-mdc-card::before { @@ -239,10 +245,21 @@ a { .controls-panel { display: flex; - gap: 10px; - margin-bottom: 20px; + gap: 1rem; + margin-bottom: 1rem; align-items: center; flex-wrap: wrap; + margin-top: 10px; + font-size: 0.9em; + + mat-button-toggle-group { + border-radius: 4px; + overflow: hidden; + } + + mat-form-field { + width: 200px; + } } .grid-size { @@ -250,6 +267,10 @@ a { gap: 0.75rem; align-items: center; flex-wrap: wrap; + + .grid-field { + width: 150px; + } } canvas { @@ -288,3 +309,34 @@ canvas { flex-direction: column; margin-bottom: 1rem; } + +/* Sorting Visualization */ +.sorting-visualization-area { + display: flex; + align-items: flex-end; + height: 300px; /* Max height for bars */ + border-bottom: 1px solid #ccc; + margin-bottom: 20px; + gap: 1px; + background-color: #f0f0f0; + + .sorting-bar { + flex-grow: 1; + background-color: #424242; /* Default unsorted color */ + transition: height 0.05s ease-in-out, background-color 0.05s ease-in-out; + width: 10px; /* Default width, flex-grow will adjust */ + min-width: 1px; /* Ensure bars are always visible */ + + &.unsorted { + background-color: #424242; + } + + &.comparing { + background-color: #ffeb3b; /* Yellow for comparing */ + } + + &.sorted { + background-color: #4caf50; /* Green for sorted */ + } + } +} \ No newline at end of file From 930f0110b05f508f2346af015754a797a8eca8d1 Mon Sep 17 00:00:00 2001 From: Lobo Date: Fri, 6 Feb 2026 20:59:56 +0100 Subject: [PATCH 5/6] 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 with 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. --- .../algorithms/conway-gol/conway-gol.html | 18 +- .../pages/algorithms/conway-gol/conway-gol.ts | 271 ++----- .../pathfinding/pathfinding.component.html | 48 +- .../pathfinding/pathfinding.component.ts | 715 +++++++----------- .../components/generic-grid/generic-grid.html | 1 + .../components/generic-grid/generic-grid.scss | 0 .../components/generic-grid/generic-grid.ts | 213 ++++++ 7 files changed, 581 insertions(+), 685 deletions(-) create mode 100644 src/app/shared/components/generic-grid/generic-grid.html create mode 100644 src/app/shared/components/generic-grid/generic-grid.scss create mode 100644 src/app/shared/components/generic-grid/generic-grid.ts diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html index c94f1cf..185995a 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -33,8 +33,7 @@ [(ngModel)]="gridRows" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (blur)="applyGridSize()" - (keyup.enter)="applyGridSize()" + (ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" /> @@ -45,8 +44,7 @@ [(ngModel)]="gridCols" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (blur)="applyGridSize()" - (keyup.enter)="applyGridSize()" + (ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" /> @@ -67,6 +65,16 @@ {{ 'GOL.DEAD' | translate }}
- + diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index 4f8d05b..5d6d524 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, ViewChild} from '@angular/core'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; import {TranslatePipe} from "@ngx-translate/core"; import {UrlConstants} from '../../../constants/UrlConstants'; @@ -9,8 +9,7 @@ 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, 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 } +import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid'; @Component({ selector: 'app-conway-gol', @@ -27,7 +26,8 @@ interface GridPos { row: number; col: number } MatInput, MatLabel, ReactiveFormsModule, - FormsModule + FormsModule, + GenericGridComponent ], templateUrl: './conway-gol.html', }) @@ -52,45 +52,68 @@ export class ConwayGol implements AfterViewInit { protected lifeSpeed = DEFAULT_TIME_PER_GENERATION; protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE; protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE; - nodeSize = 10; + protected readonly MAX_GRID_PX = MAX_GRID_PX; + grid: Node[][] = []; currentScenario: Scenario = 0; - @ViewChild('gridCanvas', { static: true }) - canvas!: ElementRef; - private ctx!: CanvasRenderingContext2D; - private lastCell: GridPos | null = null; - isDrawing = false; + @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; 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(); - }); + if (this.genericGridComponent) { + this.genericGridComponent.initializationFn = this.initializeConwayGrid; + this.genericGridComponent.createNodeFn = this.createConwayNode; + this.genericGridComponent.getNodeColorFn = this.getConwayNodeColor; + this.genericGridComponent.applySelectionFn = this.applyConwaySelection; + this.genericGridComponent.gridRows = this.gridRows; + this.genericGridComponent.gridCols = this.gridCols; + this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE; + this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE; + this.genericGridComponent.maxGridPx = this.MAX_GRID_PX; + this.genericGridComponent.initializeGrid(); + } } generate(scene: Scenario): void { this.currentScenario = scene; - this.initializeGrid(); + this.genericGridComponent.initializationFn = this.initializeConwayGrid; + this.genericGridComponent.initializeGrid(); } + applySpeed(): void { + this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION); + } + + // --- Callbacks for GenericGridComponent --- + createConwayNode = (row: number, col: number): Node => { + return { + row, + col, + alive: false + }; + }; + + getConwayNodeColor = (node: Node): string => { + if (node.alive) { + return 'black'; + } + return 'lightgray'; + }; + + applyConwaySelection = (pos: GridPos, grid: Node[][]): void => { + this.grid = grid; // Keep internal grid in sync + const node = grid[pos.row][pos.col]; + node.alive = !node.alive; // Toggle alive status + }; + + initializeConwayGrid = (grid: Node[][]): void => { + this.grid = grid; + if (this.currentScenario === Scenario.RANDOM) { + this.setupRandomLives(); + } + }; + + // --- Conway-specific logic (kept local) --- setupRandomLives(): void { for (let row = 0; row < this.gridRows; row++) { for (let col = 0; col < this.gridCols; col++) { @@ -99,189 +122,7 @@ export class ConwayGol implements AfterViewInit { } } - applyGridSize(): void { - this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS); - this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS); - 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(); - } - - 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(); - } - - private createEmptyGrid(): Node[][] { - const grid: Node[][] = []; - - 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, false)); - } - grid.push(currentRow); - } - - return grid; - } - - private createNode(row: number, col: number, alive: boolean): Node { - return { - row, - col, - alive - }; - } - - private 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]); - } - } - } - - private drawNode(node: Node): void { - 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(node: Node): string { - if (node.alive) - { - return 'black'; - } - return 'lightgray'; - } - - private getContextOrThrow(): CanvasRenderingContext2D { - const ctx = this.canvas.nativeElement.getContext('2d'); - if (!ctx) { - throw new Error('CanvasRenderingContext2D not available.'); - } - return ctx; - } - - private clampGridSize(value: number, fallback: number): number { - const parsed = Math.floor(Number(value)); - const safe = Number.isFinite(parsed) ? parsed : fallback; - return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE); - } - - private computeNodeSize(rows: number, cols: number): number { - const sizeByWidth = Math.floor(MAX_GRID_PX / cols); - const sizeByHeight = Math.floor(MAX_GRID_PX / 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; - } - - //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); - } - + // --- Other methods --- 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/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index ed364a0..36d9ccd 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -29,29 +29,25 @@
{{ 'ALGORITHM.GRID_HEIGHT' | translate }} - - + {{ 'ALGORITHM.GRID_WIDTH' | translate }} - - +
@@ -68,6 +64,16 @@
- +
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts index 37837e1..98e99ca 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, inject, ViewChild} from '@angular/core'; import {CommonModule} from '@angular/common'; import {FormsModule} from '@angular/forms'; @@ -15,6 +15,7 @@ import {UrlConstants} from '../../../constants/UrlConstants'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {Information} from '../information/information'; import {AlgorithmInformation} from '../information/information.models'; +import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid'; enum NodeType { Start = 'start', @@ -23,8 +24,6 @@ enum NodeType { None = 'none' } -interface GridPos { row: number; col: number } - @Component({ selector: 'app-pathfinding', standalone: true, @@ -40,7 +39,8 @@ interface GridPos { row: number; col: number } MatCardHeader, MatCardTitle, MatCardContent, - Information + Information, + GenericGridComponent ], templateUrl: './pathfinding.component.html', }) @@ -51,6 +51,7 @@ export class PathfindingComponent implements AfterViewInit { readonly NodeType = NodeType; readonly MIN_GRID_SIZE = MIN_GRID_SIZE; readonly MAX_GRID_SIZE = MAX_GRID_SIZE; + readonly MAX_GRID_PX = MAX_GRID_PX; algoInformation: AlgorithmInformation = { title: 'PATHFINDING.EXPLANATION.TITLE', @@ -71,24 +72,15 @@ export class PathfindingComponent implements AfterViewInit { disclaimerListEntry: [] }; - @ViewChild('gridCanvas', { static: true }) - canvas!: ElementRef; - - private ctx!: CanvasRenderingContext2D; - gridRows = DEFAULT_GRID_ROWS; gridCols = DEFAULT_GRID_COLS; - nodeSize = 10; grid: Node[][] = []; startNode: Node | null = null; endNode: Node | null = null; selectedNodeType: NodeType = NodeType.None; - - isDrawing = false; - private lastCell: GridPos | null = null; - private shouldAddWall = true; + private shouldAddWall = true; // Moved here animationSpeed = 3; pathLength = "0"; @@ -96,58 +88,79 @@ export class PathfindingComponent implements AfterViewInit { private timeoutIds: number[] = []; + @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; + ngAfterViewInit(): void { - this.ctx = this.getContextOrThrow(); - this.applyGridSize(true); - - 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(); - }); + // Canvas logic is now handled by GenericGridComponent + // Ensure genericGridComponent is initialized + if (this.genericGridComponent) { + this.genericGridComponent.initializationFn = this.initializePathfindingGrid; + this.genericGridComponent.createNodeFn = this.createPathfindingNode; + this.genericGridComponent.getNodeColorFn = this.getPathfindingNodeColor; + this.genericGridComponent.applySelectionFn = this.applyPathfindingSelection; + this.genericGridComponent.gridRows = this.gridRows; + this.genericGridComponent.gridCols = this.gridCols; + this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE; + this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE; + this.genericGridComponent.maxGridPx = MAX_GRID_PX; + this.genericGridComponent.applyGridSize(); // Trigger initial grid setup + } + this.createCase({withWalls: true, scenario: "normal"}); } - applyGridSize(skipReset?: boolean): void { - this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS); - this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS); - this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols); - this.resizeCanvas(); + // --- Callbacks for GenericGridComponent --- + createPathfindingNode = (row: number, col: number): Node => { + return { + row, + col, + isStart: false, + isEnd: false, + isWall: false, + isVisited: false, + isPath: false, + distance: Infinity, + previousNode: null, + hScore: 0, + fScore: Infinity, + }; + }; - if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length) - { - this.drawGrid(); - return; + getPathfindingNodeColor = (node: Node): string => { + if (node.isStart) return 'green'; + if (node.isEnd) return 'red'; + if (node.isPath) return 'gold'; + if (node.isVisited) return 'skyblue'; + if (node.isWall) return 'black'; + return 'lightgray'; + }; + + applyPathfindingSelection = (pos: GridPos, grid: Node[][]): void => { + this.grid = grid; // Keep internal grid in sync + const node = grid[pos.row][pos.col]; + + // Determine if we should add or remove a wall + if (this.selectedNodeType === NodeType.Wall && this.genericGridComponent.isDrawing && this.genericGridComponent['lastCell'] === null) { + this.shouldAddWall = !node.isWall; } - if (skipReset) { - this.initializeGrid({withWalls: true, scenario: 'normal'}); - this.drawGrid(); - return; + switch (this.selectedNodeType) { + case NodeType.Start: + this.trySetStart(node); + break; + + case NodeType.End: + this.trySetEnd(node); + break; + + case NodeType.Wall: + this.tryToggleWall(node, this.shouldAddWall); + break; + + case NodeType.None: + this.tryClearNode(node); + break; } - - this.createCase({withWalls: true, scenario: 'normal'}); - } - - createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void - { - this.stopAnimations(); - this.initializeGrid({withWalls, scenario}); - this.drawGrid(); - } + }; visualize(algorithm: string): void { if (!this.ensureStartAndEnd()) { @@ -166,13 +179,13 @@ export class PathfindingComponent implements AfterViewInit { this.grid[this.startNode!.row][this.startNode!.col], this.grid[this.endNode!.row][this.endNode!.col] ); - break; + break; case 'astar': result = this.pathfindingService.aStar( this.grid, this.grid[this.startNode!.row][this.startNode!.col], this.grid[this.endNode!.row][this.endNode!.col] ); - break; + break; } if (!result) @@ -195,320 +208,19 @@ export class PathfindingComponent implements AfterViewInit { this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder); } - // Mouse interactions - private onMouseDown(event: MouseEvent): void { - this.stopAnimations(); - this.clearPath(); - const pos = this.getGridPosition(event); - if (!pos) { - return; - } - - this.shouldAddWall = this.shouldStartWallStroke(pos); - - 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; - } - - private applySelectionAt(pos: GridPos): void { - const node = this.grid[pos.row][pos.col]; - - switch (this.selectedNodeType) { - case NodeType.Start: - this.trySetStart(node); - break; - - case NodeType.End: - this.trySetEnd(node); - break; - - case NodeType.Wall: - this.tryToggleWall(node, this.shouldAddWall); - break; - - case NodeType.None: - this.tryClearNode(node); - break; - } - - this.lastCell = pos; - this.drawNode(node); - } - - // Grid init - private initializeGrid({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void { - this.grid = this.createEmptyGrid(); - - const { start, end } = this.getScenarioStartEnd(scenario); + initializePathfindingGrid = (grid: Node[][]): void => { + this.grid = grid; // Update the component's grid reference + const {start, end} = this.getScenarioStartEnd('normal'); // Default scenario this.startNode = this.grid[start.row][start.col]; this.endNode = this.grid[end.row][end.col]; this.startNode.isStart = true; this.endNode.isEnd = true; - if (withWalls) { - this.placeDefaultDiagonalWall(scenario); - } - } + this.placeDefaultDiagonalWall('normal'); + }; - private createEmptyGrid(): Node[][] { - const grid: Node[][] = []; - - 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)); - } - grid.push(currentRow); - } - - return grid; - } - - private createNode(row: number, col: number): Node { - return { - row, - col, - isStart: false, - isEnd: false, - isWall: false, - isVisited: false, - isPath: false, - distance: Infinity, - previousNode: null, - hScore: 0, - fScore: Infinity, - }; - } - - private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } { - if (scenario === 'edge') { - return { - start: { row: 0, col: 0 }, - end: { row: this.gridRows - 1, col: this.gridCols - 1 } - }; - } - else if (scenario === 'random') { - return this.createRandomStartEndPosition(); - } - else { - // normal: mid-left -> mid-right - const midRow = Math.floor(this.gridRows / 2); - return { - start: { row: midRow, col: 0 }, - end: { row: midRow, col: this.gridCols - 1 } - }; - } - } - - private createRandomStartEndPosition() { - const midCol = Math.floor(this.gridCols / 2); - - const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1); - const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1); - - const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1); - let endCol: number; - - if (startCol <= midCol) { - endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1); - } else { - endCol = this.randomIntFromInterval(0, midCol); - } - - return { - start: {row: startRow, col: startCol}, - end: {row: endRow, col: endCol} - }; - } - - private placeDefaultDiagonalWall(scenario: 'normal' | 'edge' | 'random'): void { - if (scenario === 'edge') { - this.createDiagonalWall(); - } - else if (scenario === 'normal') { - this.createVerticalWall(); - } - else if (scenario === 'random') { - this.createRandomWalls(); - } - } - - private createRandomWalls(){ - const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows); - - for (let wall = 0; wall < maxNumberOfWalls; wall++) { - - const row: number = this.randomIntFromInterval(0, this.gridRows - 1); - const col: number = this.randomIntFromInterval(0, this.gridCols - 1); - - if (!this.isValidPosition(row, col)) { - wall--; - continue; - } - - const node = this.grid[row][col]; - if (node.isStart || node.isEnd) { - wall--; - continue; - } - - node.isWall = true; - } - - } - - private createVerticalWall() { - const height = this.gridRows; - const startCol = Math.floor(this.gridCols / 2); - - for (let i = 5; i < (height - 5); i++) { - const row = i; - - if (!this.isValidPosition(row, startCol)) { - continue; - } - - const node = this.grid[row][startCol]; - if (node.isStart || node.isEnd) { - continue; - } - - node.isWall = true; - } - - } - - private createDiagonalWall() { - // Diagonal-ish wall; avoids start/end - const len = Math.min(this.gridRows, this.gridCols); - const startCol = Math.floor((this.gridCols - len) / 2); - - for (let i = 0; i < Math.max(0, len - 10); i++) { - const row = len - i - 1; - const col = startCol + i; - - if (!this.isValidPosition(row, col)) { - continue; - } - - const node = this.grid[row][col]; - if (node.isStart || node.isEnd) { - continue; - } - - node.isWall = true; - } - } - -// Path state - private clearPath(): void { - for (let row = 0; row < this.gridRows; row++) { - for (let col = 0; col < this.gridCols; col++) { - const node = this.grid[row][col]; - node.isVisited = false; - node.isPath = false; - node.distance = Infinity; - node.previousNode = null; - } - } - this.drawGrid(); - } - - // Animation - private stopAnimations(): void { - for (const id of this.timeoutIds) { - clearTimeout(id); - } - this.timeoutIds = []; - } - - private animateAlgorithm(visited: Node[], path: Node[]): void { - for (let i = 0; i <= visited.length; i++) { - if (i === visited.length) { - const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i); - this.timeoutIds.push(id); - return; - } - - const node = visited[i]; - const id = globalThis.setTimeout(() => { - if (!node.isStart && !node.isEnd) { - node.isVisited = true; - this.drawNode(node); - } - }, this.animationSpeed * i); - - this.timeoutIds.push(id); - } - } - - private animateShortestPath(path: Node[]): void { - for (let i = 0; i < path.length; i++) { - const node = path[i]; - const id = globalThis.setTimeout(() => { - if (!node.isStart && !node.isEnd) { - node.isPath = true; - this.drawNode(node); - } - }, this.animationSpeed * i); - - this.timeoutIds.push(id); - } - } - - // Drawing - private 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]); - } - } - } - - private drawNode(node: Node): void { - 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(node: Node): string { - if (node.isStart) return 'green'; - if (node.isEnd) return 'red'; - if (node.isPath) return 'gold'; - if (node.isVisited) return 'skyblue'; - if (node.isWall) return 'black'; - return 'lightgray'; - } - - // Placement rules (readability helpers) + // --- Helper methods for node manipulation (kept local) --- private trySetStart(node: Node): void { if (!this.canBeStart(node)) { return; @@ -516,7 +228,7 @@ export class PathfindingComponent implements AfterViewInit { if (this.startNode) { this.startNode.isStart = false; - this.drawNode(this.startNode); + this.genericGridComponent.drawNode(this.startNode); // Redraw old start node } node.isStart = true; @@ -530,7 +242,7 @@ export class PathfindingComponent implements AfterViewInit { if (this.endNode) { this.endNode.isEnd = false; - this.drawNode(this.endNode); + this.genericGridComponent.drawNode(this.endNode); // Redraw old end node } node.isEnd = true; @@ -574,16 +286,197 @@ export class PathfindingComponent implements AfterViewInit { return !node.isStart && !node.isEnd; } - private shouldStartWallStroke(pos: GridPos): boolean { - if (this.selectedNodeType !== NodeType.Wall) { - return true; - } + // --- Grid manipulation for scenarios (kept local) --- + createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void { + this.stopAnimations(); + // Reinitialize grid through the generic component + this.genericGridComponent.initializationFn = (grid) => { + this.grid = grid; + const {start, end} = this.getScenarioStartEnd(scenario); + this.startNode = this.grid[start.row][start.col]; + this.endNode = this.grid[end.row][end.col]; + this.startNode.isStart = true; + this.endNode.isEnd = true; - const node = this.grid[pos.row][pos.col]; - return !node.isWall; + if (withWalls) { + this.placeDefaultDiagonalWall(scenario); + } + }; + this.genericGridComponent.initializeGrid(); // Trigger re-initialization and redraw } - // Validation + private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } { + if (scenario === 'edge') { + return { + start: {row: 0, col: 0}, + end: {row: this.gridRows - 1, col: this.gridCols - 1} + }; + } else if (scenario === 'random') { + return this.createRandomStartEndPosition(); + } else { + // normal: mid-left -> mid-right + const midRow = Math.floor(this.gridRows / 2); + return { + start: {row: midRow, col: 0}, + end: {row: midRow, col: this.gridCols - 1} + }; + } + } + + private createRandomStartEndPosition(): { start: GridPos; end: GridPos } { + const midCol = Math.floor(this.gridCols / 2); + + const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1); + const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1); + + const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1); + let endCol: number; + + if (startCol <= midCol) { + endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1); + } else { + endCol = this.randomIntFromInterval(0, midCol); + } + + return { + start: {row: startRow, col: startCol}, + end: {row: endRow, col: endCol} + }; + } + + private placeDefaultDiagonalWall(scenario: 'normal' | 'edge' | 'random'): void { + if (scenario === 'edge') { + this.createDiagonalWall(); + } else if (scenario === 'normal') { + this.createVerticalWall(); + } else if (scenario === 'random') { + this.createRandomWalls(); + } + } + + private createRandomWalls() { + const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows); + + for (let wall = 0; wall < maxNumberOfWalls; wall++) { + + const row: number = this.randomIntFromInterval(0, this.gridRows - 1); + const col: number = this.randomIntFromInterval(0, this.gridCols - 1); + + if (!this.grid[row][col]) { // Use the grid passed from GenericGrid + wall--; + continue; + } + + const node = this.grid[row][col]; + if (node.isStart || node.isEnd) { + wall--; + continue; + } + + node.isWall = true; + } + + } + + private createVerticalWall() { + const height = this.gridRows; + const startCol = Math.floor(this.gridCols / 2); + + for (let i = 5; i < (height - 5); i++) { + const row = i; + + if (!this.grid[row]?.[startCol]) { + continue; + } + + const node = this.grid[row][startCol]; + if (node.isStart || node.isEnd) { + continue; + } + + node.isWall = true; + } + + } + + private createDiagonalWall() { + // Diagonal-ish wall; avoids start/end + const len = Math.min(this.gridRows, this.gridCols); + const startCol = Math.floor((this.gridCols - len) / 2); + + for (let i = 0; i < Math.max(0, len - 10); i++) { + const row = len - i - 1; + const col = startCol + i; + + if (!this.grid[row]?.[col]) { + continue; + } + + const node = this.grid[row][col]; + if (node.isStart || node.isEnd) { + continue; + } + + node.isWall = true; + } + } + + // --- Animation (adapted to use genericGridComponent for redraw) --- + private stopAnimations(): void { + for (const id of this.timeoutIds) { + clearTimeout(id); + } + this.timeoutIds = []; + } + + private clearPath(): void { + for (let row = 0; row < this.gridRows; row++) { + for (let col = 0; col < this.gridCols; col++) { + const node = this.grid[row][col]; + node.isVisited = false; + node.isPath = false; + node.distance = Infinity; + node.previousNode = null; + } + } + this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component + } + + private animateAlgorithm(visited: Node[], path: Node[]): void { + for (let i = 0; i <= visited.length; i++) { + if (i === visited.length) { + const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i); + this.timeoutIds.push(id); + return; + } + + const node = visited[i]; + const id = globalThis.setTimeout(() => { + if (!node.isStart && !node.isEnd) { + node.isVisited = true; + this.genericGridComponent?.drawNode(node); // Redraw single node + } + }, this.animationSpeed * i); + + this.timeoutIds.push(id); + } + } + + private animateShortestPath(path: Node[]): void { + for (let i = 0; i < path.length; i++) { + const node = path[i]; + const id = globalThis.setTimeout(() => { + if (!node.isStart && !node.isEnd) { + node.isPath = true; + this.genericGridComponent?.drawNode(node); // Redraw single node + } + }, this.animationSpeed * i); + + this.timeoutIds.push(id); + } + } + + // --- Validation --- private ensureStartAndEnd(): boolean { if (this.startNode && this.endNode) { return true; @@ -593,73 +486,7 @@ export class PathfindingComponent implements AfterViewInit { return false; } - // Grid sizing - private clampGridSize(value: number, fallback: number): number { - const parsed = Math.floor(Number(value)); - const safe = Number.isFinite(parsed) ? parsed : fallback; - return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE); - } - - private computeNodeSize(rows: number, cols: number): number { - const sizeByWidth = Math.floor(MAX_GRID_PX / cols); - const sizeByHeight = Math.floor(MAX_GRID_PX / 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; - } - - // 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 getContextOrThrow(): CanvasRenderingContext2D { - const ctx = this.canvas.nativeElement.getContext('2d'); - if (!ctx) { - throw new Error('CanvasRenderingContext2D not available.'); - } - return ctx; - } - + // --- Utility --- private randomIntFromInterval(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1) + min); } diff --git a/src/app/shared/components/generic-grid/generic-grid.html b/src/app/shared/components/generic-grid/generic-grid.html new file mode 100644 index 0000000..d0e9eb5 --- /dev/null +++ b/src/app/shared/components/generic-grid/generic-grid.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/shared/components/generic-grid/generic-grid.scss b/src/app/shared/components/generic-grid/generic-grid.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/generic-grid/generic-grid.ts b/src/app/shared/components/generic-grid/generic-grid.ts new file mode 100644 index 0000000..55c8035 --- /dev/null +++ b/src/app/shared/components/generic-grid/generic-grid.ts @@ -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; + + @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(); + @Output() nodeClick = new EventEmitter(); + + 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; + } +} From bf46c57db0beef1bdefc6aaf7de0dfa978775153 Mon Sep 17 00:00:00 2001 From: Lobo Date: Fri, 6 Feb 2026 22:03:18 +0100 Subject: [PATCH 6/6] Conway GOL: add scenarios & start/pause loop Add predefined scenarios (SIMPLE, PULSAR, GUN) and UI controls to generate them; introduce a start/pause game loop driven by an Angular signal. Reduce default grid to 40x40 and max grid to 100, speed up default generation to 30ms, and pause the game when grid size changes. Implement scenario setup helpers (simple, pulsar, glider gun), life-rule evaluation, neighbor counting, grid swapping and a delay helper. Update template to show scenario buttons and conditional start/pause button, and add corresponding i18n entries for English and German. --- .../algorithms/conway-gol/conway-gol.html | 32 ++-- .../conway-gol/conway-gol.models.ts | 13 +- .../pages/algorithms/conway-gol/conway-gol.ts | 137 +++++++++++++++++- src/assets/i18n/de.json | 4 + src/assets/i18n/en.json | 4 + 5 files changed, 169 insertions(+), 21 deletions(-) diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html index 185995a..7352516 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -6,23 +6,33 @@
+ + + - -
- + @if (gameStarted()) + { + + } @else { + + }
@@ -33,7 +43,7 @@ [(ngModel)]="gridRows" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" + (ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" /> @@ -44,7 +54,7 @@ [(ngModel)]="gridCols" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" + (ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" /> 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 f2854d6..c582cc2 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.models.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.models.ts @@ -6,15 +6,18 @@ export interface Node { export enum Scenario { RANDOM = 0, - EMPTY + EMPTY = 1, + SIMPLE = 2, + PULSAR = 3, + GUN = 4 } -export const DEFAULT_GRID_ROWS = 100; -export const DEFAULT_GRID_COLS = 100; +export const DEFAULT_GRID_ROWS = 40; +export const DEFAULT_GRID_COLS = 40; export const MIN_GRID_SIZE = 20; -export const MAX_GRID_SIZE = 200; -export const DEFAULT_TIME_PER_GENERATION = 50; +export const MAX_GRID_SIZE = 100; +export const DEFAULT_TIME_PER_GENERATION = 30; export const MIN_TIME_PER_GENERATION = 20; export const MAX_TIME_PER_GENERATION = 200; diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index 5d6d524..9190bed 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, signal, ViewChild} from '@angular/core'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; import {TranslatePipe} from "@ngx-translate/core"; import {UrlConstants} from '../../../constants/UrlConstants'; @@ -8,7 +8,7 @@ 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, LIVE_SPAWN_PROBABILITY, Scenario, MAX_TIME_PER_GENERATION, MIN_TIME_PER_GENERATION, DEFAULT_TIME_PER_GENERATION} from './conway-gol.models'; +import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, DEFAULT_TIME_PER_GENERATION, LIVE_SPAWN_PROBABILITY, MAX_GRID_PX, MAX_GRID_SIZE, MAX_TIME_PER_GENERATION, MIN_GRID_SIZE, MIN_TIME_PER_GENERATION, Node, Scenario} from './conway-gol.models'; import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid'; @Component({ @@ -55,7 +55,8 @@ export class ConwayGol implements AfterViewInit { protected readonly MAX_GRID_PX = MAX_GRID_PX; grid: Node[][] = []; - currentScenario: Scenario = 0; + currentScenario: Scenario = Scenario.SIMPLE; + readonly gameStarted = signal(false); @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; @@ -72,6 +73,7 @@ export class ConwayGol implements AfterViewInit { this.genericGridComponent.maxGridPx = this.MAX_GRID_PX; this.genericGridComponent.initializeGrid(); } + this.gameStarted.set(false); } generate(scene: Scenario): void { @@ -107,9 +109,14 @@ export class ConwayGol implements AfterViewInit { }; initializeConwayGrid = (grid: Node[][]): void => { + this.gameStarted.set(false); this.grid = grid; - if (this.currentScenario === Scenario.RANDOM) { - this.setupRandomLives(); + + switch(this.currentScenario) { + case Scenario.RANDOM: this.setupRandomLives(); break; + case Scenario.SIMPLE: this.setupSimpleLive(); break; + case Scenario.PULSAR: this.setupPulsar(); break; + case Scenario.GUN: this.setupGliderGun(); break; } }; @@ -122,8 +129,128 @@ export class ConwayGol implements AfterViewInit { } } + setupSimpleLive(): void { + this.grid[3][4].alive = true; + this.grid[4][5].alive = true; + this.grid[5][3].alive = true; + this.grid[5][4].alive = true; + this.grid[5][5].alive = true; + } + + setupPulsar(): void { + const centerRow = Math.floor(this.gridRows / 2); + const centerCol = Math.floor(this.gridCols / 2); + + const rows = [-6, -1, 1, 6]; + const offsets = [2, 3, 4]; + + rows.forEach(r => { + offsets.forEach(c => { + this.setAlive(centerRow + r, centerCol + c); + this.setAlive(centerRow + r, centerCol - c); + this.setAlive(centerRow + c, centerCol + r); + this.setAlive(centerRow - c, centerCol + r); + }); + }); + } + + setupGliderGun(): void { + const r = 5; + const c = 5; + + const dots = [ + [r+4, c], [r+4, c+1], [r+5, c], [r+5, c+1], // Block links + [r+4, c+10], [r+5, c+10], [r+6, c+10], [r+3, c+11], [r+7, c+11], + [r+2, c+12], [r+8, c+12], [r+2, c+13], [r+8, c+13], [r+5, c+14], + [r+3, c+15], [r+7, c+15], [r+4, c+16], [r+5, c+16], [r+6, c+16], [r+5, c+17], + [r+2, c+20], [r+3, c+20], [r+4, c+20], [r+2, c+21], [r+3, c+21], [r+4, c+21], + [r+1, c+22], [r+5, c+22], [r+0, c+24], [r+1, c+24], [r+5, c+24], [r+6, c+24], + [r+2, c+34], [r+3, c+34], [r+2, c+35], [r+3, c+35] + ]; + + dots.forEach(([row, col]) => this.setAlive(row, col)); + } + + // --- The rules of the game + + pauseGame(): void { + this.gameStarted.set(false); + } + + async startGame(): Promise { + this.gameStarted.set(true); + let lifeIsDead = false; + while (this.gameStarted()){ + let gridClone = structuredClone(this.grid); + lifeIsDead = true; + for (let row = 0; row < this.gridRows; row++) { + for (let col = 0; col < this.gridCols; col++) { + lifeIsDead = this.checkLifeRules(row, col, gridClone, lifeIsDead); + } + } + + this.swapGrid(gridClone); + if (lifeIsDead){ + this.gameStarted.set(false); + } + await this.delay(this.lifeSpeed); + } + } + + private checkLifeRules(row: number, col: number, gridClone: Node[][], lifeIsDead: boolean) { + const itsMe = this.grid[row][col]; + let aliveNeighbors = this.howManyNeighborsAreLiving(row, col); + if (itsMe.alive && (aliveNeighbors < 2 || aliveNeighbors > 3)) { + gridClone[row][col].alive = false; + lifeIsDead = false; + } else if (!itsMe.alive && aliveNeighbors === 3) { + gridClone[row][col].alive = true; + lifeIsDead = false; + } + return lifeIsDead; + } + + private swapGrid(gridClone: Node[][]) { + this.grid = gridClone; + if (this.genericGridComponent) { + this.genericGridComponent.grid = this.grid; + this.genericGridComponent.drawGrid(); + } + } + + private howManyNeighborsAreLiving(row: number, col: number): number { + + let aliveNeighborCount = 0; + const minRow = Math.max(row - 1, 0); + const minCol = Math.max(col - 1, 0); + const maxRow = Math.min(row + 1, this.gridRows - 1); + const maxCol = Math.min(col + 1, this.gridCols - 1); + + for (let nRow = minRow; nRow <= maxRow; nRow++) { + for (let nCol = minCol; nCol <= maxCol; nCol++) { + if (nRow == row && nCol == col) { + continue; + } + if (this.grid[nRow][nCol].alive) { + aliveNeighborCount++; + } + } + } + return aliveNeighborCount; + } + // --- Other methods --- protected readonly Scenario = Scenario; protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION; protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION; + + delay(ms: number) { + return new Promise( resolve => setTimeout(resolve, ms) ); + } + + private setAlive(r: number, c: number): void { + if (r >= 0 && r < this.gridRows && c >= 0 && c < this.gridCols) { + this.grid[r][c].alive = true; + } + } } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 7e03d1a..638d321 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -343,8 +343,12 @@ "GOL": { "TITLE": "Conway's Spiel des Lebens", "START": "Starten", + "PAUSE": "Pause", "RANDOM_SCENE": "Zufällig", "EMPTY_SCENE": "Leer", + "SIMPLE_SCENE": "Simpel", + "PULSAR_SCENE": "Pulsar", + "GUN_SCENE": "Pistole", "ALIVE": "Lebend", "DEAD": "Leer", "SPEED": "Zeit pro Generation", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 8754e04..30b4f67 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -342,8 +342,12 @@ "GOL": { "TITLE": "Conway's Game of Life", "START": "Start", + "PAUSE": "Pause", "RANDOM_SCENE": "Random", "EMPTY_SCENE": "Empty", + "SIMPLE_SCENE": "Simple", + "PULSAR_SCENE": "Pulsar", + "GUN_SCENE": "Gun", "ALIVE": "Alive", "DEAD": "Empty", "SPEED": "Time per Generation",