refactor(components): use OnPush and signals in components

Angular 22 defaults to OnPush, so Eager is removed.
Template state is now signals, so sorting needs no detectChanges.
Game of Life execution time now updates live.
Remove the unused ImageDialogComponent and turn the OnPush lint rule
back on.
This commit is contained in:
lobo committed 2026-10-13 11:10:04 +02:00
1 parent 2abc7c31e2
commit dc9f09bede
33 files changed
+336 -336

No files matched your search

-2
View File
@@ -18,8 +18,6 @@ module.exports = defineConfig([
"@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/prefer-for-of": "off", "@typescript-eslint/prefer-for-of": "off",
// ng update set ChangeDetectionStrategy.Eager everywhere; OnPush is a separate refactor.
"@angular-eslint/prefer-on-push-component-change-detection": "off",
"@angular-eslint/directive-selector": [ "@angular-eslint/directive-selector": [
"error", "error",
{ {
+1 -2
View File
@@ -1,4 +1,4 @@
import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; import { Component, inject } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router'; import { RouterLink, RouterOutlet } from '@angular/router';
import {TopbarComponent} from '../topbar/topbar.component'; import {TopbarComponent} from '../topbar/topbar.component';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
@@ -10,7 +10,6 @@ import {SeoService} from '../../service/seo.service';
selector: 'app-root', selector: 'app-root',
imports: [RouterLink, RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent], imports: [RouterLink, RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
templateUrl: './app.component.html', templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })
export class AppComponent { export class AppComponent {
@@ -1,72 +0,0 @@
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
@Component({
imports: [MatDialogModule, MatButtonModule, MatIconModule],
template: `
<div class="dialog">
<div class="topbar">
<div class="title">{{ data.title }}</div>
<button mat-icon-button mat-dialog-close aria-label="Schließen">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="stage">
<img class="full" [src]="data.src" [alt]="data.title" />
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.Eager,
styles: [`
.dialog {
display: flex;
flex-direction: column;
height: 100%;
}
.topbar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 14px 16px;
}
.title {
font-weight: 600;
line-height: 1.2;
max-width: calc(100% - 56px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stage {
flex: 1 1 auto;
padding: 0 16px 16px;
min-height: 0;
display: grid;
place-items: center;
}
.full {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 12px;
}
`],
})
export class ImageDialogComponent {
data = inject<{
title: string;
src: string;
}>(MAT_DIALOG_DATA);
}
+1 -2
View File
@@ -1,4 +1,4 @@
import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router'; import { RouterLink, RouterLinkActive } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -21,7 +21,6 @@ import {RouterConstants} from '../../constants/RouterConstants';
TranslatePipe, MatDivider, NgOptimizedImage TranslatePipe, MatDivider, NgOptimizedImage
], ],
templateUrl: './topbar.component.html', templateUrl: './topbar.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './topbar.component.scss' styleUrl: './topbar.component.scss'
}) })
export class TopbarComponent { export class TopbarComponent {
+1 -2
View File
@@ -1,4 +1,4 @@
import { Component, computed, signal, ChangeDetectionStrategy } from '@angular/core'; import { Component, computed, signal } from '@angular/core';
import { NgOptimizedImage } from '@angular/common'; import { NgOptimizedImage } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
@@ -37,7 +37,6 @@ interface SkillGroup {
RouterLink RouterLink
], ],
templateUrl: './about.component.html', templateUrl: './about.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './about.component.scss' styleUrl: './about.component.scss'
}) })
export class AboutComponent { export class AboutComponent {
@@ -1,4 +1,4 @@
import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; import { Component, inject } from '@angular/core';
import { AlgorithmsService } from './algorithms.service'; import { AlgorithmsService } from './algorithms.service';
import { AlgorithmCategory } from './algorithm-category'; import { AlgorithmCategory } from './algorithm-category';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
@@ -10,7 +10,6 @@ import {TranslatePipe} from '@ngx-translate/core';
selector: 'app-algorithms', selector: 'app-algorithms',
templateUrl: './algorithms.component.html', templateUrl: './algorithms.component.html',
styleUrl: './algorithms.component.scss', styleUrl: './algorithms.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe], imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe],
}) })
export class AlgorithmsComponent { export class AlgorithmsComponent {
@@ -7,17 +7,17 @@
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]"> <div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
<button mat-raised-button color="primary" (click)="toggleWind()"> <button mat-raised-button color="primary" (click)="toggleWind()">
{{ isWindActive ? ('CLOTH.WIND_OFF' | translate) : ('CLOTH.WIND_ON' | translate) }} {{ isWindActive() ? ('CLOTH.WIND_OFF' | translate) : ('CLOTH.WIND_ON' | translate) }}
</button> </button>
<button mat-raised-button color="primary" (click)="toggleMesh()"> <button mat-raised-button color="primary" (click)="toggleMesh()">
{{ isOutlineActive ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }} {{ isOutlineActive() ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }}
</button> </button>
<button mat-raised-button color="accent" (click)="restartSimulation()"> <button mat-raised-button color="accent" (click)="restartSimulation()">
{{ 'CLOTH.RESTART_SIMULATION' | translate }} {{ 'CLOTH.RESTART_SIMULATION' | translate }}
</button> </button>
</div> </div>
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<span>{{ 'CLOTH.ELONGATION' | translate }}: {{ elongation }}</span> <span>{{ 'CLOTH.ELONGATION' | translate }}: {{ elongation() }}</span>
<mat-slider min="0.5" max="2.0" step="0.1"> <mat-slider min="0.5" max="2.0" step="0.1">
<input matSliderThumb [(ngModel)]="elongation"> <input matSliderThumb [(ngModel)]="elongation">
</mat-slider> </mat-slider>
@@ -1,4 +1,4 @@
import {Component, ChangeDetectionStrategy} from '@angular/core'; import {Component, signal} from '@angular/core';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {MatSliderModule} from '@angular/material/slider'; import {MatSliderModule} from '@angular/material/slider';
@@ -28,7 +28,6 @@ import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
Information Information
], ],
templateUrl: './cloth.component.html', templateUrl: './cloth.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './cloth.component.scss', styleUrl: './cloth.component.scss',
}) })
export class ClothComponent { export class ClothComponent {
@@ -36,10 +35,10 @@ export class ClothComponent {
private simulationTime: number = 0; private simulationTime: number = 0;
private strategy: ClothSimulationStrategy | null = null; private strategy: ClothSimulationStrategy | null = null;
public isWindActive: boolean = false; public readonly isWindActive = signal(false);
public isOutlineActive: boolean = false; public readonly isOutlineActive = signal(false);
public stiffness: number = 80; public stiffness: number = 80;
public elongation: number = 1.0; public readonly elongation = signal(1.0);
public renderConfig: RenderConfig = { public renderConfig: RenderConfig = {
mode: '3D', mode: '3D',
@@ -85,16 +84,16 @@ export class ClothComponent {
} }
public toggleWind(): void { public toggleWind(): void {
this.isWindActive = !this.isWindActive; this.isWindActive.update(v => !v);
} }
public toggleMesh(): void { public toggleMesh(): void {
this.isOutlineActive = !this.isOutlineActive; this.isOutlineActive.update(v => !v);
const mesh = this.strategy?.getMesh(); const mesh = this.strategy?.getMesh();
if (!mesh?.material) { if (!mesh?.material) {
return; return;
} }
mesh.material.wireframe = this.isOutlineActive; mesh.material.wireframe = this.isOutlineActive();
} }
public restartSimulation(): void { public restartSimulation(): void {
@@ -130,8 +129,8 @@ export class ClothComponent {
if (this.strategy) { if (this.strategy) {
this.strategy.updateParams({ this.strategy.updateParams({
stiffness: this.stiffness, stiffness: this.stiffness,
elongation: this.elongation, elongation: this.elongation(),
isWindActive: this.isWindActive, isWindActive: this.isWindActive(),
simulationTime: this.simulationTime, simulationTime: this.simulationTime,
deltaTime: engine.getDeltaTime() / 1000.0 deltaTime: engine.getDeltaTime() / 1000.0
}); });
@@ -33,7 +33,7 @@
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }} <mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
</button> </button>
} }
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p> <p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime() }} ms</p>
</div> </div>
<div class="flex gap-3 items-center flex-wrap"> <div class="flex gap-3 items-center flex-wrap">
<mat-form-field appearance="outline" class="w-[150px]"> <mat-form-field appearance="outline" class="w-[150px]">
@@ -44,7 +44,7 @@
[(ngModel)]="gridRows" [(ngModel)]="gridRows"
[min]="MIN_GRID_SIZE" [min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE" [max]="MAX_GRID_SIZE"
(ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" (ngModelChange)="applyGridRows($event)"
/> />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="w-[150px]"> <mat-form-field appearance="outline" class="w-[150px]">
@@ -55,7 +55,7 @@
[(ngModel)]="gridCols" [(ngModel)]="gridCols"
[min]="MIN_GRID_SIZE" [min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE" [max]="MAX_GRID_SIZE"
(ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" (ngModelChange)="applyGridCols($event)"
/> />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="w-[150px]"> <mat-form-field appearance="outline" class="w-[150px]">
@@ -78,8 +78,8 @@
</div> </div>
</div> </div>
<app-generic-grid <app-generic-grid
[gridRows]="gridRows" [gridRows]="gridRows()"
[gridCols]="gridCols" [gridCols]="gridCols()"
[minGridSize]="MIN_GRID_SIZE" [minGridSize]="MIN_GRID_SIZE"
[maxGridSize]="MAX_GRID_SIZE" [maxGridSize]="MAX_GRID_SIZE"
[maxGridPx]="MAX_GRID_PX" [maxGridPx]="MAX_GRID_PX"
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, signal, ViewChild, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, signal, ViewChild} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
import {TranslatePipe} from "@ngx-translate/core"; import {TranslatePipe} from "@ngx-translate/core";
import {UrlConstants} from '../../../constants/UrlConstants'; import {UrlConstants} from '../../../constants/UrlConstants';
@@ -29,7 +29,6 @@ import {GenericGridComponent, GridPos} from '../../../shared/components/generic-
FormsModule, FormsModule,
GenericGridComponent GenericGridComponent
], ],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './conway-gol.component.html', templateUrl: './conway-gol.component.html',
}) })
export class ConwayGolComponent implements AfterViewInit { export class ConwayGolComponent implements AfterViewInit {
@@ -48,16 +47,16 @@ export class ConwayGolComponent implements AfterViewInit {
disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4'] disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4']
}; };
protected gridCols = DEFAULT_GRID_COLS; protected readonly gridCols = signal(DEFAULT_GRID_COLS);
protected gridRows = DEFAULT_GRID_ROWS; protected readonly gridRows = signal(DEFAULT_GRID_ROWS);
protected lifeSpeed = DEFAULT_TIME_PER_GENERATION; protected readonly lifeSpeed = signal(DEFAULT_TIME_PER_GENERATION);
protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE; protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE; protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
protected readonly MAX_GRID_PX = MAX_GRID_PX; protected readonly MAX_GRID_PX = MAX_GRID_PX;
readGrid: Node[][] = []; readGrid: Node[][] = [];
writeGrid: Node[][] = []; writeGrid: Node[][] = [];
executionTime = 0; readonly executionTime = signal(0);
currentScenario: Scenario = Scenario.SIMPLE; currentScenario: Scenario = Scenario.SIMPLE;
readonly gameStarted = signal(false); readonly gameStarted = signal(false);
@@ -69,8 +68,8 @@ export class ConwayGolComponent implements AfterViewInit {
this.genericGridComponent.createNodeFn = this.createConwayNode; this.genericGridComponent.createNodeFn = this.createConwayNode;
this.genericGridComponent.getNodeColorFn = this.getConwayNodeColor; this.genericGridComponent.getNodeColorFn = this.getConwayNodeColor;
this.genericGridComponent.applySelectionFn = this.applyConwaySelection; this.genericGridComponent.applySelectionFn = this.applyConwaySelection;
this.genericGridComponent.gridRows = this.gridRows; this.genericGridComponent.gridRows = this.gridRows();
this.genericGridComponent.gridCols = this.gridCols; this.genericGridComponent.gridCols = this.gridCols();
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE; this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE; this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
this.genericGridComponent.maxGridPx = this.MAX_GRID_PX; this.genericGridComponent.maxGridPx = this.MAX_GRID_PX;
@@ -86,7 +85,19 @@ export class ConwayGolComponent implements AfterViewInit {
} }
applySpeed(): void { applySpeed(): void {
this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION); this.lifeSpeed.set(Math.min(Math.max(this.lifeSpeed(), MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION));
}
protected applyGridRows(rows: number): void {
this.pauseGame();
this.genericGridComponent.gridRows = rows;
this.genericGridComponent.applyGridSize();
}
protected applyGridCols(cols: number): void {
this.pauseGame();
this.genericGridComponent.gridCols = cols;
this.genericGridComponent.applyGridSize();
} }
// --- Callbacks for GenericGridComponent --- // --- Callbacks for GenericGridComponent ---
@@ -122,8 +133,10 @@ export class ConwayGolComponent implements AfterViewInit {
// --- Conway-specific logic (kept local) --- // --- Conway-specific logic (kept local) ---
setupRandomLives(): void { setupRandomLives(): void {
for (let row = 0; row < this.gridRows; row++) { const rows = this.gridRows();
for (let col = 0; col < this.gridCols; col++) { const cols = this.gridCols();
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
this.readGrid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY; this.readGrid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY;
} }
} }
@@ -138,8 +151,8 @@ export class ConwayGolComponent implements AfterViewInit {
} }
setupPulsar(): void { setupPulsar(): void {
const centerRow = Math.floor(this.gridRows / 2); const centerRow = Math.floor(this.gridRows() / 2);
const centerCol = Math.floor(this.gridCols / 2); const centerCol = Math.floor(this.gridCols() / 2);
const rows = [-6, -1, 1, 6]; const rows = [-6, -1, 1, 6];
const offsets = [2, 3, 4]; const offsets = [2, 3, 4];
@@ -182,22 +195,25 @@ export class ConwayGolComponent implements AfterViewInit {
while (this.gameStarted()){ while (this.gameStarted()){
const startTime = performance.now(); const startTime = performance.now();
let lifeIsDead = true; let lifeIsDead = true;
for (let row = 0; row < this.gridRows; row++) { const rows = this.gridRows();
for (let col = 0; col < this.gridCols; col++) { const cols = this.gridCols();
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
lifeIsDead = this.checkLifeRules(row, col, this.writeGrid) && lifeIsDead; lifeIsDead = this.checkLifeRules(row, col, this.writeGrid) && lifeIsDead;
} }
} }
this.swapGrids(); this.swapGrids();
const endTime = performance.now(); const endTime = performance.now();
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4)); const elapsed = Number.parseFloat((endTime - startTime).toFixed(4));
this.executionTime.set(elapsed);
if (lifeIsDead){ if (lifeIsDead){
this.gameStarted.set(false); this.gameStarted.set(false);
} }
const delta = Math.max(this.lifeSpeed - this.executionTime, 0); const delta = Math.max(this.lifeSpeed() - elapsed, 0);
await this.delay(delta); await this.delay(delta);
} }
this.executionTime = 0; this.executionTime.set(0);
} }
private checkLifeRules(row: number, col: number, writeGrid: Node[][]): boolean { private checkLifeRules(row: number, col: number, writeGrid: Node[][]): boolean {
@@ -227,8 +243,8 @@ export class ConwayGolComponent implements AfterViewInit {
let aliveNeighborCount = 0; let aliveNeighborCount = 0;
const minRow = Math.max(row - 1, 0); const minRow = Math.max(row - 1, 0);
const minCol = Math.max(col - 1, 0); const minCol = Math.max(col - 1, 0);
const maxRow = Math.min(row + 1, this.gridRows - 1); const maxRow = Math.min(row + 1, this.gridRows() - 1);
const maxCol = Math.min(col + 1, this.gridCols - 1); const maxCol = Math.min(col + 1, this.gridCols() - 1);
for (let nRow = minRow; nRow <= maxRow; nRow++) { for (let nRow = minRow; nRow <= maxRow; nRow++) {
for (let nCol = minCol; nCol <= maxCol; nCol++) { for (let nCol = minCol; nCol <= maxCol; nCol++) {
@@ -253,7 +269,7 @@ export class ConwayGolComponent implements AfterViewInit {
} }
private setAlive(r: number, c: number): void { private setAlive(r: number, c: number): void {
if (r >= 0 && r < this.gridRows && c >= 0 && c < this.gridCols) { if (r >= 0 && r < this.gridRows() && c >= 0 && c < this.gridCols()) {
this.readGrid[r][c].alive = true; this.readGrid[r][c].alive = true;
} }
} }
@@ -49,13 +49,13 @@
<div class="mt-5 flex gap-2.5 py-2.5 px-4 rounded-sm border-l-[5px] font-medium min-w-[300px] items-center" <div class="mt-5 flex gap-2.5 py-2.5 px-4 rounded-sm border-l-[5px] font-medium min-w-[300px] items-center"
[ngClass]="{ [ngClass]="{
'border-l-gray-500 bg-app-bg': solutionStatus === 'INCOMPLETE', 'border-l-gray-500 bg-app-bg': solutionStatus() === 'INCOMPLETE',
'border-l-green-500 bg-green-50 text-green-800': solutionStatus === 'SOLVED', 'border-l-green-500 bg-green-50 text-green-800': solutionStatus() === 'SOLVED',
'border-l-orange-500 bg-orange-50 text-orange-700': solutionStatus === 'CONFLICTS', 'border-l-orange-500 bg-orange-50 text-orange-700': solutionStatus() === 'CONFLICTS',
'border-l-red-500 bg-red-50 text-red-800': solutionStatus === 'INVALID' 'border-l-red-500 bg-red-50 text-red-800': solutionStatus() === 'INVALID'
}"> }">
<span class="uppercase text-[0.8em] opacity-70">{{ 'FOUR_COLOR.STATUS.LABEL' | translate }}:</span> <span class="uppercase text-[0.8em] opacity-70">{{ 'FOUR_COLOR.STATUS.LABEL' | translate }}:</span>
<span>{{ 'FOUR_COLOR.STATUS.' + solutionStatus | translate }}</span> <span>{{ 'FOUR_COLOR.STATUS.' + solutionStatus() | translate }}</span>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, inject, ViewChild, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, ElementRef, inject, signal, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
@@ -29,7 +29,6 @@ import {UrlConstants} from '../../../constants/UrlConstants';
Information Information
], ],
templateUrl: './four-color.component.html', templateUrl: './four-color.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './four-color.component.scss' styleUrl: './four-color.component.scss'
}) })
export class FourColorComponent implements AfterViewInit { export class FourColorComponent implements AfterViewInit {
@@ -59,12 +58,12 @@ export class FourColorComponent implements AfterViewInit {
] ]
}; };
gridRows = DEFAULT_GRID_ROWS; readonly gridRows = signal(DEFAULT_GRID_ROWS);
gridCols = DEFAULT_GRID_COLS; readonly gridCols = signal(DEFAULT_GRID_COLS);
grid: FourColorNode[][] = []; grid: FourColorNode[][] = [];
regions: Region[] = []; regions: Region[] = [];
executionTime = 0; executionTime = 0;
solutionStatus: 'INCOMPLETE' | 'SOLVED' | 'CONFLICTS' | 'INVALID' = 'INCOMPLETE'; readonly solutionStatus = signal<'INCOMPLETE' | 'SOLVED' | 'CONFLICTS' | 'INVALID'>('INCOMPLETE');
@ViewChild('fourColorCanvas') canvasRef!: ElementRef<HTMLCanvasElement>; @ViewChild('fourColorCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
private ctx!: CanvasRenderingContext2D; private ctx!: CanvasRenderingContext2D;
@@ -76,20 +75,18 @@ export class FourColorComponent implements AfterViewInit {
} }
applyGridSize(): void { applyGridSize(): void {
if (this.gridRows < MIN_GRID_SIZE) this.gridRows = MIN_GRID_SIZE; this.gridRows.set(Math.min(Math.max(this.gridRows(), MIN_GRID_SIZE), MAX_GRID_SIZE));
if (this.gridRows > MAX_GRID_SIZE) this.gridRows = MAX_GRID_SIZE; this.gridCols.set(Math.min(Math.max(this.gridCols(), MIN_GRID_SIZE), MAX_GRID_SIZE));
if (this.gridCols < MIN_GRID_SIZE) this.gridCols = MIN_GRID_SIZE;
if (this.gridCols > MAX_GRID_SIZE) this.gridCols = MAX_GRID_SIZE;
this.initializeGrid(); this.initializeGrid();
} }
initializeGrid(): void { initializeGrid(): void {
this.grid = []; this.grid = [];
this.solutionStatus = 'INCOMPLETE'; this.solutionStatus.set('INCOMPLETE');
for (let r = 0; r < this.gridRows; r++) { for (let r = 0; r < this.gridRows(); r++) {
const row: FourColorNode[] = []; const row: FourColorNode[] = [];
for (let c = 0; c < this.gridCols; c++) { for (let c = 0; c < this.gridCols(); c++) {
row.push({ row.push({
row: r, row: r,
col: c, col: c,
@@ -107,15 +104,15 @@ export class FourColorComponent implements AfterViewInit {
private resizeCanvas(): void { private resizeCanvas(): void {
const canvas = this.canvasRef.nativeElement; const canvas = this.canvasRef.nativeElement;
const maxDim = Math.max(this.gridRows, this.gridCols); const maxDim = Math.max(this.gridRows(), this.gridCols());
this.nodeSize = Math.floor(MAX_GRID_PX / maxDim); this.nodeSize = Math.floor(MAX_GRID_PX / maxDim);
canvas.width = this.gridCols * this.nodeSize; canvas.width = this.gridCols() * this.nodeSize;
canvas.height = this.gridRows * this.nodeSize; canvas.height = this.gridRows() * this.nodeSize;
} }
generateRegions(): void { generateRegions(): void {
const numRegions = Math.floor((this.gridRows * this.gridCols) / 30); const numRegions = Math.floor((this.gridRows() * this.gridCols()) / 30);
this.regions = []; this.regions = [];
const seeds = this.determineSeeds(numRegions); const seeds = this.determineSeeds(numRegions);
this.regionGrowth(seeds); this.regionGrowth(seeds);
@@ -123,8 +120,8 @@ export class FourColorComponent implements AfterViewInit {
} }
private determineAdjacency() { private determineAdjacency() {
for (let row = 0; row < this.gridRows; row++) { for (let row = 0; row < this.gridRows(); row++) {
for (let col = 0; col < this.gridCols; col++) { for (let col = 0; col < this.gridCols(); col++) {
const currentRegionId = this.grid[row][col].regionId; const currentRegionId = this.grid[row][col].regionId;
const neighbors = this.getNeighbors(row, col); const neighbors = this.getNeighbors(row, col);
for (const neighbor of neighbors) { for (const neighbor of neighbors) {
@@ -157,11 +154,11 @@ export class FourColorComponent implements AfterViewInit {
private determineSeeds(numRegions: number) { private determineSeeds(numRegions: number) {
const seeds: GridPos[] = []; const seeds: GridPos[] = [];
for (let i = 0; i < numRegions; i++) { for (let i = 0; i < numRegions; i++) {
let r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); let r = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
let c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1); let c = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
while (this.grid[r][c].regionId !== -1) { while (this.grid[r][c].regionId !== -1) {
r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); r = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1); c = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
} }
this.grid[r][c].regionId = i; this.grid[r][c].regionId = i;
seeds.push({row: r, col: c}); seeds.push({row: r, col: c});
@@ -173,9 +170,9 @@ export class FourColorComponent implements AfterViewInit {
private getNeighbors(row: number, col: number): GridPos[] { private getNeighbors(row: number, col: number): GridPos[] {
const res: GridPos[] = []; const res: GridPos[] = [];
if (row > 0) res.push({row: row - 1, col}); if (row > 0) res.push({row: row - 1, col});
if (row < this.gridRows - 1) res.push({row: row + 1, col}); if (row < this.gridRows() - 1) res.push({row: row + 1, col});
if (col > 0) res.push({row, col: col - 1}); if (col > 0) res.push({row, col: col - 1});
if (col < this.gridCols - 1) res.push({row, col: col + 1}); if (col < this.gridCols() - 1) res.push({row, col: col + 1});
return res; return res;
} }
@@ -185,8 +182,8 @@ export class FourColorComponent implements AfterViewInit {
this.ctx.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height); this.ctx.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height);
// 1. Draw Cell Backgrounds // 1. Draw Cell Backgrounds
for (let r = 0; r < this.gridRows; r++) { for (let r = 0; r < this.gridRows(); r++) {
for (let c = 0; c < this.gridCols; c++) { for (let c = 0; c < this.gridCols(); c++) {
const node = this.grid[r][c]; const node = this.grid[r][c];
this.ctx.fillStyle = this.getNodeColor(node); this.ctx.fillStyle = this.getNodeColor(node);
this.ctx.fillRect(c * this.nodeSize, r * this.nodeSize, this.nodeSize, this.nodeSize); this.ctx.fillRect(c * this.nodeSize, r * this.nodeSize, this.nodeSize, this.nodeSize);
@@ -197,18 +194,18 @@ export class FourColorComponent implements AfterViewInit {
this.ctx.strokeStyle = '#000'; this.ctx.strokeStyle = '#000';
this.ctx.lineWidth = 2; this.ctx.lineWidth = 2;
this.ctx.beginPath(); this.ctx.beginPath();
for (let r = 0; r < this.gridRows; r++) { for (let r = 0; r < this.gridRows(); r++) {
for (let c = 0; c < this.gridCols; c++) { for (let c = 0; c < this.gridCols(); c++) {
const currentRegion = this.grid[r][c].regionId; const currentRegion = this.grid[r][c].regionId;
// Right border // Right border
if (c < this.gridCols - 1 && this.grid[r][c+1].regionId !== currentRegion) { if (c < this.gridCols() - 1 && this.grid[r][c+1].regionId !== currentRegion) {
this.ctx.moveTo((c + 1) * this.nodeSize, r * this.nodeSize); this.ctx.moveTo((c + 1) * this.nodeSize, r * this.nodeSize);
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize); this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
} }
// Bottom border // Bottom border
if (r < this.gridRows - 1 && this.grid[r+1][c].regionId !== currentRegion) { if (r < this.gridRows() - 1 && this.grid[r+1][c].regionId !== currentRegion) {
this.ctx.moveTo(c * this.nodeSize, (r + 1) * this.nodeSize); this.ctx.moveTo(c * this.nodeSize, (r + 1) * this.nodeSize);
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize); this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
} }
@@ -219,7 +216,7 @@ export class FourColorComponent implements AfterViewInit {
// 3. Draw Outer Border // 3. Draw Outer Border
this.ctx.strokeStyle = '#000'; this.ctx.strokeStyle = '#000';
this.ctx.lineWidth = 2; this.ctx.lineWidth = 2;
this.ctx.strokeRect(0, 0, this.gridCols * this.nodeSize, this.gridRows * this.nodeSize); this.ctx.strokeRect(0, 0, this.gridCols() * this.nodeSize, this.gridRows() * this.nodeSize);
} }
private getNodeColor(node: FourColorNode): string { private getNodeColor(node: FourColorNode): string {
@@ -260,10 +257,10 @@ export class FourColorComponent implements AfterViewInit {
const x = event.clientX - rect.left; const x = event.clientX - rect.left;
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
const col = Math.floor(x / (rect.width / this.gridCols)); const col = Math.floor(x / (rect.width / this.gridCols()));
const row = Math.floor(y / (rect.height / this.gridRows)); const row = Math.floor(y / (rect.height / this.gridRows()));
if (row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols) { if (row >= 0 && row < this.gridRows() && col >= 0 && col < this.gridCols()) {
return {row, col}; return {row, col};
} }
return null; return null;
@@ -283,8 +280,8 @@ export class FourColorComponent implements AfterViewInit {
} }
private updateRegionColors(region: Region): void { private updateRegionColors(region: Region): void {
for (let row = 0; row < this.gridRows; row++) { for (let row = 0; row < this.gridRows(); row++) {
for (let col = 0; col < this.gridCols; col++) { for (let col = 0; col < this.gridCols(); col++) {
if (this.grid[row][col].regionId === region.id) { if (this.grid[row][col].regionId === region.id) {
this.grid[row][col].color = region.color; this.grid[row][col].color = region.color;
} }
@@ -297,7 +294,7 @@ export class FourColorComponent implements AfterViewInit {
region.color = 0; region.color = 0;
this.updateRegionColors(region); this.updateRegionColors(region);
} }
this.solutionStatus = 'INCOMPLETE'; this.solutionStatus.set('INCOMPLETE');
this.drawGrid(); this.drawGrid();
} }
@@ -363,9 +360,9 @@ export class FourColorComponent implements AfterViewInit {
} }
if (hasConflicts) { if (hasConflicts) {
this.solutionStatus = allColored ? 'INVALID' : 'CONFLICTS'; this.solutionStatus.set(allColored ? 'INVALID' : 'CONFLICTS');
} else { } else {
this.solutionStatus = allColored ? 'SOLVED' : 'INCOMPLETE'; this.solutionStatus.set(allColored ? 'SOLVED' : 'INCOMPLETE');
} }
} }
@@ -1,4 +1,4 @@
import {Component, OnInit, signal, ChangeDetectionStrategy} from '@angular/core'; import {Component, OnInit, signal} from '@angular/core';
import {Information} from '../information/information'; import {Information} from '../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
@@ -35,7 +35,6 @@ import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
NgxSliderModule NgxSliderModule
], ],
templateUrl: './fractal.component.html', templateUrl: './fractal.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './fractal.component.scss', styleUrl: './fractal.component.scss',
}) })
export class FractalComponent implements OnInit { export class FractalComponent implements OnInit {
@@ -1,4 +1,4 @@
import {Component, ChangeDetectionStrategy} from '@angular/core'; import {Component} from '@angular/core';
import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core'; import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core';
import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader'; import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader';
import {Information} from '../information/information'; import {Information} from '../information/information';
@@ -22,7 +22,6 @@ import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/compo
BabylonCanvas BabylonCanvas
], ],
templateUrl: './fractal3d.component.html', templateUrl: './fractal3d.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './fractal3d.component.scss', styleUrl: './fractal3d.component.scss',
}) })
export class Fractal3dComponent { export class Fractal3dComponent {
@@ -1,8 +1,9 @@
@let info = algorithmInformation();
<div class="mb-4 py-3 px-4 border border-[#ddd] rounded-lg"> <div class="mb-4 py-3 px-4 border border-[#ddd] rounded-lg">
<h3 class="m-0 mb-2">{{ algorithmInformation.title | translate }}</h3> <h3 class="m-0 mb-2">{{ info.title | translate }}</h3>
@if(algorithmInformation.entries && algorithmInformation.entries.length > 0){ @if(info.entries && info.entries.length > 0){
@for (algo of algorithmInformation.entries; track algo) @for (algo of info.entries; track algo)
{ {
<p class="my-2"> <p class="my-2">
<strong> <strong>
@@ -18,24 +19,24 @@
} }
} }
@if (algorithmInformation.disclaimer !== '') @if (info.disclaimer !== '')
{ {
<p class="my-2"> <p class="my-2">
<strong>{{ 'ALGORITHM.NOTE' | translate}}</strong> {{ algorithmInformation.disclaimer | translate}} <strong>{{ 'ALGORITHM.NOTE' | translate}}</strong> {{ info.disclaimer | translate}}
</p> </p>
@if (algorithmInformation.disclaimerListEntry && algorithmInformation.disclaimerListEntry.length > 0) @if (info.disclaimerListEntry && info.disclaimerListEntry.length > 0)
{ {
<ul> <ul>
@for (entry of algorithmInformation.disclaimerListEntry; track entry) @for (entry of info.disclaimerListEntry; track entry)
{ {
<li>{{ entry | translate}}</li> <li>{{ entry | translate}}</li>
} }
</ul> </ul>
} }
@if (algorithmInformation.disclaimerBottom !== '') @if (info.disclaimerBottom !== '')
{ {
<p> <p>
{{ algorithmInformation.disclaimerBottom | translate}} {{ info.disclaimerBottom | translate}}
</p> </p>
} }
} }
@@ -1,4 +1,4 @@
import {Component, Input, ChangeDetectionStrategy} from '@angular/core'; import {Component, input} from '@angular/core';
import {TranslatePipe} from "@ngx-translate/core"; import {TranslatePipe} from "@ngx-translate/core";
import {AlgorithmInformation} from './information.models'; import {AlgorithmInformation} from './information.models';
@@ -8,10 +8,9 @@ import {AlgorithmInformation} from './information.models';
TranslatePipe TranslatePipe
], ],
templateUrl: './information.html', templateUrl: './information.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './information.scss', styleUrl: './information.scss',
}) })
export class Information { export class Information {
@Input({ required: true }) algorithmInformation!: AlgorithmInformation; readonly algorithmInformation = input.required<AlgorithmInformation>();
} }
@@ -22,8 +22,8 @@
<span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span> <span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
</div> </div>
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]"> <div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p> <p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength() }}</p>
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p> <p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime() | number:'1.2-2' }} ms</p>
</div> </div>
</div> </div>
<app-generic-grid <app-generic-grid
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, inject, signal, ViewChild, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core';
import {Information} from '../../information/information'; import {Information} from '../../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
@@ -25,7 +25,6 @@ import {PathfindingService} from '../service/pathfinding.service';
DecimalPipe DecimalPipe
], ],
templateUrl: './labyrinth.component.html', templateUrl: './labyrinth.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './labyrinth.component.scss', styleUrl: './labyrinth.component.scss',
}) })
export class LabyrinthComponent implements AfterViewInit { export class LabyrinthComponent implements AfterViewInit {
@@ -64,8 +63,8 @@ export class LabyrinthComponent implements AfterViewInit {
endNode: Node | null = null; endNode: Node | null = null;
animationSpeed = 3; animationSpeed = 3;
mazeAnimationSpeed = 1; mazeAnimationSpeed = 1;
pathLength = "0"; readonly pathLength = signal("0");
executionTime = 0; readonly executionTime = signal(0);
private timeoutIds: number[] = []; private timeoutIds: number[] = [];
protected mazeNodesInOrder: Node[] = []; protected mazeNodesInOrder: Node[] = [];
@@ -309,13 +308,13 @@ export class LabyrinthComponent implements AfterViewInit {
const lengthOfShortestPath = result.nodesInShortestPathOrder.length; const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
if (lengthOfShortestPath === 0) if (lengthOfShortestPath === 0)
{ {
this.pathLength = "∞" this.pathLength.set("∞");
} }
else else
{ {
this.pathLength = result.nodesInShortestPathOrder.length + ""; this.pathLength.set(result.nodesInShortestPathOrder.length + "");
} }
this.executionTime = endTime - startTime; this.executionTime.set(endTime - startTime);
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder); this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
} }
@@ -35,7 +35,7 @@
[min]="MIN_GRID_SIZE" [min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE" [max]="MAX_GRID_SIZE"
[(ngModel)]="gridRows" [(ngModel)]="gridRows"
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" (ngModelChange)="applyGridRows($event)"
/> </mat-form-field> /> </mat-form-field>
<mat-form-field appearance="outline" class="w-[150px]"> <mat-form-field appearance="outline" class="w-[150px]">
@@ -46,7 +46,7 @@
[min]="MIN_GRID_SIZE" [min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE" [max]="MAX_GRID_SIZE"
[(ngModel)]="gridCols" [(ngModel)]="gridCols"
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" (ngModelChange)="applyGridCols($event)"
/> </mat-form-field> /> </mat-form-field>
</div> </div>
</div> </div>
@@ -59,14 +59,14 @@
<span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span> <span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
</div> </div>
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]"> <div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p> <p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength() }}</p>
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p> <p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime() | number:'1.2-2' }} ms</p>
</div> </div>
</div> </div>
<app-generic-grid <app-generic-grid
[gridRows]="gridRows" [gridRows]="gridRows()"
[gridCols]="gridCols" [gridCols]="gridCols()"
[minGridSize]="MIN_GRID_SIZE" [minGridSize]="MIN_GRID_SIZE"
[maxGridSize]="MAX_GRID_SIZE" [maxGridSize]="MAX_GRID_SIZE"
[maxGridPx]="MAX_GRID_PX" [maxGridPx]="MAX_GRID_PX"
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, inject, ViewChild, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
@@ -43,7 +43,6 @@ enum NodeType {
Information, Information,
GenericGridComponent GenericGridComponent
], ],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './pathfinding.component.html', templateUrl: './pathfinding.component.html',
}) })
export class PathfindingComponent implements AfterViewInit { export class PathfindingComponent implements AfterViewInit {
@@ -75,19 +74,19 @@ export class PathfindingComponent implements AfterViewInit {
disclaimerListEntry: [] disclaimerListEntry: []
}; };
gridRows = DEFAULT_GRID_ROWS; readonly gridRows = signal(DEFAULT_GRID_ROWS);
gridCols = DEFAULT_GRID_COLS; readonly gridCols = signal(DEFAULT_GRID_COLS);
grid: Node[][] = []; grid: Node[][] = [];
startNode: Node | null = null; startNode: Node | null = null;
endNode: Node | null = null; endNode: Node | null = null;
selectedNodeType: NodeType = NodeType.None; readonly selectedNodeType = signal<NodeType>(NodeType.None);
private shouldAddWall = true; // Moved here private shouldAddWall = true; // Moved here
animationSpeed = 3; animationSpeed = 3;
pathLength = "0"; readonly pathLength = signal("0");
executionTime = 0; readonly executionTime = signal(0);
private timeoutIds: number[] = []; private timeoutIds: number[] = [];
@@ -101,8 +100,8 @@ export class PathfindingComponent implements AfterViewInit {
this.genericGridComponent.createNodeFn = this.createPathfindingNode; this.genericGridComponent.createNodeFn = this.createPathfindingNode;
this.genericGridComponent.getNodeColorFn = this.getPathfindingNodeColor; this.genericGridComponent.getNodeColorFn = this.getPathfindingNodeColor;
this.genericGridComponent.applySelectionFn = this.applyPathfindingSelection; this.genericGridComponent.applySelectionFn = this.applyPathfindingSelection;
this.genericGridComponent.gridRows = this.gridRows; this.genericGridComponent.gridRows = this.gridRows();
this.genericGridComponent.gridCols = this.gridCols; this.genericGridComponent.gridCols = this.gridCols();
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE; this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE; this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
this.genericGridComponent.maxGridPx = MAX_GRID_PX; this.genericGridComponent.maxGridPx = MAX_GRID_PX;
@@ -111,6 +110,16 @@ export class PathfindingComponent implements AfterViewInit {
this.createCase({withWalls: true, scenario: "normal"}); this.createCase({withWalls: true, scenario: "normal"});
} }
protected applyGridRows(rows: number): void {
this.genericGridComponent.gridRows = rows;
this.genericGridComponent.applyGridSize();
}
protected applyGridCols(cols: number): void {
this.genericGridComponent.gridCols = cols;
this.genericGridComponent.applyGridSize();
}
// --- Callbacks for GenericGridComponent --- // --- Callbacks for GenericGridComponent ---
createPathfindingNode = (row: number, col: number): Node => { createPathfindingNode = (row: number, col: number): Node => {
return { return {
@@ -142,11 +151,11 @@ export class PathfindingComponent implements AfterViewInit {
const node = grid[pos.row][pos.col]; const node = grid[pos.row][pos.col];
// Determine if we should add or remove a wall // Determine if we should add or remove a wall
if (this.selectedNodeType === NodeType.Wall && this.genericGridComponent.isDrawing && this.genericGridComponent['lastCell'] === null) { if (this.selectedNodeType() === NodeType.Wall && this.genericGridComponent.isDrawing && this.genericGridComponent['lastCell'] === null) {
this.shouldAddWall = !node.isWall; this.shouldAddWall = !node.isWall;
} }
switch (this.selectedNodeType) { switch (this.selectedNodeType()) {
case NodeType.Start: case NodeType.Start:
this.trySetStart(node); this.trySetStart(node);
break; break;
@@ -200,13 +209,13 @@ export class PathfindingComponent implements AfterViewInit {
const lengthOfShortestPath = result.nodesInShortestPathOrder.length; const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
if (lengthOfShortestPath === 0) if (lengthOfShortestPath === 0)
{ {
this.pathLength = "∞" this.pathLength.set("∞");
} }
else else
{ {
this.pathLength = result.nodesInShortestPathOrder.length + ""; this.pathLength.set(result.nodesInShortestPathOrder.length + "");
} }
this.executionTime = endTime - startTime; this.executionTime.set(endTime - startTime);
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder); this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
} }
@@ -312,31 +321,31 @@ export class PathfindingComponent implements AfterViewInit {
if (scenario === 'edge') { if (scenario === 'edge') {
return { return {
start: {row: 0, col: 0}, start: {row: 0, col: 0},
end: {row: this.gridRows - 1, col: this.gridCols - 1} end: {row: this.gridRows() - 1, col: this.gridCols() - 1}
}; };
} else if (scenario === 'random') { } else if (scenario === 'random') {
return this.createRandomStartEndPosition(); return this.createRandomStartEndPosition();
} else { } else {
// normal: mid-left -> mid-right // normal: mid-left -> mid-right
const midRow = Math.floor(this.gridRows / 2); const midRow = Math.floor(this.gridRows() / 2);
return { return {
start: {row: midRow, col: 0}, start: {row: midRow, col: 0},
end: {row: midRow, col: this.gridCols - 1} end: {row: midRow, col: this.gridCols() - 1}
}; };
} }
} }
private createRandomStartEndPosition(): { start: GridPos; end: GridPos } { private createRandomStartEndPosition(): { start: GridPos; end: GridPos } {
const midCol = Math.floor(this.gridCols / 2); const midCol = Math.floor(this.gridCols() / 2);
const startRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); const startRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
const startCol: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1); const startCol: number = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
const endRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); const endRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
let endCol: number; let endCol: number;
if (startCol <= midCol) { if (startCol <= midCol) {
endCol = SharedFunctions.randomIntFromInterval(midCol + 1, this.gridCols - 1); endCol = SharedFunctions.randomIntFromInterval(midCol + 1, this.gridCols() - 1);
} else { } else {
endCol = SharedFunctions.randomIntFromInterval(0, midCol); endCol = SharedFunctions.randomIntFromInterval(0, midCol);
} }
@@ -358,12 +367,12 @@ export class PathfindingComponent implements AfterViewInit {
} }
private createRandomWalls() { private createRandomWalls() {
const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows); const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols() * this.gridRows());
for (let wall = 0; wall < maxNumberOfWalls; wall++) { for (let wall = 0; wall < maxNumberOfWalls; wall++) {
const row: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); const row: number = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
const col: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1); const col: number = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
if (!this.grid[row][col]) { // Use the grid passed from GenericGrid if (!this.grid[row][col]) { // Use the grid passed from GenericGrid
wall--; wall--;
@@ -382,8 +391,8 @@ export class PathfindingComponent implements AfterViewInit {
} }
private createVerticalWall() { private createVerticalWall() {
const height = this.gridRows; const height = this.gridRows();
const startCol = Math.floor(this.gridCols / 2); const startCol = Math.floor(this.gridCols() / 2);
for (let i = 5; i < (height - 5); i++) { for (let i = 5; i < (height - 5); i++) {
const row = i; const row = i;
@@ -404,8 +413,8 @@ export class PathfindingComponent implements AfterViewInit {
private createDiagonalWall() { private createDiagonalWall() {
// Diagonal-ish wall; avoids start/end // Diagonal-ish wall; avoids start/end
const len = Math.min(this.gridRows, this.gridCols); const len = Math.min(this.gridRows(), this.gridCols());
const startCol = Math.floor((this.gridCols - len) / 2); const startCol = Math.floor((this.gridCols() - len) / 2);
for (let i = 0; i < Math.max(0, len - 10); i++) { for (let i = 0; i < Math.max(0, len - 10); i++) {
const row = len - i - 1; const row = len - i - 1;
@@ -433,8 +442,8 @@ export class PathfindingComponent implements AfterViewInit {
} }
private clearPath(): void { private clearPath(): void {
for (let row = 0; row < this.gridRows; row++) { for (let row = 0; row < this.gridRows(); row++) {
for (let col = 0; col < this.gridCols; col++) { for (let col = 0; col < this.gridCols(); col++) {
const node = this.grid[row][col]; const node = this.grid[row][col];
node.isVisited = false; node.isVisited = false;
node.isPath = false; node.isPath = false;
@@ -8,34 +8,34 @@
<div class="grid grid-cols-2 tablet:grid-cols-1 tablet:gap-4 mb-6"> <div class="grid grid-cols-2 tablet:grid-cols-1 tablet:gap-4 mb-6">
<div class="flex items-center gap-4 mr-4"> <div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}</p> <p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}</p>
<ngx-slider class="grow" [(value)]="simParams.trailDecay" [options]="trailDecayOptions" ></ngx-slider> <ngx-slider class="grow" [(value)]="trailDecay" [options]="trailDecayOptions" ></ngx-slider>
</div> </div>
<div class="flex items-center gap-4 mr-4"> <div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.ATTRACTION' | translate }}</p> <p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.ATTRACTION' | translate }}</p>
<ngx-slider class="grow" [(value)]="simParams.g" [options]="gravityOptions" ></ngx-slider> <ngx-slider class="grow" [(value)]="g" [options]="gravityOptions" ></ngx-slider>
</div> </div>
<div class="flex items-center gap-4 mr-4"> <div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L1_LENGTH' | translate }}</p> <p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L1_LENGTH' | translate }}</p>
<ngx-slider class="grow" [(value)]="simParams.l1" [options]="lengthOptions" ></ngx-slider> <ngx-slider class="grow" [(value)]="l1" [options]="lengthOptions" ></ngx-slider>
</div> </div>
<div class="flex items-center gap-4 mr-4"> <div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L2_LENGTH' | translate }}</p> <p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L2_LENGTH' | translate }}</p>
<ngx-slider class="grow" [(value)]="simParams.l2" [options]="lengthOptions" ></ngx-slider> <ngx-slider class="grow" [(value)]="l2" [options]="lengthOptions" ></ngx-slider>
</div> </div>
<div class="flex items-center gap-4 mr-4"> <div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M1_MASS' | translate }}</p> <p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M1_MASS' | translate }}</p>
<ngx-slider class="grow" [(value)]="simParams.m1" [options]="massOptions" ></ngx-slider> <ngx-slider class="grow" [(value)]="m1" [options]="massOptions" ></ngx-slider>
</div> </div>
<div class="flex items-center gap-4 mr-4"> <div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M2_MASS' | translate }}</p> <p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M2_MASS' | translate }}</p>
<ngx-slider class="grow" [(value)]="simParams.m2" [options]="massOptions" ></ngx-slider> <ngx-slider class="grow" [(value)]="m2" [options]="massOptions" ></ngx-slider>
</div> </div>
<div class="flex items-center gap-4 mr-4 col-span-full"> <div class="flex items-center gap-4 mr-4 col-span-full">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.DAMPING' | translate }}</p> <p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.DAMPING' | translate }}</p>
<ngx-slider class="grow" [(value)]="simParams.damping" [options]="dampingOptions" ></ngx-slider> <ngx-slider class="grow" [(value)]="damping" [options]="dampingOptions" ></ngx-slider>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
import {Component, ChangeDetectionStrategy} from '@angular/core'; import {Component, signal} from '@angular/core';
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component'; import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
@@ -29,7 +29,6 @@ import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
Information, Information,
], ],
templateUrl: './pendulum.component.html', templateUrl: './pendulum.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './pendulum.component.scss', styleUrl: './pendulum.component.scss',
}) })
class PendulumComponent { class PendulumComponent {
@@ -103,16 +102,17 @@ class PendulumComponent {
hidePointerLabels: false hidePointerLabels: false
}; };
readonly simParams = { readonly trailDecay = signal(DEFAULT_TRAIL_DECAY);
readonly g = signal(DEFAULT_G);
readonly l1 = signal(DEFAULT_L1_LENGTH);
readonly l2 = signal(DEFAULT_L2_LENGTH);
readonly m1 = signal(DEFAULT_M1_MASS);
readonly m2 = signal(DEFAULT_M2_MASS);
readonly damping = signal(DEFAULT_DAMPING);
private readonly loop = {
time: 0, time: 0,
dt: 0.015, dt: 0.015,
g: DEFAULT_G,
m1: DEFAULT_M1_MASS,
m2: DEFAULT_M2_MASS,
l1: DEFAULT_L1_LENGTH,
l2: DEFAULT_L2_LENGTH,
damping: DEFAULT_DAMPING,
trailDecay: DEFAULT_TRAIL_DECAY,
impulseM1: 0.0, impulseM1: 0.0,
impulseM2: 0.0, impulseM2: 0.0,
}; };
@@ -147,10 +147,19 @@ class PendulumComponent {
private startParamUpdateLoop(scene: any): void { private startParamUpdateLoop(scene: any): void {
scene.onAfterRenderObservable.clear(); scene.onAfterRenderObservable.clear();
scene.onAfterRenderObservable.add(() => { scene.onAfterRenderObservable.add(() => {
this.simParams.time += this.simParams.dt; this.loop.time += this.loop.dt;
if (this.strategy) { if (this.strategy) {
this.strategy.updateParams({...this.simParams}); this.strategy.updateParams({
...this.loop,
g: this.g(),
m1: this.m1(),
m2: this.m2(),
l1: this.l1(),
l2: this.l2(),
damping: this.damping(),
trailDecay: this.trailDecay()
});
} }
this.resetImpulses(); this.resetImpulses();
@@ -158,22 +167,22 @@ class PendulumComponent {
} }
private resetImpulses() { private resetImpulses() {
if (this.simParams.impulseM1 !== 0.0) { if (this.loop.impulseM1 !== 0.0) {
this.simParams.impulseM1 = 0; this.loop.impulseM1 = 0;
} }
if (this.simParams.impulseM2 !== 0.0) { if (this.loop.impulseM2 !== 0.0) {
this.simParams.impulseM2 = 0; this.loop.impulseM2 = 0;
} }
} }
pushPendulum(m1: boolean) { pushPendulum(m1: boolean) {
if (m1) { if (m1) {
this.simParams.impulseM1 = IMPULSE_M1; this.loop.impulseM1 = IMPULSE_M1;
return; return;
} }
this.simParams.impulseM2 = IMPULSE_M2; this.loop.impulseM2 = IMPULSE_M2;
} }
resetPendulum() { resetPendulum() {
@@ -38,19 +38,20 @@
<mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }} <mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }}
</button> </button>
<button mat-raised-button (click)="toggleSound()"> <button mat-raised-button (click)="toggleSound()">
<mat-icon>{{ isSoundEnabled ? 'volume_up' : 'volume_off' }}</mat-icon> <mat-icon>{{ isSoundEnabled() ? 'volume_up' : 'volume_off' }}</mat-icon>
{{ isSoundEnabled ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }} {{ isSoundEnabled() ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }}
</button> </button>
</div> </div>
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]"> <div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p> <p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime() }} ms</p>
</div> </div>
<div class="flex items-end h-[clamp(200px,40vh,400px)] border-b border-app-fg mb-[clamp(10px,3vw,20px)] gap-px bg-card-bg"> <div class="flex items-end h-[clamp(200px,40vh,400px)] border-b border-app-fg mb-[clamp(10px,3vw,20px)] gap-px bg-card-bg">
@for (item of sortArray; track $index) { @for (item of sortArray(); track $index) {
<div <div
class="grow w-2.5 min-w-px transition-all duration-50" class="grow w-2.5 min-w-px transition-all duration-50"
[style.height.px]="item.value * 3" [style.height.px]="item.value * 3"
data-testid="sort-bar"
[ngClass]="{ [ngClass]="{
'bg-[#424242]': item.state === 'unsorted', 'bg-[#424242]': item.state === 'unsorted',
'bg-[#ffeb3b]': item.state === 'comparing', 'bg-[#ffeb3b]': item.state === 'comparing',
@@ -0,0 +1,71 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { By } from '@angular/platform-browser';
import { provideNoopAnimations } from '@angular/platform-browser/animations';
import { provideTranslateService } from '@ngx-translate/core';
import { SortingComponent } from './sorting.component';
import { SortingAudioService } from './service/sorting-audio.service';
/**
* The animation writes the bars from setTimeout callbacks. Under OnPush a bar
* only repaints when the sortArray signal is replaced, so these tests fail if
* the component goes back to mutating the array in place.
*/
describe('SortingComponent', () => {
let fixture: ComponentFixture<SortingComponent>;
beforeEach(async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
await TestBed.configureTestingModule({
imports: [SortingComponent],
providers: [
provideZonelessChangeDetection(),
provideNoopAnimations(),
provideTranslateService({ lang: 'en', fallbackLang: 'en' }),
{
provide: SortingAudioService,
useValue: { initOnUserGesture: vi.fn(), playTone: vi.fn(), playSortedSweep: vi.fn() },
},
],
}).compileComponents();
fixture = TestBed.createComponent(SortingComponent);
fixture.detectChanges();
});
afterEach(() => {
vi.useRealTimers();
});
function bars() {
return fixture.debugElement.queryAll(By.css('[data-testid="sort-bar"]'));
}
function heights(): number[] {
return bars().map(bar => Number.parseFloat((bar.nativeElement as HTMLElement).style.height));
}
it('shows every bar as sorted once the animation has run', () => {
fixture.componentInstance.startSorting();
vi.runAllTimers();
fixture.detectChanges();
expect(bars()).toHaveLength(50);
expect(bars().every(bar => bar.nativeElement.classList.contains('bg-[#4caf50]'))).toBe(true);
const sorted = heights();
expect(sorted).toEqual([...sorted].sort((a, b) => a - b));
});
it('restores the unsorted array when reset during the animation', () => {
const initial = heights();
fixture.componentInstance.startSorting();
vi.advanceTimersByTime(300);
fixture.componentInstance.resetSorting();
fixture.detectChanges();
expect(heights()).toEqual(initial);
expect(bars().some(bar => bar.nativeElement.classList.contains('bg-[#ffeb3b]'))).toBe(false);
});
});
@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, inject, OnInit, ChangeDetectionStrategy} from '@angular/core'; import {Component, inject, OnInit, signal} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {MatCardModule} from "@angular/material/card"; import {MatCardModule} from "@angular/material/card";
import {MatFormFieldModule} from "@angular/material/form-field"; import {MatFormFieldModule} from "@angular/material/form-field";
@@ -18,14 +18,12 @@ import {Information} from '../information/information';
selector: 'app-sorting', selector: 'app-sorting',
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslatePipe, FormsModule, MatInput, Information], imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslatePipe, FormsModule, MatInput, Information],
templateUrl: './sorting.component.html', templateUrl: './sorting.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './sorting.component.scss' styleUrl: './sorting.component.scss'
}) })
export class SortingComponent implements OnInit { export class SortingComponent implements OnInit {
private readonly sortingService: SortingService = inject(SortingService); private readonly sortingService: SortingService = inject(SortingService);
private readonly audioService: SortingAudioService = inject(SortingAudioService); private readonly audioService: SortingAudioService = inject(SortingAudioService);
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
readonly MAX_ARRAY_SIZE: number = 200; readonly MAX_ARRAY_SIZE: number = 200;
readonly MIN_ARRAY_SIZE: number = 20; readonly MIN_ARRAY_SIZE: number = 20;
@@ -69,14 +67,14 @@ export class SortingComponent implements OnInit {
}; };
private timeoutIds: number[] = []; private timeoutIds: number[] = [];
sortArray: SortData[] = []; readonly sortArray = signal<SortData[]>([]);
unsortedArrayCopy: SortData[] = []; unsortedArrayCopy: SortData[] = [];
arraySize = 50; readonly arraySize = signal(50);
maxArrayValue = 100; maxArrayValue = 100;
animationSpeed = 100; // Milliseconds per step animationSpeed = 100; // Milliseconds per step
selectedAlgorithm: string = this.algoInformation.entries[0].name; readonly selectedAlgorithm = signal(this.algoInformation.entries[0].name);
executionTime = 0; readonly executionTime = signal(0);
isSoundEnabled = false; readonly isSoundEnabled = signal(false);
ngOnInit(): void { ngOnInit(): void {
this.generateNewArray(); this.generateNewArray();
@@ -84,8 +82,9 @@ export class SortingComponent implements OnInit {
newArraySizeSet() newArraySizeSet()
{ {
this.arraySize = Math.min(Math.max(this.arraySize, this.MIN_ARRAY_SIZE), this.MAX_ARRAY_SIZE); const clamped = Math.min(Math.max(this.arraySize(), this.MIN_ARRAY_SIZE), this.MAX_ARRAY_SIZE);
if (this.arraySize == this.sortArray.length) this.arraySize.set(clamped);
if (clamped == this.sortArray().length)
{ {
return; return;
} }
@@ -94,35 +93,27 @@ export class SortingComponent implements OnInit {
generateNewArray(): void { generateNewArray(): void {
this.resetSorting(); this.resetSorting();
this.executionTime = 0; this.executionTime.set(0);
this.unsortedArrayCopy = []; const unsorted: SortData[] = [];
this.sortArray = [];
for (let i = 0; i < this.arraySize; i++) { for (let i = 0; i < this.arraySize(); i++) {
const randomValue = Math.floor(Math.random() * this.maxArrayValue) + 1; const randomValue = Math.floor(Math.random() * this.maxArrayValue) + 1;
this.sortArray.push({ unsorted.push({
value: randomValue,
state: 'unsorted'
});
this.unsortedArrayCopy.push({
value: randomValue, value: randomValue,
state: 'unsorted' state: 'unsorted'
}); });
} }
this.unsortedArrayCopy = unsorted;
this.resetSortState();
} }
private resetSortState() { private resetSortState() {
for (let i = 0; i < this.sortArray.length; i++) { this.sortArray.set(this.unsortedArrayCopy.map(item => ({...item})));
const element = this.sortArray[i];
const unsortedElement = this.unsortedArrayCopy[i];
element.value = unsortedElement.value;
element.state = 'unsorted';
}
} }
toggleSound(): void { toggleSound(): void {
this.isSoundEnabled = !this.isSoundEnabled; this.isSoundEnabled.update(v => !v);
} }
async startSorting(): Promise<void> { async startSorting(): Promise<void> {
@@ -132,26 +123,26 @@ export class SortingComponent implements OnInit {
const startTime = performance.now(); const startTime = performance.now();
let snapshots: SortSnapshot[] = []; let snapshots: SortSnapshot[] = [];
switch (this.selectedAlgorithm) { switch (this.selectedAlgorithm()) {
case 'Bubble Sort': case 'Bubble Sort':
snapshots = this.sortingService.bubbleSort(this.sortArray); snapshots = this.sortingService.bubbleSort(this.sortArray());
break; break;
case 'Quick Sort': case 'Quick Sort':
snapshots = this.sortingService.quickSort(this.sortArray); snapshots = this.sortingService.quickSort(this.sortArray());
break; break;
case 'Heap Sort': case 'Heap Sort':
snapshots = this.sortingService.heapSort(this.sortArray); snapshots = this.sortingService.heapSort(this.sortArray());
break; break;
case 'Cocktail Sort': case 'Cocktail Sort':
snapshots = this.sortingService.cocktailSort(this.sortArray); snapshots = this.sortingService.cocktailSort(this.sortArray());
break; break;
case 'Tim Sort': case 'Tim Sort':
snapshots = this.sortingService.timSort(this.sortArray); snapshots = this.sortingService.timSort(this.sortArray());
break; break;
} }
const endTime = performance.now(); const endTime = performance.now();
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4)); this.executionTime.set(Number.parseFloat((endTime - startTime).toFixed(4)));
this.animateSorting(snapshots); this.animateSorting(snapshots);
} }
@@ -159,14 +150,9 @@ export class SortingComponent implements OnInit {
private animateSorting(snapshots: SortSnapshot[]): void { private animateSorting(snapshots: SortSnapshot[]): void {
snapshots.forEach((snapshot, index) => { snapshots.forEach((snapshot, index) => {
const id = setTimeout(() => { const id = setTimeout(() => {
for (let i = 0; i < this.sortArray.length; i++) { this.sortArray.set(snapshot.array);
if (snapshot.array[i]) {
this.sortArray[i].value = snapshot.array[i].value;
this.sortArray[i].state = snapshot.array[i].state;
}
}
if (this.isSoundEnabled) { if (this.isSoundEnabled()) {
// Play a tone for each comparing bar (max 2 at once to avoid noise) // Play a tone for each comparing bar (max 2 at once to avoid noise)
snapshot.array snapshot.array
.filter(item => item.state === 'comparing') .filter(item => item.state === 'comparing')
@@ -174,13 +160,10 @@ export class SortingComponent implements OnInit {
.forEach(item => this.audioService.playTone(item.value, this.maxArrayValue, this.animationSpeed)); .forEach(item => this.audioService.playTone(item.value, this.maxArrayValue, this.animationSpeed));
} }
this.cdr.detectChanges();
if (index === snapshots.length - 1) { if (index === snapshots.length - 1) {
this.sortArray.forEach(item => item.state = 'sorted'); this.sortArray.set(snapshot.array.map(item => ({...item, state: 'sorted'})));
this.cdr.detectChanges(); if (this.isSoundEnabled()) {
if (this.isSoundEnabled) { this.audioService.playSortedSweep(this.sortArray().map(item => item.value), this.maxArrayValue);
this.audioService.playSortedSweep(this.sortArray.map(item => item.value), this.maxArrayValue);
} }
} }
}, index * this.animationSpeed); }, index * this.animationSpeed);
+1 -2
View File
@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component } from '@angular/core';
import {MatCard} from '@angular/material/card'; import {MatCard} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
import {SharedFunctions} from '../../shared/SharedFunctions'; import {SharedFunctions} from '../../shared/SharedFunctions';
@@ -10,7 +10,6 @@ import {SharedFunctions} from '../../shared/SharedFunctions';
TranslatePipe TranslatePipe
], ],
templateUrl: './imprint.component.html', templateUrl: './imprint.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './imprint.component.scss', styleUrl: './imprint.component.scss',
}) })
export class ImprintComponent { export class ImprintComponent {
@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { MatCard } from '@angular/material/card'; import { MatCard } from '@angular/material/card';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
@@ -8,7 +8,6 @@ import { RouterConstants } from '../../constants/RouterConstants';
@Component({ @Component({
selector: 'app-not-found', selector: 'app-not-found',
imports: [RouterLink, MatCard, MatButton, TranslatePipe], imports: [RouterLink, MatCard, MatButton, TranslatePipe],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './not-found.component.html', templateUrl: './not-found.component.html',
}) })
export class NotFoundComponent { export class NotFoundComponent {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, inject, CUSTOM_ELEMENTS_SCHEMA, ViewChild, ElementRef, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, inject, CUSTOM_ELEMENTS_SCHEMA, ViewChild, ElementRef} from '@angular/core';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogActions, MatDialogActions,
@@ -30,7 +30,6 @@ register();
MatButton, MatButton,
MatDialogClose MatDialogClose
], ],
changeDetection: ChangeDetectionStrategy.Eager,
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class ProjectDialogComponent implements AfterViewInit { export class ProjectDialogComponent implements AfterViewInit {
+1 -2
View File
@@ -1,4 +1,4 @@
import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { MatCardModule } from "@angular/material/card"; import { MatCardModule } from "@angular/material/card";
@@ -43,7 +43,6 @@ export interface Projects {
], ],
templateUrl: './projects.component.html', templateUrl: './projects.component.html',
styleUrl: './projects.component.scss', styleUrl: './projects.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
export class ProjectsComponent implements OnInit, OnDestroy { export class ProjectsComponent implements OnInit, OnDestroy {
@@ -1,4 +1,4 @@
import {Component, computed, effect, inject, OnDestroy, signal, WritableSignal, ChangeDetectionStrategy} from '@angular/core'; import {Component, computed, effect, inject, OnDestroy, signal, WritableSignal} from '@angular/core';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card'; import {MatCardModule} from '@angular/material/card';
@@ -30,7 +30,6 @@ const SECOND_INTERVAL_FREQUENCY_HZ = 587;
MatCheckboxModule, MatCheckboxModule,
TranslatePipe TranslatePipe
], ],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './stopwatch.component.html' templateUrl: './stopwatch.component.html'
}) })
export class StopwatchComponent implements OnDestroy { export class StopwatchComponent implements OnDestroy {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
export interface GridPos { row: number; col: number } export interface GridPos { row: number; col: number }
@@ -7,7 +7,6 @@ export interface GridPos { row: number; col: number }
selector: 'app-generic-grid', selector: 'app-generic-grid',
imports: [CommonModule], imports: [CommonModule],
templateUrl: './generic-grid.html', templateUrl: './generic-grid.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './generic-grid.scss', styleUrl: './generic-grid.scss',
}) })
export class GenericGridComponent implements AfterViewInit { export class GenericGridComponent implements AfterViewInit {
@@ -1,11 +1,10 @@
import {AfterViewInit, Component, ElementRef, HostListener, inject, NgZone, OnDestroy, PLATFORM_ID, ViewChild, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, ElementRef, HostListener, inject, NgZone, OnDestroy, PLATFORM_ID, ViewChild} from '@angular/core';
import {isPlatformBrowser} from '@angular/common'; import {isPlatformBrowser} from '@angular/common';
@Component({ @Component({
selector: 'app-particles-background', selector: 'app-particles-background',
imports: [], imports: [],
templateUrl: './particles-background.component.html', templateUrl: './particles-background.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './particles-background.component.scss', styleUrl: './particles-background.component.scss',
}) })
export class ParticleBackgroundComponent implements AfterViewInit, OnDestroy { export class ParticleBackgroundComponent implements AfterViewInit, OnDestroy {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild, ChangeDetectionStrategy} from '@angular/core'; import {AfterViewInit, Component, ElementRef, EventEmitter, inject, input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {TranslateService} from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core'; import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
@@ -26,7 +26,6 @@ export interface SceneEventData {
selector: 'app-babylon-canvas', selector: 'app-babylon-canvas',
imports: [], imports: [],
templateUrl: './babylon-canvas.component.html', templateUrl: './babylon-canvas.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './babylon-canvas.component.scss', styleUrl: './babylon-canvas.component.scss',
}) })
export class BabylonCanvas implements AfterViewInit, OnDestroy { export class BabylonCanvas implements AfterViewInit, OnDestroy {
@@ -37,8 +36,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
@ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>; @ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
@Input({ required: true }) config!: RenderConfig; readonly config = input.required<RenderConfig>();
@Input() renderCallback?: RenderCallback; readonly renderCallback = input<RenderCallback>();
@Output() sceneReady = new EventEmitter<SceneEventData>(); @Output() sceneReady = new EventEmitter<SceneEventData>();
@Output() sceneResized = new EventEmitter<SceneEventData>(); @Output() sceneResized = new EventEmitter<SceneEventData>();
@@ -131,7 +130,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
} }
private setupCamera(canvas: HTMLCanvasElement) { private setupCamera(canvas: HTMLCanvasElement) {
if (this.config.mode === '3D') { if (this.config().mode === '3D') {
this.setup3dCamera(canvas); this.setup3dCamera(canvas);
return; return;
} }
@@ -144,7 +143,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
cam.mode = Camera.ORTHOGRAPHIC_CAMERA; cam.mode = Camera.ORTHOGRAPHIC_CAMERA;
const aspect = canvas.width / canvas.height; const aspect = canvas.width / canvas.height;
const viewSize = this.config?.initialViewSize ?? 10; const viewSize = this.config().initialViewSize ?? 10;
cam.orthoLeft = -viewSize * aspect / 2; cam.orthoLeft = -viewSize * aspect / 2;
cam.orthoRight = viewSize * aspect / 2; cam.orthoRight = viewSize * aspect / 2;
cam.orthoTop = viewSize / 2; cam.orthoTop = viewSize / 2;
@@ -161,17 +160,18 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
cam.maxZ = 100; cam.maxZ = 100;
cam.lowerRadiusLimit = 1.5; cam.lowerRadiusLimit = 1.5;
cam.upperRadiusLimit = 20; cam.upperRadiusLimit = 20;
cam.radius = this.config?.initialViewSize ?? 1; cam.radius = this.config().initialViewSize ?? 1;
cam.attachControl(canvas, true); cam.attachControl(canvas, true);
this.camera = cam; this.camera = cam;
} }
private createFullScreenRect() { private createFullScreenRect() {
if (!this.config.vertexShader || !this.config.fragmentShader) { const config = this.config();
if (!config.vertexShader || !config.fragmentShader) {
return; return;
} }
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene); const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
if (this.config.mode === '3D') { if (config.mode === '3D') {
plane.parent = this.camera; plane.parent = this.camera;
plane.position.z = 1; plane.position.z = 1;
} else { } else {
@@ -183,7 +183,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
} }
private createShaderMaterial() { private createShaderMaterial() {
if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) { const config = this.config();
if (!config.vertexShader || !config.fragmentShader || !config.uniformNames) {
return; return;
} }
@@ -191,14 +192,14 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
"shaderMaterial", "shaderMaterial",
this.scene, this.scene,
{ {
vertexSource: this.config.vertexShader, vertexSource: config.vertexShader,
fragmentSource: this.config.fragmentShader fragmentSource: config.fragmentShader
}, },
{ {
attributes: ["position", "uv"], attributes: ["position", "uv"],
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames], uniforms: ["resolution", "cameraPosition", ...config.uniformNames],
uniformBuffers: this.config.uniformBufferNames ?? [], uniformBuffers: config.uniformBufferNames ?? [],
shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL shaderLanguage: config.shaderLanguage ?? ShaderLanguage.GLSL
} }
); );
this.shaderMaterial.disableDepthWrite = true; this.shaderMaterial.disableDepthWrite = true;
@@ -209,8 +210,9 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
this.engine.runRenderLoop(() => { this.engine.runRenderLoop(() => {
// callback call to call specific uniforms // callback call to call specific uniforms
if (this.renderCallback) { const callback = this.renderCallback();
this.renderCallback(this.shaderMaterial, this.camera, canvas, this.scene); if (callback) {
callback(this.shaderMaterial, this.camera, canvas, this.scene);
} }
// default uniforms which maybe each scene has // default uniforms which maybe each scene has
@@ -228,8 +230,9 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
this.engine.resize(); this.engine.resize();
} }
if (this.config.mode === '2D' && this.camera instanceof ArcRotateCamera) { const config = this.config();
const viewSize = this.config?.initialViewSize ?? 10; if (config.mode === '2D' && this.camera instanceof ArcRotateCamera) {
const viewSize = config.initialViewSize ?? 10;
this.camera.orthoLeft = -viewSize / 2; this.camera.orthoLeft = -viewSize / 2;
this.camera.orthoRight = viewSize / 2; this.camera.orthoRight = viewSize / 2;
this.camera.orthoTop = viewSize / 2; this.camera.orthoTop = viewSize / 2;