Merge remote-tracking branch 'origin/main'
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,3 +44,4 @@ Thumbs.db
|
||||
|
||||
# Lighthouse
|
||||
.lighthouseci/
|
||||
.claude/settings.local.json
|
||||
|
||||
91
CLAUDE.md
Normal file
91
CLAUDE.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Claude Code Guidelines
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is the frontend of the Playground project, built with Angular 21 and Angular Material. It includes features like a light/dark theme toggle and multi-language support via ngx-translate. The application is a static Single Page Application (SPA) served by NGINX.
|
||||
|
||||
**Key Technologies:**
|
||||
* **Frontend Framework:** Angular 21
|
||||
* **UI Components & Theming:** Angular Material
|
||||
* **Internationalization:** ngx-translate
|
||||
* **Server:** NGINX (for serving the SPA)
|
||||
* **Containerization:** Docker
|
||||
* **CI/CD:** GitHub Actions
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Local Development
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. **Start development server:**
|
||||
```bash
|
||||
ng serve --open
|
||||
```
|
||||
The app will run at `http://localhost:4200`.
|
||||
|
||||
### Building for Production
|
||||
|
||||
To build the project for production, which creates the optimized static files:
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
## Language
|
||||
- All code must be written in **English** — including variable names, function names, comments, and commit messages.
|
||||
|
||||
## Clean Code
|
||||
- Write simple, clear, and readable code.
|
||||
- Avoid deeply nested or chained calls. Break complex expressions into named intermediate variables or separate steps.
|
||||
- Code should be understandable by junior developers without additional explanation.
|
||||
|
||||
## Braces
|
||||
- Always use curly braces `{}` for branches and loops — even for single-line bodies.
|
||||
|
||||
```ts
|
||||
// ✅ correct
|
||||
if (isValid) {
|
||||
doSomething();
|
||||
}
|
||||
|
||||
// ❌ avoid
|
||||
if (isValid) doSomething();
|
||||
```
|
||||
|
||||
## Functions
|
||||
- Extract reusable or logically distinct logic into separate, well-named functions.
|
||||
- A function should do one thing and do it well.
|
||||
- Prefer short functions that are easy to test and understand.
|
||||
|
||||
## Comments
|
||||
- Only add comments when they explain the **why** or the **idea** behind the code — not the **what**.
|
||||
- Do not comment on things that are already obvious from the code itself.
|
||||
|
||||
```ts
|
||||
// ✅ useful comment — explains intent
|
||||
// Retry once to handle transient network errors
|
||||
const result = await fetchWithRetry(url);
|
||||
|
||||
// ❌ useless comment — just restates the code
|
||||
// Call fetchWithRetry with url
|
||||
const result = await fetchWithRetry(url);
|
||||
```
|
||||
|
||||
## Development Conventions
|
||||
|
||||
* **Language:** TypeScript
|
||||
* **Framework:** Angular
|
||||
* **Styling:** SCSS (based on `styles.scss` and component-specific `.scss` files).
|
||||
* **Linting:** ESLint is configured (see `eslint.config.js` and `package.json` scripts).
|
||||
* **Internationalization:** Uses `ngx-translate` with `en.json` and `de.json` asset files.
|
||||
|
||||
## Project Structure (Key Areas)
|
||||
|
||||
* `src/app/`: Contains the main application logic, components, services, and routing.
|
||||
* `src/app/pages/`: Specific pages of the application (e.g., about, algorithms, imprint, projects).
|
||||
* `src/assets/`: Static assets including images, internationalization files (`i18n`), and logos.
|
||||
* `Dockerfile`: Defines the Docker image for the application.
|
||||
* `nginx.conf`: NGINX configuration for serving the SPA.
|
||||
* `.gitea/workflows/`: Contains CI/CD workflows (e.g., `build-Frontend-a.yml`).
|
||||
3124
package-lock.json
generated
3124
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -12,34 +12,35 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-slider/ngx-slider": "^21.0.0",
|
||||
"@angular/animations": "~21.1.0",
|
||||
"@angular/cdk": "~21.1.0",
|
||||
"@angular/common": "~21.1.0",
|
||||
"@angular/compiler": "~21.1.0",
|
||||
"@angular/core": "~21.1.0",
|
||||
"@angular/forms": "~21.1.0",
|
||||
"@angular/material": "~21.1.0",
|
||||
"@angular/platform-browser": "~21.1.0",
|
||||
"@angular/router": "~21.1.0",
|
||||
"@babylonjs/core": "^8.50.5",
|
||||
"@ngx-translate/core": "~17.0.0",
|
||||
"@ngx-translate/http-loader": "~17.0.0",
|
||||
"inquirer": "^13.2.2",
|
||||
"@angular/animations": "~21.2.1",
|
||||
"@angular/cdk": "~21.2.1",
|
||||
"@angular/common": "~21.2.1",
|
||||
"@angular/compiler": "~21.2.1",
|
||||
"@angular/core": "~21.2.1",
|
||||
"@angular/forms": "~21.2.1",
|
||||
"@angular/material": "~21.2.1",
|
||||
"@angular/platform-browser": "~21.2.1",
|
||||
"@angular/router": "~21.2.1",
|
||||
"@babylonjs/core": "^8.54.1",
|
||||
"@ngx-translate/core": "^17.0.0",
|
||||
"@ngx-translate/http-loader": "^17.0.0",
|
||||
"inquirer": "^13.3.0",
|
||||
"rxjs": "~7.8.2",
|
||||
"swiper": "~12.1.0",
|
||||
"tslib": "~2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "~21.1.0",
|
||||
"@angular/cli": "~21.1.0",
|
||||
"@angular/compiler-cli": "~21.1.0",
|
||||
"@angular/build": "~21.2.1",
|
||||
"@angular/cli": "~21.2.1",
|
||||
"@angular/compiler-cli": "~21.2.1",
|
||||
"@eslint/js": "~10.0.1",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@types/jasmine": "~5.1.15",
|
||||
"angular-eslint": "21.2.0",
|
||||
"eslint": "^9.39.2",
|
||||
"jasmine-core": "~6.0.1",
|
||||
"@types/jasmine": "~6.0.0",
|
||||
"angular-eslint": "21.3.0",
|
||||
"eslint": "^10.0.3",
|
||||
"jasmine-core": "~6.1.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "8.50.1"
|
||||
"typescript-eslint": "8.56.1"
|
||||
},
|
||||
"overrides": {
|
||||
"tmp": "^0.2.3"
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import {AboutComponent} from './pages/about/about.component';
|
||||
import {RouterConstants} from './constants/RouterConstants';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: AboutComponent },
|
||||
{ path: RouterConstants.ABOUT.PATH, component: RouterConstants.ABOUT.COMPONENT},
|
||||
{ path: RouterConstants.PROJECTS.PATH, component: RouterConstants.PROJECTS.COMPONENT},
|
||||
{ path: RouterConstants.ALGORITHMS.PATH, component: RouterConstants.ALGORITHMS.COMPONENT},
|
||||
{ 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},
|
||||
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT},
|
||||
{ path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT},
|
||||
{ path: RouterConstants.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT},
|
||||
{ path: RouterConstants.PENDULUM.PATH, component: RouterConstants.PENDULUM.COMPONENT},
|
||||
{ path: RouterConstants.CLOTH.PATH, component: RouterConstants.CLOTH.COMPONENT}
|
||||
{ path: '', loadComponent: () => import('./pages/about/about.component').then(m => m.AboutComponent) },
|
||||
{ path: RouterConstants.ABOUT.PATH, loadComponent: () => import('./pages/about/about.component').then(m => m.AboutComponent) },
|
||||
{ path: RouterConstants.PROJECTS.PATH, loadComponent: () => import('./pages/projects/projects.component').then(m => m.ProjectsComponent) },
|
||||
{ path: RouterConstants.ALGORITHMS.PATH, loadComponent: () => import('./pages/algorithms/algorithms.component').then(m => m.AlgorithmsComponent) },
|
||||
{ path: RouterConstants.PATHFINDING.PATH, loadComponent: () => import('./pages/algorithms/pathfinding/pathfinding.component').then(m => m.PathfindingComponent) },
|
||||
{ path: RouterConstants.SORTING.PATH, loadComponent: () => import('./pages/algorithms/sorting/sorting.component').then(m => m.SortingComponent) },
|
||||
{ path: RouterConstants.IMPRINT.PATH, loadComponent: () => import('./pages/imprint/imprint.component').then(m => m.ImprintComponent) },
|
||||
{ path: RouterConstants.GOL.PATH, loadComponent: () => import('./pages/algorithms/conway-gol/conway-gol.component').then(m => m.ConwayGolComponent) },
|
||||
{ path: RouterConstants.LABYRINTH.PATH, loadComponent: () => import('./pages/algorithms/pathfinding/labyrinth/labyrinth.component').then(m => m.LabyrinthComponent) },
|
||||
{ path: RouterConstants.FRACTAL.PATH, loadComponent: () => import('./pages/algorithms/fractal/fractal.component').then(m => m.FractalComponent) },
|
||||
{ path: RouterConstants.FRACTAL3d.PATH, loadComponent: () => import('./pages/algorithms/fractal3d/fractal3d.component').then(m => m.Fractal3dComponent) },
|
||||
{ path: RouterConstants.PENDULUM.PATH, loadComponent: () => import('./pages/algorithms/pendulum/pendulum.component').then(m => m.default) },
|
||||
{ path: RouterConstants.CLOTH.PATH, loadComponent: () => import('./pages/algorithms/cloth/cloth.component').then(m => m.ClothComponent) },
|
||||
{ path: RouterConstants.FOUR_COLOR.PATH, loadComponent: () => import('./pages/algorithms/four-color/four-color.component').then(m => m.FourColorComponent) },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,88 +1,68 @@
|
||||
import {AboutComponent} from '../pages/about/about.component';
|
||||
import {ProjectsComponent} from '../pages/projects/projects.component';
|
||||
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 {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.component';
|
||||
import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/labyrinth.component';
|
||||
import {FractalComponent} from '../pages/algorithms/fractal/fractal.component';
|
||||
import {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component';
|
||||
import PendulumComponent from '../pages/algorithms/pendulum/pendulum.component';
|
||||
import {ClothComponent} from '../pages/algorithms/cloth/cloth.component';
|
||||
|
||||
export class RouterConstants {
|
||||
export class RouterConstants {
|
||||
|
||||
static readonly ABOUT = {
|
||||
PATH: 'about',
|
||||
LINK: '/about',
|
||||
COMPONENT: AboutComponent
|
||||
};
|
||||
|
||||
static readonly PROJECTS = {
|
||||
PATH: 'projects',
|
||||
LINK: '/projects',
|
||||
COMPONENT: ProjectsComponent
|
||||
};
|
||||
|
||||
static readonly ALGORITHMS = {
|
||||
PATH: 'algorithms',
|
||||
LINK: '/algorithms',
|
||||
COMPONENT: AlgorithmsComponent
|
||||
};
|
||||
|
||||
static readonly PATHFINDING = {
|
||||
PATH: 'algorithms/pathfinding',
|
||||
LINK: '/algorithms/pathfinding',
|
||||
COMPONENT: PathfindingComponent
|
||||
};
|
||||
|
||||
static readonly SORTING = {
|
||||
PATH: 'algorithms/sorting',
|
||||
LINK: '/algorithms/sorting',
|
||||
COMPONENT: SortingComponent
|
||||
};
|
||||
|
||||
static readonly GOL = {
|
||||
PATH: 'algorithms/gol',
|
||||
LINK: '/algorithms/gol',
|
||||
COMPONENT: ConwayGolComponent
|
||||
};
|
||||
|
||||
static readonly LABYRINTH = {
|
||||
PATH: 'algorithms/labyrinth',
|
||||
LINK: '/algorithms/labyrinth',
|
||||
COMPONENT: LabyrinthComponent
|
||||
};
|
||||
|
||||
static readonly FRACTAL = {
|
||||
PATH: 'algorithms/fractal',
|
||||
LINK: '/algorithms/fractal',
|
||||
COMPONENT: FractalComponent
|
||||
};
|
||||
|
||||
static readonly FRACTAL3d = {
|
||||
PATH: 'algorithms/fractal3d',
|
||||
LINK: '/algorithms/fractal3d',
|
||||
COMPONENT: Fractal3dComponent
|
||||
};
|
||||
|
||||
static readonly PENDULUM = {
|
||||
PATH: 'algorithms/pendulum',
|
||||
LINK: '/algorithms/pendulum',
|
||||
COMPONENT: PendulumComponent
|
||||
};
|
||||
|
||||
static readonly CLOTH = {
|
||||
PATH: 'algorithms/cloth',
|
||||
LINK: '/algorithms/cloth',
|
||||
COMPONENT: ClothComponent
|
||||
};
|
||||
|
||||
static readonly FOUR_COLOR = {
|
||||
PATH: 'algorithms/four_color',
|
||||
LINK: '/algorithms/four_color',
|
||||
};
|
||||
|
||||
static readonly IMPRINT = {
|
||||
PATH: 'imprint',
|
||||
LINK: '/imprint',
|
||||
COMPONENT: ImprintComponent
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
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 TIM_SORT_WIKI = 'https://de.wikipedia.org/wiki/Timsort'
|
||||
static readonly CONWAYS_WIKI = 'https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens'
|
||||
static readonly PRIMS_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Prim'
|
||||
static readonly KRUSKAL_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Kruskal'
|
||||
@@ -22,5 +23,6 @@
|
||||
static readonly XPBD_WIKI = 'https://www.emergentmind.com/topics/extended-position-based-dynamics-xpbd'
|
||||
static readonly GPU_COMPUTING_WIKI = 'https://en.wikipedia.org/wiki/General-purpose_computing_on_graphics_processing_units'
|
||||
static readonly DATA_STRUCTURE_WIKI = 'https://de.wikipedia.org/wiki/Datenstruktur'
|
||||
static readonly FOUR_COLOR_THEOREM = 'https://de.wikipedia.org/wiki/Vier-Farben-Satz'
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {ParticleBackgroundComponent} from '../../shared/components/particles-bac
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
|
||||
@@ -4,7 +4,6 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [MatDialogModule, MatButtonModule, MatIconModule],
|
||||
template: `
|
||||
<div class="dialog">
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
</a>
|
||||
|
||||
<nav class="nav">
|
||||
<a [routerLink]="RouterConstants.ABOUT.LINK" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
|
||||
<a [routerLink]="RouterConstants.PROJECTS.LINK" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
|
||||
<a [routerLink]="RouterConstants.ALGORITHMS.LINK" mat-button>{{ 'TOPBAR.ALGORITHMS' | translate }}</a>
|
||||
<a [routerLink]="RouterConstants.IMPRINT.LINK" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
|
||||
<a [routerLink]="RouterConstants.ABOUT.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
|
||||
<a [routerLink]="RouterConstants.PROJECTS.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
|
||||
<a [routerLink]="RouterConstants.ALGORITHMS.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.ALGORITHMS' | translate }}</a>
|
||||
<a [routerLink]="RouterConstants.IMPRINT.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile nav menu button -->
|
||||
|
||||
@@ -52,6 +52,37 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
opacity: 0.72;
|
||||
transition: opacity 150ms ease;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
border-radius: 2px;
|
||||
transform: scaleX(0);
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
|
||||
&::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@@ -14,9 +14,8 @@ import {RouterConstants} from '../../constants/RouterConstants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-topbar',
|
||||
standalone: true,
|
||||
imports: [
|
||||
RouterLink,
|
||||
RouterLink, RouterLinkActive,
|
||||
MatToolbarModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule,
|
||||
TranslateModule, MatDivider
|
||||
],
|
||||
|
||||
@@ -14,7 +14,6 @@ import {SharedFunctions} from '../../shared/SharedFunctions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgOptimizedImage,
|
||||
MatCardModule,
|
||||
|
||||
@@ -3,4 +3,5 @@ export interface AlgorithmCategory {
|
||||
title: string;
|
||||
description: string;
|
||||
routerLink: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<div class="card-grid">
|
||||
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
|
||||
<h1 class="algo-page-title">{{ 'ALGORITHM.TITLE' | translate }}</h1>
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
@for (category of categories$ | async; track category.id) {
|
||||
@for (category of categories; track category.id) {
|
||||
<mat-card class="algo-card" [routerLink]="[category.routerLink]">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ category.title | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>{{ category.description | translate}}</p>
|
||||
<div class="algo-icon-wrap">
|
||||
<mat-icon>{{ category.icon }}</mat-icon>
|
||||
</div>
|
||||
<h3 class="algo-card-title">{{ category.title | translate }}</h3>
|
||||
<p class="algo-card-desc">{{ category.description | translate }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AlgorithmsService } from './algorithms.service';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-algorithms',
|
||||
templateUrl: './algorithms.component.html',
|
||||
styleUrls: ['./algorithms.component.scss'],
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, MatCardModule, TranslatePipe],
|
||||
styleUrl: './algorithms.component.scss',
|
||||
imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe],
|
||||
})
|
||||
export class AlgorithmsComponent implements OnInit {
|
||||
export class AlgorithmsComponent {
|
||||
private readonly algorithmsService = inject(AlgorithmsService);
|
||||
|
||||
categories$: Observable<AlgorithmCategory[]> | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.categories$ = this.algorithmsService.getCategories();
|
||||
}
|
||||
readonly categories: AlgorithmCategory[] = this.algorithmsService.getCategories();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {RouterConstants} from '../../constants/RouterConstants';
|
||||
|
||||
@Injectable({
|
||||
@@ -13,53 +12,68 @@ export class AlgorithmsService {
|
||||
id: 'pathfinding',
|
||||
title: 'ALGORITHM.PATHFINDING.TITLE',
|
||||
description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
|
||||
routerLink: RouterConstants.PATHFINDING.LINK
|
||||
routerLink: RouterConstants.PATHFINDING.LINK,
|
||||
icon: 'route'
|
||||
},
|
||||
{
|
||||
id: 'sorting',
|
||||
title: 'ALGORITHM.SORTING.TITLE',
|
||||
description: 'ALGORITHM.SORTING.DESCRIPTION',
|
||||
routerLink: RouterConstants.SORTING.LINK
|
||||
routerLink: RouterConstants.SORTING.LINK,
|
||||
icon: 'sort'
|
||||
},
|
||||
{
|
||||
id: 'gameOfLife',
|
||||
title: 'ALGORITHM.GOL.TITLE',
|
||||
description: 'ALGORITHM.GOL.DESCRIPTION',
|
||||
routerLink: RouterConstants.GOL.LINK
|
||||
routerLink: RouterConstants.GOL.LINK,
|
||||
icon: 'grid_on'
|
||||
},
|
||||
{
|
||||
id: 'labyrinth',
|
||||
title: 'ALGORITHM.LABYRINTH.TITLE',
|
||||
description: 'ALGORITHM.LABYRINTH.DESCRIPTION',
|
||||
routerLink: RouterConstants.LABYRINTH.LINK
|
||||
routerLink: RouterConstants.LABYRINTH.LINK,
|
||||
icon: 'grid_view'
|
||||
},
|
||||
{
|
||||
id: 'fractal',
|
||||
title: 'ALGORITHM.FRACTAL.TITLE',
|
||||
description: 'ALGORITHM.FRACTAL.DESCRIPTION',
|
||||
routerLink: RouterConstants.FRACTAL.LINK
|
||||
routerLink: RouterConstants.FRACTAL.LINK,
|
||||
icon: 'blur_on'
|
||||
},
|
||||
{
|
||||
id: 'fractal3d',
|
||||
title: 'ALGORITHM.FRACTAL3D.TITLE',
|
||||
description: 'ALGORITHM.FRACTAL3D.DESCRIPTION',
|
||||
routerLink: RouterConstants.FRACTAL3d.LINK
|
||||
routerLink: RouterConstants.FRACTAL3d.LINK,
|
||||
icon: 'view_in_ar'
|
||||
},
|
||||
{
|
||||
id: 'pendulum',
|
||||
title: 'ALGORITHM.PENDULUM.TITLE',
|
||||
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
||||
routerLink: RouterConstants.PENDULUM.LINK
|
||||
routerLink: RouterConstants.PENDULUM.LINK,
|
||||
icon: 'rotate_right'
|
||||
},
|
||||
{
|
||||
id: 'cloth',
|
||||
title: 'ALGORITHM.CLOTH.TITLE',
|
||||
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
||||
routerLink: RouterConstants.CLOTH.LINK
|
||||
routerLink: RouterConstants.CLOTH.LINK,
|
||||
icon: 'texture'
|
||||
},
|
||||
{
|
||||
id: 'fourColor',
|
||||
title: 'ALGORITHM.FOUR_COLOR.TITLE',
|
||||
description: 'ALGORITHM.FOUR_COLOR.DESCRIPTION',
|
||||
routerLink: RouterConstants.FOUR_COLOR.LINK,
|
||||
icon: 'palette'
|
||||
}
|
||||
];
|
||||
|
||||
getCategories(): Observable<AlgorithmCategory[]> {
|
||||
return of(this.categories);
|
||||
getCategories(): AlgorithmCategory[] {
|
||||
return this.categories;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
<button mat-raised-button color="primary" (click)="toggleMesh()">
|
||||
{{ isOutlineActive ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }}
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="restartSimulation()">
|
||||
{{ 'CLOTH.RESTART_SIMULATION' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="sliders-panel">
|
||||
<span>{{ 'CLOTH.ELONGATION' | translate }}: {{ elongation }}</span>
|
||||
<mat-slider min="0.5" max="2.0" step="0.1">
|
||||
<input matSliderThumb [(ngModel)]="elongation">
|
||||
</mat-slider>
|
||||
</div>
|
||||
</div>
|
||||
<app-babylon-canvas
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { BabylonCanvas, RenderConfig, SceneEventData } from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh, WebGPUEngine, Scene} from '@babylonjs/core';
|
||||
@@ -31,6 +33,8 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
TranslatePipe,
|
||||
BabylonCanvas,
|
||||
MatButton,
|
||||
MatSliderModule,
|
||||
FormsModule,
|
||||
Information
|
||||
],
|
||||
templateUrl: './cloth.component.html',
|
||||
@@ -42,6 +46,9 @@ export class ClothComponent {
|
||||
private clothMesh: GroundMesh | null = null;
|
||||
public isWindActive: boolean = false;
|
||||
public isOutlineActive: boolean = false;
|
||||
public stiffness: number = 80;
|
||||
// Elongation along the vertical (Y) axis, 0.5 = compressed, 2.0 = stretched
|
||||
public elongation: number = 1.0;
|
||||
|
||||
public renderConfig: RenderConfig = {
|
||||
mode: '3D',
|
||||
@@ -103,6 +110,11 @@ export class ClothComponent {
|
||||
this.clothMesh.material.wireframe = this.isOutlineActive;
|
||||
}
|
||||
|
||||
public restartSimulation(): void {
|
||||
this.simulationTime = 0;
|
||||
this.createSimulation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and starts the cloth simulation.
|
||||
*/
|
||||
@@ -164,9 +176,13 @@ export class ClothComponent {
|
||||
const constraintsP2: number[] = [];
|
||||
const constraintsP3: number[] = [];
|
||||
|
||||
const addConstraint = (arr: number[], a: number, b: number): void => {
|
||||
// Type 1.0 = horizontal/diagonal (no elongation), Type 2.0 = vertical (elongation applies)
|
||||
const addHorizontalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 1.0);
|
||||
};
|
||||
const addVerticalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 2.0);
|
||||
};
|
||||
|
||||
// Fill positions (Pin top row)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
@@ -186,14 +202,14 @@ export class ClothComponent {
|
||||
|
||||
// Graph Coloring (4 Phases)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) addHorizontalConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) addHorizontalConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
}
|
||||
for (let y = 0; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
for (let x = 0; x < config.gridWidth; x++) addVerticalConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
for (let y = 1; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
for (let x = 0; x < config.gridWidth; x++) addVerticalConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
|
||||
const constraintsP4: number[] = [];
|
||||
@@ -228,7 +244,7 @@ export class ClothComponent {
|
||||
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||
],
|
||||
params: new Float32Array(8)
|
||||
params: new Float32Array(9)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -331,7 +347,7 @@ export class ClothComponent {
|
||||
// 6. RENDER LOOP
|
||||
// ========================================================================
|
||||
private startRenderLoop(engine: WebGPUEngine, scene: Scene, config: ClothConfig, buffers: ClothBuffers, pipelines: ClothPipelines): void {
|
||||
const paramsData = new Float32Array(8);
|
||||
const paramsData = new Float32Array(9);
|
||||
|
||||
// Pre-calculate constraint dispatch sizes for the 4 phases
|
||||
const constraintsLength = buffers.constraints.map(b => (b as any)._buffer.capacity / 4 / 4); // Elements / vec4 length
|
||||
@@ -347,16 +363,23 @@ export class ClothComponent {
|
||||
const windX = this.isWindActive ? 5.0 : 0.0;
|
||||
const windY = 0.0;
|
||||
const windZ = this.isWindActive ? 15.0 : 0.0;
|
||||
const scaledCompliance = 0.00001 * config.particleInvMass * config.spacing;
|
||||
|
||||
// Logarithmic compliance: stiffness=1 → very soft fabric, stiffness=100 → rigid metal sheet.
|
||||
// alpha = compliance / dt² must be >> wSum (≈800) to be soft, << wSum to be rigid.
|
||||
const softCompliance = 10.0;
|
||||
const rigidCompliance = 0.00001;
|
||||
const t = (this.stiffness - 1) / 99.0;
|
||||
const compliance = softCompliance * Math.pow(rigidCompliance / softCompliance, t);
|
||||
|
||||
paramsData[0] = 0.016; // dt
|
||||
paramsData[1] = -9.81; // gravity
|
||||
paramsData[2] = scaledCompliance;
|
||||
paramsData[2] = compliance;
|
||||
paramsData[3] = config.numVertices;
|
||||
paramsData[4] = windX;
|
||||
paramsData[5] = windY;
|
||||
paramsData[6] = windZ;
|
||||
paramsData[7] = this.simulationTime;
|
||||
paramsData[8] = this.elongation;
|
||||
|
||||
buffers.params.update(paramsData);
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ export const CLOTH_SHARED_STRUCTS = `
|
||||
wind_x: f32,
|
||||
wind_y: f32,
|
||||
wind_z: f32,
|
||||
time: f32
|
||||
time: f32,
|
||||
elongation: f32
|
||||
};
|
||||
`;
|
||||
|
||||
@@ -26,9 +27,8 @@ export const CLOTH_VERTEX_SHADER_WGSL = `
|
||||
|
||||
uniform viewProjection : mat4x4<f32>;
|
||||
|
||||
// Varyings, um Daten an den Fragment-Shader zu senden
|
||||
varying vUV : vec2<f32>;
|
||||
varying vWorldPos : vec3<f32>; // NEU: Wir brauchen die 3D-Position für das Licht!
|
||||
varying vWorldPos : vec3<f32>;
|
||||
|
||||
@vertex
|
||||
fn main(input : VertexInputs) -> FragmentInputs {
|
||||
@@ -38,7 +38,7 @@ export const CLOTH_VERTEX_SHADER_WGSL = `
|
||||
output.position = uniforms.viewProjection * vec4<f32>(worldPos, 1.0);
|
||||
|
||||
output.vUV = input.uv;
|
||||
output.vWorldPos = worldPos; // Position weitergeben
|
||||
output.vWorldPos = worldPos;
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -133,13 +133,15 @@ export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
||||
if (idx >= arrayLength(&constraints)) { return; }
|
||||
|
||||
let constraint = constraints[idx];
|
||||
let isActive = constraint.w;
|
||||
|
||||
if (isActive < 0.5) { return; }
|
||||
// constraint.w: 0.0 = inactive, 1.0 = horizontal/diagonal, 2.0 = vertical
|
||||
if (constraint.w < 0.5) { return; }
|
||||
|
||||
let idA = u32(constraint.x);
|
||||
let idB = u32(constraint.y);
|
||||
let restLength = constraint.z;
|
||||
|
||||
// constraint.w encodes type: 1.0 = horizontal/diagonal, 2.0 = vertical (elongation applies)
|
||||
let restLength =constraint.z * p.elongation;
|
||||
|
||||
var pA = positions[idA];
|
||||
var pB = positions[idB];
|
||||
|
||||
@@ -178,10 +178,9 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
|
||||
async startGame(): Promise<void> {
|
||||
this.gameStarted.set(true);
|
||||
let lifeIsDead = false;
|
||||
while (this.gameStarted()){
|
||||
const startTime = performance.now();
|
||||
lifeIsDead = true;
|
||||
let lifeIsDead = true;
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
lifeIsDead = this.checkLifeRules(row, col, this.writeGrid) && lifeIsDead;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<mat-card class="algo-container">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'FOUR_COLOR.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
|
||||
<div class="controls-container">
|
||||
<div class="controls-panel">
|
||||
<button mat-flat-button color="primary" (click)="generateNewMap()">{{ 'FOUR_COLOR.GENERATE' | translate }}</button>
|
||||
<button mat-flat-button color="accent" (click)="autoSolve()">{{ 'FOUR_COLOR.SOLVE' | translate }}</button>
|
||||
<button mat-stroked-button (click)="resetColors()">{{ 'FOUR_COLOR.CLEAR' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-panel">
|
||||
<div class="input-container">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridRows"
|
||||
(ngModelChange)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridCols"
|
||||
(ngModelChange)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><span class="legend-color color1"></span> {{ 'FOUR_COLOR.COLOR_1' | translate }}</span>
|
||||
<span><span class="legend-color color2"></span> {{ 'FOUR_COLOR.COLOR_2' | translate }}</span>
|
||||
<span><span class="legend-color color3"></span> {{ 'FOUR_COLOR.COLOR_3' | translate }}</span>
|
||||
<span><span class="legend-color color4"></span> {{ 'FOUR_COLOR.COLOR_4' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="status-panel" [ngClass]="solutionStatus.toLowerCase()">
|
||||
<span class="status-label">{{ 'FOUR_COLOR.STATUS.LABEL' | translate }}:</span>
|
||||
<span class="status-message">{{ 'FOUR_COLOR.STATUS.' + solutionStatus | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-container">
|
||||
<canvas #fourColorCanvas
|
||||
(mousedown)="onMouseDown($event)"
|
||||
(mousemove)="onMouseMove($event)"
|
||||
(touchstart)="onTouchStart($event)"
|
||||
(touchmove)="onTouchMove($event)"
|
||||
></canvas>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
.status-panel {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--app-fg);
|
||||
border-left: 5px solid #9e9e9e;
|
||||
font-weight: 500;
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-panel.incomplete {
|
||||
border-left-color: #9e9e9e;
|
||||
background-color: var(--app-bg);
|
||||
}
|
||||
|
||||
.status-panel.solved {
|
||||
border-left-color: #4CAF50;
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-panel.conflicts {
|
||||
border-left-color: #ff9800;
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.status-panel.invalid {
|
||||
border-left-color: #f44336;
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.color1 { background-color: #FF5252; }
|
||||
.color2 { background-color: #448AFF; }
|
||||
.color3 { background-color: #4CAF50; }
|
||||
.color4 { background-color: #FFEB3B; }
|
||||
|
||||
canvas {
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
374
src/app/pages/algorithms/four-color/four-color.component.ts
Normal file
374
src/app/pages/algorithms/four-color/four-color.component.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, FourColorNode, Region} from './four-color.models';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {Information} from '../information/information';
|
||||
import {GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||
import {SharedFunctions} from '../../../shared/SharedFunctions';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-four-color',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
TranslateModule,
|
||||
Information
|
||||
],
|
||||
templateUrl: './four-color.component.html',
|
||||
styleUrl: './four-color.component.scss'
|
||||
})
|
||||
export class FourColorComponent implements AfterViewInit {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'FOUR_COLOR.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: 'FOUR_COLOR.TITLE',
|
||||
translateName: true,
|
||||
description: 'FOUR_COLOR.EXPLANATION.EXPLANATION',
|
||||
link: UrlConstants.FOUR_COLOR_THEOREM
|
||||
}
|
||||
],
|
||||
disclaimer: 'FOUR_COLOR.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: 'FOUR_COLOR.EXPLANATION.DISCLAIMER_BOTTOM',
|
||||
disclaimerListEntry: [
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_1',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_2',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_3',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_4'
|
||||
]
|
||||
};
|
||||
|
||||
gridRows = DEFAULT_GRID_ROWS;
|
||||
gridCols = DEFAULT_GRID_COLS;
|
||||
grid: FourColorNode[][] = [];
|
||||
regions: Region[] = [];
|
||||
executionTime = 0;
|
||||
solutionStatus: 'INCOMPLETE' | 'SOLVED' | 'CONFLICTS' | 'INVALID' = 'INCOMPLETE';
|
||||
|
||||
@ViewChild('fourColorCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
private nodeSize = 0;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ctx = this.canvasRef.nativeElement.getContext('2d')!;
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
applyGridSize(): void {
|
||||
if (this.gridRows < MIN_GRID_SIZE) this.gridRows = MIN_GRID_SIZE;
|
||||
if (this.gridRows > MAX_GRID_SIZE) this.gridRows = 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();
|
||||
}
|
||||
|
||||
initializeGrid(): void {
|
||||
this.grid = [];
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
const row: FourColorNode[] = [];
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
row.push({
|
||||
row: r,
|
||||
col: c,
|
||||
regionId: -1,
|
||||
color: 0,
|
||||
});
|
||||
}
|
||||
this.grid.push(row);
|
||||
}
|
||||
|
||||
this.generateRegions();
|
||||
this.resizeCanvas();
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
private resizeCanvas(): void {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const maxDim = Math.max(this.gridRows, this.gridCols);
|
||||
this.nodeSize = Math.floor(MAX_GRID_PX / maxDim);
|
||||
|
||||
canvas.width = this.gridCols * this.nodeSize;
|
||||
canvas.height = this.gridRows * this.nodeSize;
|
||||
}
|
||||
|
||||
generateRegions(): void {
|
||||
const numRegions = Math.floor((this.gridRows * this.gridCols) / 30);
|
||||
this.regions = [];
|
||||
const seeds = this.determineSeeds(numRegions);
|
||||
this.regionGrowth(seeds);
|
||||
this.determineAdjacency();
|
||||
}
|
||||
|
||||
private determineAdjacency() {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
const currentRegionId = this.grid[row][col].regionId;
|
||||
const neighbors = this.getNeighbors(row, col);
|
||||
for (const neighbor of neighbors) {
|
||||
const neighborRegionId = this.grid[neighbor.row][neighbor.col].regionId;
|
||||
if (neighborRegionId !== -1 && neighborRegionId !== currentRegionId) {
|
||||
this.regions[currentRegionId].neighbors.add(neighborRegionId);
|
||||
this.regions[neighborRegionId].neighbors.add(currentRegionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private regionGrowth(seeds: GridPos[]) {
|
||||
const queue: GridPos[] = [...seeds];
|
||||
while (queue.length > 0) {
|
||||
const {row, col} = queue.shift()!;
|
||||
const regionId = this.grid[row][col].regionId;
|
||||
|
||||
const neighbors = this.getNeighbors(row, col);
|
||||
for (const neighbor of neighbors) {
|
||||
if (this.grid[neighbor.row][neighbor.col].regionId === -1) {
|
||||
this.grid[neighbor.row][neighbor.col].regionId = regionId;
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private determineSeeds(numRegions: number) {
|
||||
const seeds: GridPos[] = [];
|
||||
for (let i = 0; i < numRegions; i++) {
|
||||
let r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
let c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
while (this.grid[r][c].regionId !== -1) {
|
||||
r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
}
|
||||
this.grid[r][c].regionId = i;
|
||||
seeds.push({row: r, col: c});
|
||||
this.regions.push({id: i, color: 0, neighbors: new Set<number>()});
|
||||
}
|
||||
return seeds;
|
||||
}
|
||||
|
||||
private getNeighbors(row: number, col: number): GridPos[] {
|
||||
const res: GridPos[] = [];
|
||||
if (row > 0) 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 < this.gridCols - 1) res.push({row, col: col + 1});
|
||||
return res;
|
||||
}
|
||||
|
||||
drawGrid(): void {
|
||||
if (!this.ctx) return;
|
||||
|
||||
this.ctx.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height);
|
||||
|
||||
// 1. Draw Cell Backgrounds
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
const node = this.grid[r][c];
|
||||
this.ctx.fillStyle = this.getNodeColor(node);
|
||||
this.ctx.fillRect(c * this.nodeSize, r * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw Region Borders
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
const currentRegion = this.grid[r][c].regionId;
|
||||
|
||||
// Right border
|
||||
if (c < this.gridCols - 1 && this.grid[r][c+1].regionId !== currentRegion) {
|
||||
this.ctx.moveTo((c + 1) * this.nodeSize, r * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
if (r < this.gridRows - 1 && this.grid[r+1][c].regionId !== currentRegion) {
|
||||
this.ctx.moveTo(c * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.ctx.stroke();
|
||||
|
||||
// 3. Draw Outer Border
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.strokeRect(0, 0, this.gridCols * this.nodeSize, this.gridRows * this.nodeSize);
|
||||
}
|
||||
|
||||
private getNodeColor(node: FourColorNode): string {
|
||||
switch (node.color) {
|
||||
case 1: return '#FF5252'; // Red
|
||||
case 2: return '#448AFF'; // Blue
|
||||
case 3: return '#4CAF50'; // Green
|
||||
case 4: return '#FFEB3B'; // Yellow
|
||||
default: return 'white';
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent): void {
|
||||
const pos = this.getGridPos(event);
|
||||
if (pos) this.handleInteraction(pos);
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent): void {
|
||||
if (event.buttons !== 1){
|
||||
return;
|
||||
}
|
||||
this.getGridPos(event);
|
||||
}
|
||||
|
||||
onTouchStart(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
const touch = event.touches[0];
|
||||
const pos = this.getGridPos(touch);
|
||||
if (pos) this.handleInteraction(pos);
|
||||
}
|
||||
|
||||
onTouchMove(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private getGridPos(event: MouseEvent | Touch): GridPos | null {
|
||||
const rect = this.canvasRef.nativeElement.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
const col = Math.floor(x / (rect.width / this.gridCols));
|
||||
const row = Math.floor(y / (rect.height / this.gridRows));
|
||||
|
||||
if (row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols) {
|
||||
return {row, col};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private handleInteraction(pos: GridPos): void {
|
||||
const node = this.grid[pos.row][pos.col];
|
||||
if (node.regionId === -1){
|
||||
return;
|
||||
}
|
||||
|
||||
const region = this.regions[node.regionId];
|
||||
region.color = (region.color % 4) + 1;
|
||||
this.updateRegionColors(region);
|
||||
this.checkSolution();
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
private updateRegionColors(region: Region): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
if (this.grid[row][col].regionId === region.id) {
|
||||
this.grid[row][col].color = region.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetColors(): void {
|
||||
for (const region of this.regions) {
|
||||
region.color = 0;
|
||||
this.updateRegionColors(region);
|
||||
}
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
autoSolve(): void {
|
||||
const startTime = performance.now();
|
||||
this.resetColors();
|
||||
|
||||
const success = this.backtrackSolve(0);
|
||||
const endTime = performance.now();
|
||||
this.executionTime = endTime - startTime;
|
||||
|
||||
if (success) {
|
||||
this.checkSolution();
|
||||
this.drawGrid();
|
||||
} else {
|
||||
const message = this.translate.instant('FOUR_COLOR.ALERT.NO_SOLUTION');
|
||||
this.snackBar.open(message, 'ALERT');
|
||||
}
|
||||
}
|
||||
|
||||
private backtrackSolve(regionIndex: number): boolean {
|
||||
if (regionIndex === this.regions.length) return true;
|
||||
|
||||
const region = this.regions[regionIndex];
|
||||
const availableColors = [1, 2, 3, 4];
|
||||
|
||||
for (const color of availableColors) {
|
||||
if (this.isColorValid(region, color)) {
|
||||
region.color = color;
|
||||
this.updateRegionColors(region);
|
||||
|
||||
if (this.backtrackSolve(regionIndex + 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
region.color = 0;
|
||||
//this.updateRegionColors(region);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isColorValid(region: Region, color: number): boolean {
|
||||
for (const neighborId of region.neighbors) {
|
||||
if (this.regions[neighborId].color === color){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
checkSolution(): void {
|
||||
let allColored = true;
|
||||
let hasConflicts = false;
|
||||
|
||||
for (const region of this.regions) {
|
||||
if (region.color === 0) {
|
||||
allColored = false;
|
||||
}
|
||||
if (region.color > 0 && !this.isColorValid(region, region.color)) {
|
||||
hasConflicts = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConflicts) {
|
||||
this.solutionStatus = allColored ? 'INVALID' : 'CONFLICTS';
|
||||
} else {
|
||||
this.solutionStatus = allColored ? 'SOLVED' : 'INCOMPLETE';
|
||||
}
|
||||
}
|
||||
|
||||
generateNewMap(): void {
|
||||
this.initializeGrid();
|
||||
}
|
||||
}
|
||||
18
src/app/pages/algorithms/four-color/four-color.models.ts
Normal file
18
src/app/pages/algorithms/four-color/four-color.models.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface FourColorNode {
|
||||
row: number;
|
||||
col: number;
|
||||
regionId: number;
|
||||
color: number;
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: number;
|
||||
color: number;
|
||||
neighbors: Set<number>;
|
||||
}
|
||||
|
||||
export const DEFAULT_GRID_ROWS = 25;
|
||||
export const DEFAULT_GRID_COLS = 25;
|
||||
export const MIN_GRID_SIZE = 20;
|
||||
export const MAX_GRID_SIZE = 38;
|
||||
export const MAX_GRID_PX = 600;
|
||||
@@ -6,6 +6,7 @@ import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
|
||||
@@ -27,7 +28,6 @@ enum NodeType {
|
||||
|
||||
@Component({
|
||||
selector: 'app-pathfinding',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@@ -48,6 +48,7 @@ enum NodeType {
|
||||
export class PathfindingComponent implements AfterViewInit {
|
||||
private readonly pathfindingService = inject(PathfindingService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
readonly NodeType = NodeType;
|
||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
@@ -483,7 +484,8 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
return true;
|
||||
}
|
||||
|
||||
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
|
||||
const message = this.translate.instant('PATHFINDING.ALERT.START_END_NODES');
|
||||
this.snackBar.open(message, 'OK', { duration: 5000, horizontalPosition: 'center', verticalPosition: 'top' });
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SortingAudioService {
|
||||
private audioContext: AudioContext | null = null;
|
||||
|
||||
private ensureContext(): AudioContext {
|
||||
this.audioContext ??= new AudioContext();
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
return this.audioContext;
|
||||
}
|
||||
|
||||
// Call this on the user gesture (button click) so the AudioContext can be created/resumed.
|
||||
initOnUserGesture(): void {
|
||||
this.ensureContext();
|
||||
}
|
||||
|
||||
playTone(value: number, maxValue: number, animationSpeedMs: number): void {
|
||||
const ctx = this.ensureContext();
|
||||
const frequency = this.valueToFrequency(value, maxValue);
|
||||
|
||||
// Keep tone duration slightly shorter than the animation frame to avoid overlap
|
||||
const duration = Math.min(0.1, (animationSpeedMs * 0.75) / 1000);
|
||||
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gainNode = ctx.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
|
||||
oscillator.type = 'sawtooth';
|
||||
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
|
||||
|
||||
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
||||
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + duration);
|
||||
}
|
||||
|
||||
playSortedSweep(sortedValues: number[], maxValue: number): void {
|
||||
const ctx = this.ensureContext();
|
||||
// Play a quick ascending sweep through all sorted bar values
|
||||
const step = Math.ceil(sortedValues.length / 40);
|
||||
sortedValues.forEach((value, i) => {
|
||||
if (i % step !== 0) {
|
||||
return;
|
||||
}
|
||||
const delayMs = (i / step) * 18;
|
||||
setTimeout(() => {
|
||||
const frequency = this.valueToFrequency(value, maxValue);
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gainNode = ctx.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
|
||||
|
||||
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.06);
|
||||
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + 0.06);
|
||||
}, delayMs);
|
||||
});
|
||||
}
|
||||
|
||||
// Maps a bar value linearly to the frequency range 180–1100 Hz (roughly 3 octaves)
|
||||
private valueToFrequency(value: number, maxValue: number): number {
|
||||
const minFreq = 400;
|
||||
const maxFreq = 800;
|
||||
return minFreq + (value / maxValue) * (maxFreq - minFreq);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class SortingService {
|
||||
|
||||
let start = -1;
|
||||
let end = array.length-1;
|
||||
let changed = false;
|
||||
let changed: boolean;
|
||||
do {
|
||||
changed = false;
|
||||
start += 1;
|
||||
@@ -224,6 +224,103 @@ export class SortingService {
|
||||
}
|
||||
}
|
||||
|
||||
// --- TIM SORT ---
|
||||
timSort(array: SortData[]): SortSnapshot[] {
|
||||
const snapshots: SortSnapshot[] = [];
|
||||
const arr = array.map(item => ({ ...item }));
|
||||
const n = arr.length;
|
||||
const RUN = 32;
|
||||
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
// Step 1: Sort small runs with insertion sort
|
||||
for (let i = 0; i < n; i += RUN) {
|
||||
const end = Math.min(i + RUN - 1, n - 1);
|
||||
this.insertionSortRange(arr, i, end, snapshots);
|
||||
}
|
||||
|
||||
// Step 2: Merge the sorted runs
|
||||
for (let size = RUN; size < n; size *= 2) {
|
||||
for (let left = 0; left < n; left += 2 * size) {
|
||||
const mid = Math.min(left + size - 1, n - 1);
|
||||
const right = Math.min(left + 2 * size - 1, n - 1);
|
||||
if (mid < right) {
|
||||
this.mergeRanges(arr, left, mid, right, snapshots);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arr.forEach(item => item.state = 'sorted');
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
private insertionSortRange(arr: SortData[], left: number, right: number, snapshots: SortSnapshot[]): void {
|
||||
for (let i = left + 1; i <= right; i++) {
|
||||
const tempValue = arr[i].value;
|
||||
arr[i].state = 'comparing';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
let j = i - 1;
|
||||
while (j >= left && arr[j].value > tempValue) {
|
||||
arr[j].state = 'comparing';
|
||||
arr[j + 1].value = arr[j].value;
|
||||
arr[j].state = 'unsorted';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
j--;
|
||||
}
|
||||
|
||||
arr[j + 1].value = tempValue;
|
||||
arr[i].state = 'unsorted';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
}
|
||||
}
|
||||
|
||||
private mergeRanges(arr: SortData[], left: number, mid: number, right: number, snapshots: SortSnapshot[]): void {
|
||||
const leftPart = arr.slice(left, mid + 1).map(item => ({ ...item }));
|
||||
const rightPart = arr.slice(mid + 1, right + 1).map(item => ({ ...item }));
|
||||
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
let k = left;
|
||||
|
||||
while (i < leftPart.length && j < rightPart.length) {
|
||||
// Highlight the write target and the right-side source (arr[mid+1+j] is still
|
||||
// untouched in the array since k never overtakes it during a merge)
|
||||
const rightSourceIdx = mid + 1 + j;
|
||||
arr[k].state = 'comparing';
|
||||
arr[rightSourceIdx].state = 'comparing';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
arr[rightSourceIdx].state = 'unsorted';
|
||||
|
||||
if (leftPart[i].value <= rightPart[j].value) {
|
||||
arr[k].value = leftPart[i].value;
|
||||
i++;
|
||||
} else {
|
||||
arr[k].value = rightPart[j].value;
|
||||
j++;
|
||||
}
|
||||
|
||||
arr[k].state = 'unsorted';
|
||||
k++;
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
}
|
||||
|
||||
while (i < leftPart.length) {
|
||||
arr[k].value = leftPart[i].value;
|
||||
i++;
|
||||
k++;
|
||||
}
|
||||
|
||||
while (j < rightPart.length) {
|
||||
arr[k].value = rightPart[j].value;
|
||||
j++;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
|
||||
private swap(arr: SortData[], i: number, j: number) {
|
||||
const temp = arr[i].value;
|
||||
arr[i].value = arr[j].value;
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
<button mat-raised-button (click)="generateNewArray()">
|
||||
<mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button (click)="toggleSound()">
|
||||
<mat-icon>{{ isSoundEnabled ? 'volume_up' : 'volume_off' }}</mat-icon>
|
||||
{{ isSoundEnabled ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-panel">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import { SortingService } from './service/sorting.service';
|
||||
import { SortingAudioService } from './service/sorting-audio.service';
|
||||
import {SortData, SortSnapshot} from './sorting.models';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {MatInput} from '@angular/material/input';
|
||||
@@ -15,14 +16,14 @@ 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, Information],
|
||||
templateUrl: './sorting.component.html',
|
||||
styleUrls: ['./sorting.component.scss']
|
||||
styleUrl: './sorting.component.scss'
|
||||
})
|
||||
export class SortingComponent implements OnInit {
|
||||
|
||||
private readonly sortingService: SortingService = inject(SortingService);
|
||||
private readonly audioService: SortingAudioService = inject(SortingAudioService);
|
||||
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
||||
|
||||
readonly MAX_ARRAY_SIZE: number = 200;
|
||||
@@ -50,6 +51,11 @@ export class SortingComponent implements OnInit {
|
||||
name: 'Heap Sort',
|
||||
description: 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION',
|
||||
link: UrlConstants.HEAP_SORT_WIKI
|
||||
},
|
||||
{
|
||||
name: 'Tim Sort',
|
||||
description: 'SORTING.EXPLANATION.TIMSORT_EXPLANATION',
|
||||
link: UrlConstants.TIM_SORT_WIKI
|
||||
}
|
||||
],
|
||||
disclaimer: 'SORTING.EXPLANATION.DISCLAIMER',
|
||||
@@ -66,9 +72,10 @@ export class SortingComponent implements OnInit {
|
||||
unsortedArrayCopy: SortData[] = [];
|
||||
arraySize = 50;
|
||||
maxArrayValue = 100;
|
||||
animationSpeed = 50; // Milliseconds per step
|
||||
animationSpeed = 100; // Milliseconds per step
|
||||
selectedAlgorithm: string = this.algoInformation.entries[0].name;
|
||||
executionTime = 0;
|
||||
isSoundEnabled = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.generateNewArray();
|
||||
@@ -113,8 +120,14 @@ export class SortingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSound(): void {
|
||||
this.isSoundEnabled = !this.isSoundEnabled;
|
||||
}
|
||||
|
||||
async startSorting(): Promise<void> {
|
||||
this.resetSorting();
|
||||
// Init the AudioContext on this user gesture so the browser allows sound
|
||||
this.audioService.initOnUserGesture();
|
||||
const startTime = performance.now();
|
||||
let snapshots: SortSnapshot[] = [];
|
||||
|
||||
@@ -131,6 +144,9 @@ export class SortingComponent implements OnInit {
|
||||
case 'Cocktail Sort':
|
||||
snapshots = this.sortingService.cocktailSort(this.sortArray);
|
||||
break;
|
||||
case 'Tim Sort':
|
||||
snapshots = this.sortingService.timSort(this.sortArray);
|
||||
break;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
@@ -149,11 +165,22 @@ export class SortingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isSoundEnabled) {
|
||||
// Play a tone for each comparing bar (max 2 at once to avoid noise)
|
||||
snapshot.array
|
||||
.filter(item => item.state === 'comparing')
|
||||
.slice(0, 2)
|
||||
.forEach(item => this.audioService.playTone(item.value, this.maxArrayValue, this.animationSpeed));
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
|
||||
if (index === snapshots.length - 1) {
|
||||
this.sortArray.forEach(item => item.state = 'sorted');
|
||||
this.cdr.detectChanges();
|
||||
if (this.isSoundEnabled) {
|
||||
this.audioService.playSortedSweep(this.sortArray.map(item => item.value), this.maxArrayValue);
|
||||
}
|
||||
}
|
||||
}, index * this.animationSpeed);
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ import {MatButton} from '@angular/material/button';
|
||||
@Component({
|
||||
selector: 'app-project-dialog',
|
||||
templateUrl: './project-dialog.component.html',
|
||||
styleUrls: ['./project-dialog.component.scss'],
|
||||
standalone: true,
|
||||
styleUrl: './project-dialog.component.scss',
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface Projects {
|
||||
|
||||
@Component({
|
||||
selector: 'app-projects',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatCardModule,
|
||||
MatChipsModule,
|
||||
|
||||
@@ -10,10 +10,9 @@ export class LanguageService {
|
||||
readonly lang = signal<Lang>(this.getInitial());
|
||||
|
||||
constructor() {
|
||||
this.translate.addLangs(['de', 'en']);
|
||||
this.translate.setFallbackLang('en');
|
||||
this.lang.set(this.getInitial());
|
||||
this.use(this.lang());
|
||||
// translate service lang and fallback are already configured via provideTranslateService in app.config
|
||||
// just ensure the stored preference is active on startup
|
||||
this.translate.use(this.lang());
|
||||
}
|
||||
|
||||
use(l: Lang) {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Injectable, NgZone, signal } from '@angular/core';
|
||||
import {LocalStoreConstants} from '../constants/LocalStoreConstants';
|
||||
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ReloadService {
|
||||
private readonly _reloadTick = signal(0);
|
||||
readonly reloadTick = this._reloadTick.asReadonly();
|
||||
|
||||
private readonly _languageChangedTick = signal(0);
|
||||
readonly languageChangedTick = this._languageChangedTick.asReadonly();
|
||||
|
||||
|
||||
|
||||
|
||||
private informListeners(e: StorageEvent, zone: NgZone) {
|
||||
if (e.key === LocalStoreConstants.LANGUAGE_KEY) {
|
||||
zone.run(() => this._languageChangedTick.update(v => v + 1));
|
||||
}
|
||||
}
|
||||
|
||||
bumpLanguageChanged(): void {
|
||||
this._reloadTick.update(v => v + 1);
|
||||
localStorage.setItem(LocalStoreConstants.RELOAD_ALL_LANG_LISTENER_KEY, String(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ export interface GridPos { row: number; col: number }
|
||||
|
||||
@Component({
|
||||
selector: 'app-generic-grid',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './generic-grid.html',
|
||||
styleUrl: './generic-grid.scss',
|
||||
@@ -36,7 +35,7 @@ export class GenericGridComponent implements AfterViewInit {
|
||||
grid: any[][] = [];
|
||||
|
||||
isDrawing = false;
|
||||
private lastCell: GridPos | null = null;
|
||||
protected lastCell: GridPos | null = null;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ctx = this.getContextOrThrow();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
||||
|
||||
export interface RenderConfig {
|
||||
@@ -26,6 +28,8 @@ export interface SceneEventData {
|
||||
})
|
||||
export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
readonly ngZone = inject(NgZone);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
@ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@@ -52,19 +56,16 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
|
||||
const canvas = this.canvasRef?.nativeElement;
|
||||
if (canvas) {
|
||||
canvas.removeEventListener('wheel', this.wheelHandler);
|
||||
}
|
||||
canvas?.removeEventListener('wheel', this.wheelHandler);
|
||||
|
||||
if (this.engine) {
|
||||
this.engine.dispose();
|
||||
}
|
||||
this.engine?.dispose();
|
||||
}
|
||||
|
||||
private async initBabylon(): Promise<void> {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
this.engine = new WebGPUEngine(canvas);
|
||||
await this.engine.initAsync().then(() => {
|
||||
const tmpEngine = new WebGPUEngine(canvas);
|
||||
await tmpEngine.initAsync().then(() => {
|
||||
this.engine = tmpEngine;
|
||||
this.scene = new Scene(this.engine);
|
||||
this.setupCamera(canvas);
|
||||
this.addListener(canvas);
|
||||
@@ -75,6 +76,12 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
engine: this.engine
|
||||
});
|
||||
this.addRenderLoop(canvas);
|
||||
|
||||
})
|
||||
.catch(() => {
|
||||
const message = this.translate.instant('WEBGPU.NOT_SUPPORTED');
|
||||
this.snackBar.open(message, 'OK', { duration: 8000, horizontalPosition: "center", verticalPosition: "top" });
|
||||
this.engine = null!;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
},
|
||||
"TRIBBLE": {
|
||||
"TITLE": "Homeserver 'Tribble'",
|
||||
"DESCRIPTION": "In diesem Projekt geht es um die Einrichtung und Wartung meines eigenen Homeservers. Er betreibt mehrere Docker-Container wie Gitea, Jellyfin und mehr. Es ist eine großartige Lernerfahrung im Bereich Self-Hosting und Systemadministration.",
|
||||
"DESCRIPTION": "In diesem Projekt geht es um die Einrichtung und Wartung meines eigenen Homeservers. Er betreibt mehrere Docker-Container wie Gitea, Jellyfin and more. Es ist eine großartige Lernerfahrung im Bereich Self-Hosting und Systemadministration.",
|
||||
"LINK_INTERNAL": "Projektdetails",
|
||||
"HIGHLIGHTS": {
|
||||
"P1": "Self-Hosting verschiedener Dienste mit Docker.",
|
||||
@@ -273,7 +273,7 @@
|
||||
"BULLET_1": "Entwicklung mit Angular 19+ und Material Design.",
|
||||
"BULLET_2": "Implementierung performanter Visualisierungen (WebGPU, Shader, Canvas).",
|
||||
"BULLET_3": "Automatisierte CI/CD-Pipelines und Containerisierung mit Docker.",
|
||||
"BULLET_4": "Internationalisierung (i18n) für globale Reichweite.",
|
||||
"BULLET_4": "Internationalization (i18n) für globale Reichweite.",
|
||||
"CHALLENGE_1": "Optimierung der Render-Performance bei komplexen 3D-Fraktalen in Echtzeit.",
|
||||
"CHALLENGE_2": "Architektur einer skalierbaren und wartbaren Frontend-Struktur für diverse Sub-Projekte.",
|
||||
"LEARNING_1": "Effektives UI/UX-Design für komplexe datengesteuerte Visualisierungen.",
|
||||
@@ -310,7 +310,7 @@
|
||||
"TITLE": "Rapid Prototyping & Game Jams",
|
||||
"SHORT_DESCRIPTION": "Sammlung innovativer Spielkonzepte, entstanden in unter 48 Stunden.",
|
||||
"INTRODUCTION": "Teilnahme an nationalen Wettbewerben (z.B. Beansjam). Hier geht es darum, unter extremem Zeitdruck funktionale und spaßige Prototypen zu erschaffen.",
|
||||
"BULLET_1": "Fokus auf 'Core Game Loop' und schnelles Feedback.",
|
||||
"BULLET_1": "Fokus on 'Core Game Loop' und schnelles Feedback.",
|
||||
"BULLET_2": "Kollaborative Entwicklung in kleinen, agilen Teams.",
|
||||
"BULLET_3": "Effektives Zeitmanagement und Scope-Kontrolle.",
|
||||
"BULLET_4": "Veröffentlichung und Iteration basierend auf Community-Votings.",
|
||||
@@ -371,6 +371,8 @@
|
||||
"START": "Sortierung starten",
|
||||
"RESET": "Zurücksetzen",
|
||||
"GENERATE_NEW_ARRAY": "Neues Array generieren",
|
||||
"SOUND_ON": "Ton an",
|
||||
"SOUND_OFF": "Ton aus",
|
||||
"EXECUTION_TIME": "Ausführungszeit",
|
||||
"ARRAY_SIZE": "Anzahl der Balken",
|
||||
"EXPLANATION": {
|
||||
@@ -378,6 +380,7 @@
|
||||
"BUBBLE_SORT_EXPLANATION":"vergleicht wiederholt benachbarte Elemente und tauscht sie, wenn sie in der falschen Reihenfolge stehen. Das größte Element \"blubbert\" dabei wie eine Luftblase ans Ende der Liste. Vorteil: Extrem einfach zu verstehen und zu implementieren; erkennt bereits sortierte Listen sehr schnell. Nachteil: Sehr ineffizient bei großen Listen (Laufzeit O(n²)). In der Praxis kaum genutzt.",
|
||||
"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.",
|
||||
"TIMSORT_EXPLANATION": "ist ein hybrider Sortieralgorithmus, der aus Merge Sort und Insertion Sort kombiniert ist. Er unterteilt das Array in kleine 'Runs' und sortiert jeden davon mit Insertion Sort, um sie anschließend schrittweise mit Merge Sort zusammenzuführen. Vorteil: Extrem effizient bei realen Daten, die oft teilweise sortiert sind – O(n log n) im schlechtesten und O(n) im besten Fall. Er ist der Standardalgorithmus in Python und Java. Nachteil: Komplexer zu implementieren als ein reiner Algorithmus und benötigt zusätzlichen Speicher für den Merge-Schritt.",
|
||||
"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.",
|
||||
"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).",
|
||||
@@ -434,7 +437,7 @@
|
||||
"MANDELBROT_EXPLANATION": "basiert auf der iterativen Formel 'z_{n+1} = z_n^2 + c'. Sie prüft für jeden Punkt in der komplexen Ebene, ob die Zahlenfolge stabil bleibt oder ins Unendliche entkommt. Vorteil: Gilt als 'Apfelmännchen' und Mutter der Fraktale. Sie bietet eine unendliche Vielfalt an selbstähnlichen Strukturen, in die man ewig hineinzoomen kann.",
|
||||
"JULIA_EXPLANATION": "nutzt dieselbe Formel wie Mandelbrot, fixiert jedoch den Parameter 'c' und variiert den Startwert. Je nach Wahl von 'c' entstehen filigrane, wolkenartige Gebilde oder zusammenhanglose 'Staubwolken'. Vorteil: Ermöglicht eine enorme ästiehetische Varianz, da jede Koordinate der Mandelbrot-Menge ein völlig eigenes, einzigartiges Julia-Fraktal erzeugt.",
|
||||
"NEWTON_EXPLANATION": "entsteht durch die Visualisierung des Newton-Verfahrens zur Nullstellen-Suche einer komplexen Funktion. Jeder Pixel wird danach eingefärbt, zu welcher Nullstelle der Algorithmus konvergiert. Vorteil: Erzeugt faszinierende, sternförmige Symmetrien und komplexe Grenzen, an denen sich die Einzugsgebiete der Nullstellen auf chaotische Weise treffen.",
|
||||
"BURNING_SHIP_EXPLANATION": "ist eine Variation des Mandelbrots, bei der vor jedem Iterationsschritt der Absolutbetrag der Real- und Imaginärteile genommen wird: '(|Re(z)| + i|Im(z)|)^2 + c'. Vorteil: Erzeugt eine markante, asymmetrische Struktur, die einem brennenden Schiff mit Segeln ähnelt. Das Fraktal wirkt düsterer und 'mechanischer' als die klassischen Mengen.",
|
||||
"BURNING_SHIP_EXPLANATION": "ist eine variation des Mandelbrots, bei der vor jedem Iterationsschritt der Absolutbetrag der Real- und Imaginärteile genommen wird: '(|Re(z)| + i|Im(z)|)^2 + c'. Vorteil: Erzeugt eine markante, asymmetrische Struktur, die einem brennenden Schiff mit Segeln ähnelt. Das Fraktal wirkt düsterer und 'mechanischer' als die klassischen Mengen.",
|
||||
"DISCLAIMER": "Alle diese Fraktale basieren auf dem Prinzip der Iteration und dem Chaos-Effekt. Das bedeutet für deine Visualisierung:",
|
||||
"DISCLAIMER_1": "Unendliche Tiefe: Egal wie weit du hineinzoomst, es erscheinen immer neue, komplexe Strukturen, die dem Ganzen oft ähneln (Selbstähnlichkeit).",
|
||||
"DISCLAIMER_2": "Fluchtzeit-Algorithmus: Die Farben geben meist an, wie schnell eine Folge einen bestimmten Schwellenwert überschreitet – je schneller, desto 'heißer' oder heller die Farbe.",
|
||||
@@ -490,6 +493,9 @@
|
||||
"WIND_OFF": "Wind Ausschalten",
|
||||
"OUTLINE_ON": "Mesh anzeigen",
|
||||
"OUTLINE_OFF": "Mesh ausschalten",
|
||||
"STIFFNESS": "Steifigkeit",
|
||||
"ELONGATION": "Dehnung",
|
||||
"RESTART_SIMULATION": "Simulation neu starten",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Echtzeit-Stoffsimulation auf der GPU",
|
||||
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Stoffsimulation",
|
||||
@@ -502,11 +508,14 @@
|
||||
"DATA_STRUCTURES_EXPLANATION": "Für maximale GPU-Performance müssen Daten speicherfreundlich ausgerichtet werden (16-Byte-Alignment). Anstatt viele einzelne Variablen zu nutzen, packt man Informationen clever in 4er-Blöcke (vec4). Ein Vertex speichert so z. B. [X, Y, Z, Inverse_Masse]. Hat ein Punkt die inverse Masse 0.0, wird er vom Algorithmus ignoriert und schwebt unbeweglich in der Luft – ein eleganter Trick für Aufhängungen ohne extra Wenn-Dann-Abfragen.",
|
||||
"DISCLAIMER": "XPBD vs. Masse-Feder-Systeme: In der physikalischen Simulation gibt es grundlegende Architektur-Unterschiede beim Lösen der Gleichungen:",
|
||||
"DISCLAIMER_1": "Klassische Masse-Feder-Systeme: Hier werden Kräfte (Hookesches Gesetz) berechnet, die zu Beschleunigungen und schließlich zu neuen Positionen führen. Es gibt zwei Wege, diese mathematisch in die Zukunft zu rechnen (Integration):",
|
||||
"DISCLAIMER_2": "Explizite Löser (z.B. Forward Euler): Sie berechnen den nächsten Schritt stur aus dem aktuellen Zustand. Sie sind leicht zu programmieren, aber bei steifen Stoffen extrem instabil. Die Kräfte schaukeln sich auf und die Simulation 'explodiert', sofern man keine winzigen, sehr leistungsfressenden Zeitschritte wählt.",
|
||||
"DISCLAIMER_2": "Explizite Löser (z.B. Forward Euler): Sie berechnen den nächsten Schritt stur aus dem aktuellen Zustand. Sie sind leicht zu programmieren, aber bei steifen Stoffen extrem instabil. Die Kräfte schaukeln sich auf und die simulation 'explodiert', sofern man keine winzigen, sehr leistungsfressenden Zeitschritte wählt.",
|
||||
"DISCLAIMER_3": "Implizite Löser (z.B. Backward Euler): Sie berechnen den nächsten Schritt basierend auf dem zukünftigen Zustand. Das ist mathematisch enorm stabil, erfordert aber das Lösen riesiger globaler Matrix-Gleichungssysteme in jedem Frame. Dies ist auf der GPU schwerer zu parallelisieren und bricht zusammen, wenn sich die Struktur ändert (z. B. durch Zerschneiden des Stoffs).",
|
||||
"DISCLAIMER_4": "Der XPBD-Kompromiss: XPBD umgeht dieses komplexe Matrix-Problem völlig, indem es als lokaler Löser arbeitet. Es kombiniert die unbedingte Stabilität eines impliziten Lösers mit der enormen Geschwindigkeit, Parallelisierbarkeit und dynamischen Anpassungsfähigkeit eines expliziten Systems."
|
||||
}
|
||||
},
|
||||
"WEBGPU": {
|
||||
"NOT_SUPPORTED": "WebGPU konnte nicht gestartet werden. Bitte prüfe, ob dein Browser WebGPU unterstützt."
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithmen",
|
||||
"PATHFINDING": {
|
||||
@@ -525,6 +534,10 @@
|
||||
"TITLE": "Labyrinth-Erzeugung",
|
||||
"DESCRIPTION": "Visualisierung verschiedener Laybrinth-Erzeugungs-Algorithmen."
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Vier-Farben-Satz",
|
||||
"DESCRIPTION": "Der Vier-Farben-Satz besagt, dass jede Landkarte in der Ebene mit maximal vier Farben so eingefärbt werden kann, dass keine zwei aneinandergrenzenden Gebiete dieselbe Farbe besitzen."
|
||||
},
|
||||
"FRACTAL": {
|
||||
"TITLE": "Fraktale",
|
||||
"DESCRIPTION": "Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)."
|
||||
@@ -544,5 +557,35 @@
|
||||
"NOTE": "HINWEIS",
|
||||
"GRID_HEIGHT": "Höhe",
|
||||
"GRID_WIDTH": "Beite"
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Vier-Farben-Satz",
|
||||
"GENERATE": "Neue Karte generieren",
|
||||
"SOLVE": "Automatisch lösen",
|
||||
"CLEAR": "Farben zurücksetzen",
|
||||
"COLOR_1": "Farbe 1",
|
||||
"COLOR_2": "Farbe 2",
|
||||
"COLOR_3": "Farbe 3",
|
||||
"COLOR_4": "Farbe 4",
|
||||
"EXECUTION_TIME": "Ausführungszeit",
|
||||
"STATUS": {
|
||||
"LABEL": "Status",
|
||||
"INCOMPLETE": "Die Karte ist noch nicht vollständig eingefärbt.",
|
||||
"SOLVED": "Glückwunsch! Du hast die Karte korrekt gelöst!",
|
||||
"CONFLICTS": "Achtung: Benachbarte Regionen haben die gleiche Farbe!",
|
||||
"INVALID": "Karte ist vollständig, enthält aber Fehler."
|
||||
},
|
||||
"EXPLANATION": { "TITLE": "Der Vier-Farben-Satz",
|
||||
"EXPLANATION": "Der Vier-Farben-Satz besagt, dass jede Landkarte in der Ebene mit maximal vier Farben so eingefärbt werden kann, dass keine zwei aneinandergrenzenden Gebiete dieselbe Farbe besitzen.",
|
||||
"DISCLAIMER": "Dieser Algorithmus verwendet Backtracking, um eine gültige Färbung für die generierten Regionen zu finden.",
|
||||
"DISCLAIMER_1": "Kartengenerierung: Regionen werden mittels eines zufälligen Seed-Wachstumsalgorithmus (Voronoi-ähnlich) auf einem Gitter erzeugt.",
|
||||
"DISCLAIMER_2": "Adjazenz: Zwei Regionen gelten als benachbart, wenn sie mindestens eine gemeinsame Kante im Gitter teilen.",
|
||||
"DISCLAIMER_3": "Backtracking: Der Löser probiert die Farben 1-4 für jede Region aus und macht Schritte rückgängig (Backtracking), wenn ein Konflikt auftritt.",
|
||||
"DISCLAIMER_4": "Interaktiv: Sie können auch auf Regionen klicken, um manuell durch die Farben zu wechseln.",
|
||||
"DISCLAIMER_BOTTOM": "Zum einfärben in die Zelle klicken. Durch erneutes klicken ändert sich die Farbe."
|
||||
},
|
||||
"ALERT": {
|
||||
"NO_SOLUTION": "Keine Lösung gefunden (das sollte bei einer planaren Karte nicht passieren!)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +371,8 @@
|
||||
"START": "Start Sorting",
|
||||
"RESET": "Reset",
|
||||
"GENERATE_NEW_ARRAY": "Generate New Array",
|
||||
"SOUND_ON": "Sound On",
|
||||
"SOUND_OFF": "Sound Off",
|
||||
"EXECUTION_TIME": "Execution Time",
|
||||
"ARRAY_SIZE": "Number of Bars",
|
||||
"EXPLANATION": {
|
||||
@@ -378,6 +380,7 @@
|
||||
"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.",
|
||||
"TIMSORT_EXPLANATION": "is a hybrid sorting algorithm derived from Merge Sort and Insertion Sort. It divides the array into small 'runs' and sorts each using Insertion Sort, then merges them step by step using Merge Sort. Advantage: Extremely efficient on real-world data that is often partially sorted — O(n log n) in the worst case and O(n) in the best case. It is the standard sorting algorithm in Python and Java. Disadvantage: More complex to implement than a pure algorithm and requires additional memory for the merge step.",
|
||||
"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.",
|
||||
@@ -489,6 +492,9 @@
|
||||
"WIND_OFF": "Wind Off",
|
||||
"OUTLINE_ON": "Show Mesh",
|
||||
"OUTLINE_OFF": "Hide Mesh",
|
||||
"STIFFNESS": "Stiffness",
|
||||
"ELONGATION": "Elongation",
|
||||
"RESTART_SIMULATION": "Restart Simulation",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Real-time Cloth Simulation on the GPU",
|
||||
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Cloth Simulation",
|
||||
@@ -506,6 +512,9 @@
|
||||
"DISCLAIMER_4": "The XPBD Compromise: XPBD completely bypasses this complex matrix problem by acting as a local solver. It combines the absolute stability of an implicit solver with the enormous speed, parallelizability, and dynamic adaptability of an explicit system."
|
||||
}
|
||||
},
|
||||
"WEBGPU": {
|
||||
"NOT_SUPPORTED": "WebGPU could not be started. Please check if your browser supports WebGPU."
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithms",
|
||||
"PATHFINDING": {
|
||||
@@ -524,6 +533,10 @@
|
||||
"TITLE": "Maze Generation",
|
||||
"DESCRIPTION": "Visualizing various maze generation algorithms."
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Four Color Theorem",
|
||||
"DESCRIPTION": "The four color theorem states that any map in a plane can be colored using at most four colors in such a way that regions sharing a common boundary (other than a single point) do not share the same color."
|
||||
},
|
||||
"FRACTAL": {
|
||||
"TITLE": "Fractals",
|
||||
"DESCRIPTION": "Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)."
|
||||
@@ -543,5 +556,35 @@
|
||||
"NOTE": "Note",
|
||||
"GRID_HEIGHT": "Height",
|
||||
"GRID_WIDTH": "Width"
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Four Color Theorem",
|
||||
"GENERATE": "Generate Map",
|
||||
"SOLVE": "Auto Solve",
|
||||
"CLEAR": "Clear Colors",
|
||||
"COLOR_1": "Color 1",
|
||||
"COLOR_2": "Color 2",
|
||||
"COLOR_3": "Color 3",
|
||||
"COLOR_4": "Color 4",
|
||||
"EXECUTION_TIME": "Execution Time",
|
||||
"STATUS": {
|
||||
"LABEL": "Status",
|
||||
"INCOMPLETE": "Map is not fully colored yet.",
|
||||
"SOLVED": "Congratulations! You solved the map correctly!",
|
||||
"CONFLICTS": "Warning: Adjacent regions have the same color!",
|
||||
"INVALID": "Map is fully colored, but contains conflicts."
|
||||
},
|
||||
"EXPLANATION": { "TITLE": "Four Color Theorem",
|
||||
"EXPLANATION": "The four color theorem states that any map in a plane can be colored using at most four colors in such a way that regions sharing a common boundary (other than a single point) do not share the same color.",
|
||||
"DISCLAIMER": "This algorithm uses backtracking to find a valid coloring for the generated regions.",
|
||||
"DISCLAIMER_1": "Map Generation: Regions are generated using a random seed growth algorithm (Voronoi-like) on a grid.",
|
||||
"DISCLAIMER_2": "Adjacency: Two regions are considered neighbors if they share at least one edge in the grid.",
|
||||
"DISCLAIMER_3": "Backtracking: The solver tries colors 1-4 for each region, backtracking when a conflict is found.",
|
||||
"DISCLAIMER_4": "Interactive: You can also click on regions to cycle through colors manually.",
|
||||
"DISCLAIMER_BOTTOM": "Click to color the region. Click again to change the color."
|
||||
},
|
||||
"ALERT": {
|
||||
"NO_SOLUTION": "No solution found (this should not happen for a planar map!)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ $dark-theme: mat.define-theme((color: (theme-type: dark, primary: mat.$cyan-pale
|
||||
--link-color-hover: #9ad2ff;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background: radial-gradient(ellipse at 50% 0%, #1e2530 0%, #1a1a1a 65%);
|
||||
}
|
||||
|
||||
/* ---- global background and tests ---- */
|
||||
html,
|
||||
body {
|
||||
@@ -287,12 +291,17 @@ a {
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid lightgray;
|
||||
border: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dark canvas {
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -351,6 +360,22 @@ canvas {
|
||||
&.M2 {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
&.color1 {
|
||||
background-color: #FF5252;
|
||||
}
|
||||
|
||||
&.color2 {
|
||||
background-color: #448AFF;
|
||||
}
|
||||
|
||||
&.color3 {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
&.color4 {
|
||||
background-color: #FFEB3B;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +543,8 @@ app-root {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: clamp(1.5rem, 5vw, 2.5rem);
|
||||
background: linear-gradient(135deg, var(--mat-sys-primary), var(--mat-sys-tertiary));
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero .intro .lead {
|
||||
@@ -706,6 +733,57 @@ app-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.algo-card::after, .project-card::after {
|
||||
inset: unset;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--mat-sys-primary), var(--mat-sys-tertiary));
|
||||
border-radius: var(--card-radius) var(--card-radius) 0 0;
|
||||
}
|
||||
|
||||
.algo-icon-wrap {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(in oklab, var(--mat-sys-primary) 15%, transparent);
|
||||
color: var(--mat-sys-primary);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
mat-icon {
|
||||
font-size: 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.algo-page-title {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: clamp(1.4rem, 4vw, 2rem);
|
||||
}
|
||||
|
||||
.algo-card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.algo-card-desc {
|
||||
margin: 0;
|
||||
opacity: 0.75;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
|
||||
Reference in New Issue
Block a user