Compare commits
9 Commits
88ca86ae54
...
40c3b9dd5a
| Author | SHA1 | Date | |
|---|---|---|---|
| 40c3b9dd5a | |||
| 66643d8e18 | |||
| 2ab1d2dd85 | |||
| 150306333e | |||
| 7ff59bf734 | |||
| 5c97667ec1 | |||
| f46a1ed0bf | |||
| f7557efa88 | |||
| 408702be5c |
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`).
|
||||||
3120
package-lock.json
generated
3120
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -12,34 +12,34 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-slider/ngx-slider": "^21.0.0",
|
"@angular-slider/ngx-slider": "^21.0.0",
|
||||||
"@angular/animations": "~21.1.0",
|
"@angular/animations": "~21.2.1",
|
||||||
"@angular/cdk": "~21.1.0",
|
"@angular/cdk": "~21.2.1",
|
||||||
"@angular/common": "~21.1.0",
|
"@angular/common": "~21.2.1",
|
||||||
"@angular/compiler": "~21.1.0",
|
"@angular/compiler": "~21.2.1",
|
||||||
"@angular/core": "~21.1.0",
|
"@angular/core": "~21.2.1",
|
||||||
"@angular/forms": "~21.1.0",
|
"@angular/forms": "~21.2.1",
|
||||||
"@angular/material": "~21.1.0",
|
"@angular/material": "~21.2.1",
|
||||||
"@angular/platform-browser": "~21.1.0",
|
"@angular/platform-browser": "~21.2.1",
|
||||||
"@angular/router": "~21.1.0",
|
"@angular/router": "~21.2.1",
|
||||||
"@babylonjs/core": "^8.50.5",
|
"@babylonjs/core": "^8.54.1",
|
||||||
"@ngx-translate/core": "~17.0.0",
|
"@ngx-translate/core": "^17.0.0",
|
||||||
"@ngx-translate/http-loader": "~17.0.0",
|
"@ngx-translate/http-loader": "^17.0.0",
|
||||||
"inquirer": "^13.2.2",
|
"inquirer": "^13.3.0",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
"swiper": "~12.1.0",
|
"swiper": "~12.1.0",
|
||||||
"tslib": "~2.8.1"
|
"tslib": "~2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "~21.1.0",
|
"@angular/build": "~21.2.1",
|
||||||
"@angular/cli": "~21.1.0",
|
"@angular/cli": "~21.2.1",
|
||||||
"@angular/compiler-cli": "~21.1.0",
|
"@angular/compiler-cli": "~21.2.1",
|
||||||
"@lhci/cli": "^0.15.1",
|
"@lhci/cli": "^0.15.1",
|
||||||
"@types/jasmine": "~5.1.15",
|
"@types/jasmine": "~6.0.0",
|
||||||
"angular-eslint": "21.2.0",
|
"angular-eslint": "21.3.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^10.0.3",
|
||||||
"jasmine-core": "~6.0.1",
|
"jasmine-core": "~6.1.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "8.50.1"
|
"typescript-eslint": "8.56.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"tmp": "^0.2.3"
|
"tmp": "^0.2.3"
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import {AboutComponent} from './pages/about/about.component';
|
|
||||||
import {RouterConstants} from './constants/RouterConstants';
|
import {RouterConstants} from './constants/RouterConstants';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: AboutComponent },
|
{ path: '', loadComponent: () => import('./pages/about/about.component').then(m => m.AboutComponent) },
|
||||||
{ path: RouterConstants.ABOUT.PATH, component: RouterConstants.ABOUT.COMPONENT},
|
{ path: RouterConstants.ABOUT.PATH, loadComponent: () => import('./pages/about/about.component').then(m => m.AboutComponent) },
|
||||||
{ path: RouterConstants.PROJECTS.PATH, component: RouterConstants.PROJECTS.COMPONENT},
|
{ path: RouterConstants.PROJECTS.PATH, loadComponent: () => import('./pages/projects/projects.component').then(m => m.ProjectsComponent) },
|
||||||
{ path: RouterConstants.ALGORITHMS.PATH, component: RouterConstants.ALGORITHMS.COMPONENT},
|
{ path: RouterConstants.ALGORITHMS.PATH, loadComponent: () => import('./pages/algorithms/algorithms.component').then(m => m.AlgorithmsComponent) },
|
||||||
{ path: RouterConstants.PATHFINDING.PATH, component: RouterConstants.PATHFINDING.COMPONENT},
|
{ path: RouterConstants.PATHFINDING.PATH, loadComponent: () => import('./pages/algorithms/pathfinding/pathfinding.component').then(m => m.PathfindingComponent) },
|
||||||
{ path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT},
|
{ path: RouterConstants.SORTING.PATH, loadComponent: () => import('./pages/algorithms/sorting/sorting.component').then(m => m.SortingComponent) },
|
||||||
{ path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT},
|
{ path: RouterConstants.IMPRINT.PATH, loadComponent: () => import('./pages/imprint/imprint.component').then(m => m.ImprintComponent) },
|
||||||
{ path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT},
|
{ path: RouterConstants.GOL.PATH, loadComponent: () => import('./pages/algorithms/conway-gol/conway-gol.component').then(m => m.ConwayGolComponent) },
|
||||||
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT},
|
{ path: RouterConstants.LABYRINTH.PATH, loadComponent: () => import('./pages/algorithms/pathfinding/labyrinth/labyrinth.component').then(m => m.LabyrinthComponent) },
|
||||||
{ path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT},
|
{ path: RouterConstants.FRACTAL.PATH, loadComponent: () => import('./pages/algorithms/fractal/fractal.component').then(m => m.FractalComponent) },
|
||||||
{ path: RouterConstants.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT},
|
{ path: RouterConstants.FRACTAL3d.PATH, loadComponent: () => import('./pages/algorithms/fractal3d/fractal3d.component').then(m => m.Fractal3dComponent) },
|
||||||
{ path: RouterConstants.PENDULUM.PATH, component: RouterConstants.PENDULUM.COMPONENT},
|
{ path: RouterConstants.PENDULUM.PATH, loadComponent: () => import('./pages/algorithms/pendulum/pendulum.component').then(m => m.default) },
|
||||||
{ path: RouterConstants.CLOTH.PATH, component: RouterConstants.CLOTH.COMPONENT}
|
{ path: RouterConstants.CLOTH.PATH, loadComponent: () => import('./pages/algorithms/cloth/cloth.component').then(m => m.ClothComponent) },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,88 +1,63 @@
|
|||||||
import {AboutComponent} from '../pages/about/about.component';
|
export class RouterConstants {
|
||||||
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 {
|
|
||||||
|
|
||||||
static readonly ABOUT = {
|
static readonly ABOUT = {
|
||||||
PATH: 'about',
|
PATH: 'about',
|
||||||
LINK: '/about',
|
LINK: '/about',
|
||||||
COMPONENT: AboutComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly PROJECTS = {
|
static readonly PROJECTS = {
|
||||||
PATH: 'projects',
|
PATH: 'projects',
|
||||||
LINK: '/projects',
|
LINK: '/projects',
|
||||||
COMPONENT: ProjectsComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly ALGORITHMS = {
|
static readonly ALGORITHMS = {
|
||||||
PATH: 'algorithms',
|
PATH: 'algorithms',
|
||||||
LINK: '/algorithms',
|
LINK: '/algorithms',
|
||||||
COMPONENT: AlgorithmsComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly PATHFINDING = {
|
static readonly PATHFINDING = {
|
||||||
PATH: 'algorithms/pathfinding',
|
PATH: 'algorithms/pathfinding',
|
||||||
LINK: '/algorithms/pathfinding',
|
LINK: '/algorithms/pathfinding',
|
||||||
COMPONENT: PathfindingComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly SORTING = {
|
static readonly SORTING = {
|
||||||
PATH: 'algorithms/sorting',
|
PATH: 'algorithms/sorting',
|
||||||
LINK: '/algorithms/sorting',
|
LINK: '/algorithms/sorting',
|
||||||
COMPONENT: SortingComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly GOL = {
|
static readonly GOL = {
|
||||||
PATH: 'algorithms/gol',
|
PATH: 'algorithms/gol',
|
||||||
LINK: '/algorithms/gol',
|
LINK: '/algorithms/gol',
|
||||||
COMPONENT: ConwayGolComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly LABYRINTH = {
|
static readonly LABYRINTH = {
|
||||||
PATH: 'algorithms/labyrinth',
|
PATH: 'algorithms/labyrinth',
|
||||||
LINK: '/algorithms/labyrinth',
|
LINK: '/algorithms/labyrinth',
|
||||||
COMPONENT: LabyrinthComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly FRACTAL = {
|
static readonly FRACTAL = {
|
||||||
PATH: 'algorithms/fractal',
|
PATH: 'algorithms/fractal',
|
||||||
LINK: '/algorithms/fractal',
|
LINK: '/algorithms/fractal',
|
||||||
COMPONENT: FractalComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly FRACTAL3d = {
|
static readonly FRACTAL3d = {
|
||||||
PATH: 'algorithms/fractal3d',
|
PATH: 'algorithms/fractal3d',
|
||||||
LINK: '/algorithms/fractal3d',
|
LINK: '/algorithms/fractal3d',
|
||||||
COMPONENT: Fractal3dComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly PENDULUM = {
|
static readonly PENDULUM = {
|
||||||
PATH: 'algorithms/pendulum',
|
PATH: 'algorithms/pendulum',
|
||||||
LINK: '/algorithms/pendulum',
|
LINK: '/algorithms/pendulum',
|
||||||
COMPONENT: PendulumComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly CLOTH = {
|
static readonly CLOTH = {
|
||||||
PATH: 'algorithms/cloth',
|
PATH: 'algorithms/cloth',
|
||||||
LINK: '/algorithms/cloth',
|
LINK: '/algorithms/cloth',
|
||||||
COMPONENT: ClothComponent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static readonly IMPRINT = {
|
static readonly IMPRINT = {
|
||||||
PATH: 'imprint',
|
PATH: 'imprint',
|
||||||
LINK: '/imprint',
|
LINK: '/imprint',
|
||||||
COMPONENT: ImprintComponent
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
static readonly QUICK_SORT_WIKI = 'https://de.wikipedia.org/wiki/Quicksort'
|
static readonly QUICK_SORT_WIKI = 'https://de.wikipedia.org/wiki/Quicksort'
|
||||||
static readonly HEAP_SORT_WIKI = 'https://de.wikipedia.org/wiki/Heapsort'
|
static readonly HEAP_SORT_WIKI = 'https://de.wikipedia.org/wiki/Heapsort'
|
||||||
static readonly SHAKE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Shakersort'
|
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 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 PRIMS_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Prim'
|
||||||
static readonly KRUSKAL_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Kruskal'
|
static readonly KRUSKAL_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Kruskal'
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {ParticleBackgroundComponent} from '../../shared/components/particles-bac
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
|
||||||
imports: [RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
|
imports: [RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss'
|
styleUrl: './app.component.scss'
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
|
||||||
imports: [MatDialogModule, MatButtonModule, MatIconModule],
|
imports: [MatDialogModule, MatButtonModule, MatIconModule],
|
||||||
template: `
|
template: `
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a [routerLink]="RouterConstants.ABOUT.LINK" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
|
<a [routerLink]="RouterConstants.ABOUT.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
|
||||||
<a [routerLink]="RouterConstants.PROJECTS.LINK" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
|
<a [routerLink]="RouterConstants.PROJECTS.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
|
||||||
<a [routerLink]="RouterConstants.ALGORITHMS.LINK" mat-button>{{ 'TOPBAR.ALGORITHMS' | translate }}</a>
|
<a [routerLink]="RouterConstants.ALGORITHMS.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.ALGORITHMS' | translate }}</a>
|
||||||
<a [routerLink]="RouterConstants.IMPRINT.LINK" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
|
<a [routerLink]="RouterConstants.IMPRINT.LINK" routerLinkActive="active" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Mobile nav menu button -->
|
<!-- Mobile nav menu button -->
|
||||||
|
|||||||
@@ -52,6 +52,37 @@
|
|||||||
justify-content: center;
|
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 {
|
.nav-menu-btn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, computed, inject } from '@angular/core';
|
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 { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@@ -14,9 +14,8 @@ import {RouterConstants} from '../../constants/RouterConstants';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-topbar',
|
selector: 'app-topbar',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
RouterLink,
|
RouterLink, RouterLinkActive,
|
||||||
MatToolbarModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule,
|
MatToolbarModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule,
|
||||||
TranslateModule, MatDivider
|
TranslateModule, MatDivider
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {SharedFunctions} from '../../shared/SharedFunctions';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-about',
|
selector: 'app-about',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
NgOptimizedImage,
|
NgOptimizedImage,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export interface AlgorithmCategory {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
routerLink: string;
|
routerLink: string;
|
||||||
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<div class="card-grid">
|
<div class="card-grid">
|
||||||
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
|
<h1 class="algo-page-title">{{ 'ALGORITHM.TITLE' | translate }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-grid">
|
<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 class="algo-card" [routerLink]="[category.routerLink]">
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title>{{ category.title | translate }}</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
<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-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
import { Component, OnInit, inject } 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 { Observable } from 'rxjs';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import {TranslatePipe} from '@ngx-translate/core';
|
import {TranslatePipe} from '@ngx-translate/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-algorithms',
|
selector: 'app-algorithms',
|
||||||
templateUrl: './algorithms.component.html',
|
templateUrl: './algorithms.component.html',
|
||||||
styleUrls: ['./algorithms.component.scss'],
|
styleUrl: './algorithms.component.scss',
|
||||||
standalone: true,
|
imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe],
|
||||||
imports: [CommonModule, RouterLink, MatCardModule, TranslatePipe],
|
|
||||||
})
|
})
|
||||||
export class AlgorithmsComponent implements OnInit {
|
export class AlgorithmsComponent {
|
||||||
private readonly algorithmsService = inject(AlgorithmsService);
|
private readonly algorithmsService = inject(AlgorithmsService);
|
||||||
|
|
||||||
categories$: Observable<AlgorithmCategory[]> | undefined;
|
readonly categories: AlgorithmCategory[] = this.algorithmsService.getCategories();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.categories$ = this.algorithmsService.getCategories();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AlgorithmCategory } from './algorithm-category';
|
import { AlgorithmCategory } from './algorithm-category';
|
||||||
import { Observable, of } from 'rxjs';
|
|
||||||
import {RouterConstants} from '../../constants/RouterConstants';
|
import {RouterConstants} from '../../constants/RouterConstants';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -13,53 +12,61 @@ export class AlgorithmsService {
|
|||||||
id: 'pathfinding',
|
id: 'pathfinding',
|
||||||
title: 'ALGORITHM.PATHFINDING.TITLE',
|
title: 'ALGORITHM.PATHFINDING.TITLE',
|
||||||
description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
|
description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
|
||||||
routerLink: RouterConstants.PATHFINDING.LINK
|
routerLink: RouterConstants.PATHFINDING.LINK,
|
||||||
|
icon: 'route'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sorting',
|
id: 'sorting',
|
||||||
title: 'ALGORITHM.SORTING.TITLE',
|
title: 'ALGORITHM.SORTING.TITLE',
|
||||||
description: 'ALGORITHM.SORTING.DESCRIPTION',
|
description: 'ALGORITHM.SORTING.DESCRIPTION',
|
||||||
routerLink: RouterConstants.SORTING.LINK
|
routerLink: RouterConstants.SORTING.LINK,
|
||||||
|
icon: 'sort'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gameOfLife',
|
id: 'gameOfLife',
|
||||||
title: 'ALGORITHM.GOL.TITLE',
|
title: 'ALGORITHM.GOL.TITLE',
|
||||||
description: 'ALGORITHM.GOL.DESCRIPTION',
|
description: 'ALGORITHM.GOL.DESCRIPTION',
|
||||||
routerLink: RouterConstants.GOL.LINK
|
routerLink: RouterConstants.GOL.LINK,
|
||||||
|
icon: 'grid_on'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'labyrinth',
|
id: 'labyrinth',
|
||||||
title: 'ALGORITHM.LABYRINTH.TITLE',
|
title: 'ALGORITHM.LABYRINTH.TITLE',
|
||||||
description: 'ALGORITHM.LABYRINTH.DESCRIPTION',
|
description: 'ALGORITHM.LABYRINTH.DESCRIPTION',
|
||||||
routerLink: RouterConstants.LABYRINTH.LINK
|
routerLink: RouterConstants.LABYRINTH.LINK,
|
||||||
|
icon: 'grid_view'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fractal',
|
id: 'fractal',
|
||||||
title: 'ALGORITHM.FRACTAL.TITLE',
|
title: 'ALGORITHM.FRACTAL.TITLE',
|
||||||
description: 'ALGORITHM.FRACTAL.DESCRIPTION',
|
description: 'ALGORITHM.FRACTAL.DESCRIPTION',
|
||||||
routerLink: RouterConstants.FRACTAL.LINK
|
routerLink: RouterConstants.FRACTAL.LINK,
|
||||||
|
icon: 'blur_on'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fractal3d',
|
id: 'fractal3d',
|
||||||
title: 'ALGORITHM.FRACTAL3D.TITLE',
|
title: 'ALGORITHM.FRACTAL3D.TITLE',
|
||||||
description: 'ALGORITHM.FRACTAL3D.DESCRIPTION',
|
description: 'ALGORITHM.FRACTAL3D.DESCRIPTION',
|
||||||
routerLink: RouterConstants.FRACTAL3d.LINK
|
routerLink: RouterConstants.FRACTAL3d.LINK,
|
||||||
|
icon: 'view_in_ar'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pendulum',
|
id: 'pendulum',
|
||||||
title: 'ALGORITHM.PENDULUM.TITLE',
|
title: 'ALGORITHM.PENDULUM.TITLE',
|
||||||
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
||||||
routerLink: RouterConstants.PENDULUM.LINK
|
routerLink: RouterConstants.PENDULUM.LINK,
|
||||||
|
icon: 'rotate_right'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cloth',
|
id: 'cloth',
|
||||||
title: 'ALGORITHM.CLOTH.TITLE',
|
title: 'ALGORITHM.CLOTH.TITLE',
|
||||||
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
||||||
routerLink: RouterConstants.CLOTH.LINK
|
routerLink: RouterConstants.CLOTH.LINK,
|
||||||
|
icon: 'texture'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
getCategories(): Observable<AlgorithmCategory[]> {
|
getCategories(): AlgorithmCategory[] {
|
||||||
return of(this.categories);
|
return this.categories;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,15 @@
|
|||||||
<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()">
|
||||||
|
{{ 'CLOTH.RESTART_SIMULATION' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sliders-panel">
|
||||||
|
<label>{{ 'CLOTH.ELONGATION' | translate }}: {{ elongation }}</label>
|
||||||
|
<mat-slider min="0.5" max="2.0" step="0.1">
|
||||||
|
<input matSliderThumb [(ngModel)]="elongation">
|
||||||
|
</mat-slider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-babylon-canvas
|
<app-babylon-canvas
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
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 { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/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 {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh, WebGPUEngine, Scene} from '@babylonjs/core';
|
import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh, WebGPUEngine, Scene} from '@babylonjs/core';
|
||||||
@@ -31,6 +33,8 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
|||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
BabylonCanvas,
|
BabylonCanvas,
|
||||||
MatButton,
|
MatButton,
|
||||||
|
MatSliderModule,
|
||||||
|
FormsModule,
|
||||||
Information
|
Information
|
||||||
],
|
],
|
||||||
templateUrl: './cloth.component.html',
|
templateUrl: './cloth.component.html',
|
||||||
@@ -42,6 +46,9 @@ export class ClothComponent {
|
|||||||
private clothMesh: GroundMesh | null = null;
|
private clothMesh: GroundMesh | null = null;
|
||||||
public isWindActive: boolean = false;
|
public isWindActive: boolean = false;
|
||||||
public isOutlineActive: 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 = {
|
public renderConfig: RenderConfig = {
|
||||||
mode: '3D',
|
mode: '3D',
|
||||||
@@ -103,6 +110,11 @@ export class ClothComponent {
|
|||||||
this.clothMesh.material.wireframe = this.isOutlineActive;
|
this.clothMesh.material.wireframe = this.isOutlineActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public restartSimulation(): void {
|
||||||
|
this.simulationTime = 0;
|
||||||
|
this.createSimulation();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and starts the cloth simulation.
|
* Initializes and starts the cloth simulation.
|
||||||
*/
|
*/
|
||||||
@@ -164,9 +176,13 @@ export class ClothComponent {
|
|||||||
const constraintsP2: number[] = [];
|
const constraintsP2: number[] = [];
|
||||||
const constraintsP3: 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);
|
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)
|
// Fill positions (Pin top row)
|
||||||
for (let y = 0; y < config.gridHeight; y++) {
|
for (let y = 0; y < config.gridHeight; y++) {
|
||||||
@@ -186,14 +202,14 @@ export class ClothComponent {
|
|||||||
|
|
||||||
// Graph Coloring (4 Phases)
|
// Graph Coloring (4 Phases)
|
||||||
for (let y = 0; y < config.gridHeight; y++) {
|
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 = 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) addConstraint(constraintsP1, 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 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 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[] = [];
|
const constraintsP4: number[] = [];
|
||||||
@@ -228,7 +244,7 @@ export class ClothComponent {
|
|||||||
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||||
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||||
],
|
],
|
||||||
params: new Float32Array(8)
|
params: new Float32Array(9)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +347,7 @@ export class ClothComponent {
|
|||||||
// 6. RENDER LOOP
|
// 6. RENDER LOOP
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
private startRenderLoop(engine: WebGPUEngine, scene: Scene, config: ClothConfig, buffers: ClothBuffers, pipelines: ClothPipelines): void {
|
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
|
// 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
|
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 windX = this.isWindActive ? 5.0 : 0.0;
|
||||||
const windY = 0.0;
|
const windY = 0.0;
|
||||||
const windZ = this.isWindActive ? 15.0 : 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[0] = 0.016; // dt
|
||||||
paramsData[1] = -9.81; // gravity
|
paramsData[1] = -9.81; // gravity
|
||||||
paramsData[2] = scaledCompliance;
|
paramsData[2] = compliance;
|
||||||
paramsData[3] = config.numVertices;
|
paramsData[3] = config.numVertices;
|
||||||
paramsData[4] = windX;
|
paramsData[4] = windX;
|
||||||
paramsData[5] = windY;
|
paramsData[5] = windY;
|
||||||
paramsData[6] = windZ;
|
paramsData[6] = windZ;
|
||||||
paramsData[7] = this.simulationTime;
|
paramsData[7] = this.simulationTime;
|
||||||
|
paramsData[8] = this.elongation;
|
||||||
|
|
||||||
buffers.params.update(paramsData);
|
buffers.params.update(paramsData);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export const CLOTH_SHARED_STRUCTS = `
|
|||||||
wind_x: f32,
|
wind_x: f32,
|
||||||
wind_y: f32,
|
wind_y: f32,
|
||||||
wind_z: f32,
|
wind_z: f32,
|
||||||
time: f32
|
time: f32,
|
||||||
|
elongation: f32
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -26,9 +27,8 @@ export const CLOTH_VERTEX_SHADER_WGSL = `
|
|||||||
|
|
||||||
uniform viewProjection : mat4x4<f32>;
|
uniform viewProjection : mat4x4<f32>;
|
||||||
|
|
||||||
// Varyings, um Daten an den Fragment-Shader zu senden
|
|
||||||
varying vUV : vec2<f32>;
|
varying vUV : vec2<f32>;
|
||||||
varying vWorldPos : vec3<f32>; // NEU: Wir brauchen die 3D-Position für das Licht!
|
varying vWorldPos : vec3<f32>;
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn main(input : VertexInputs) -> FragmentInputs {
|
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.position = uniforms.viewProjection * vec4<f32>(worldPos, 1.0);
|
||||||
|
|
||||||
output.vUV = input.uv;
|
output.vUV = input.uv;
|
||||||
output.vWorldPos = worldPos; // Position weitergeben
|
output.vWorldPos = worldPos;
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -133,13 +133,15 @@ export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
|||||||
if (idx >= arrayLength(&constraints)) { return; }
|
if (idx >= arrayLength(&constraints)) { return; }
|
||||||
|
|
||||||
let constraint = constraints[idx];
|
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 idA = u32(constraint.x);
|
||||||
let idB = u32(constraint.y);
|
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 pA = positions[idA];
|
||||||
var pB = positions[idB];
|
var pB = positions[idB];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {MatButtonModule} from '@angular/material/button';
|
|||||||
import {MatButtonToggleModule} from '@angular/material/button-toggle';
|
import {MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
import {MatInputModule} from '@angular/material/input';
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||||
|
|
||||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -27,7 +28,6 @@ enum NodeType {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pathfinding',
|
selector: 'app-pathfinding',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -48,6 +48,7 @@ enum NodeType {
|
|||||||
export class PathfindingComponent implements AfterViewInit {
|
export class PathfindingComponent implements AfterViewInit {
|
||||||
private readonly pathfindingService = inject(PathfindingService);
|
private readonly pathfindingService = inject(PathfindingService);
|
||||||
private readonly translate = inject(TranslateService);
|
private readonly translate = inject(TranslateService);
|
||||||
|
private readonly snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
readonly NodeType = NodeType;
|
readonly NodeType = NodeType;
|
||||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||||
@@ -483,7 +484,8 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
return true;
|
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;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
private swap(arr: SortData[], i: number, j: number) {
|
||||||
const temp = arr[i].value;
|
const temp = arr[i].value;
|
||||||
arr[i].value = arr[j].value;
|
arr[i].value = arr[j].value;
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
<button mat-raised-button (click)="generateNewArray()">
|
<button mat-raised-button (click)="generateNewArray()">
|
||||||
<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()">
|
||||||
|
<mat-icon>{{ isSoundEnabled ? 'volume_up' : 'volume_off' }}</mat-icon>
|
||||||
|
{{ isSoundEnabled ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls-panel">
|
<div class="controls-panel">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {MatButtonModule} from "@angular/material/button";
|
|||||||
import {MatIconModule} from "@angular/material/icon";
|
import {MatIconModule} from "@angular/material/icon";
|
||||||
import {TranslateModule} from "@ngx-translate/core";
|
import {TranslateModule} from "@ngx-translate/core";
|
||||||
import { SortingService } from './service/sorting.service';
|
import { SortingService } from './service/sorting.service';
|
||||||
|
import { SortingAudioService } from './service/sorting-audio.service';
|
||||||
import {SortData, SortSnapshot} from './sorting.models';
|
import {SortData, SortSnapshot} from './sorting.models';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {MatInput} from '@angular/material/input';
|
import {MatInput} from '@angular/material/input';
|
||||||
@@ -15,14 +16,14 @@ import {AlgorithmInformation} from '../information/information.models';
|
|||||||
import {Information} from '../information/information';
|
import {Information} from '../information/information';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sorting',
|
selector: 'app-sorting',
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput, Information],
|
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput, Information],
|
||||||
templateUrl: './sorting.component.html',
|
templateUrl: './sorting.component.html',
|
||||||
styleUrls: ['./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 cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
readonly MAX_ARRAY_SIZE: number = 200;
|
readonly MAX_ARRAY_SIZE: number = 200;
|
||||||
@@ -50,6 +51,11 @@ export class SortingComponent implements OnInit {
|
|||||||
name: 'Heap Sort',
|
name: 'Heap Sort',
|
||||||
description: 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION',
|
description: 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION',
|
||||||
link: UrlConstants.HEAP_SORT_WIKI
|
link: UrlConstants.HEAP_SORT_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tim Sort',
|
||||||
|
description: 'SORTING.EXPLANATION.TIMSORT_EXPLANATION',
|
||||||
|
link: UrlConstants.TIM_SORT_WIKI
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
disclaimer: 'SORTING.EXPLANATION.DISCLAIMER',
|
disclaimer: 'SORTING.EXPLANATION.DISCLAIMER',
|
||||||
@@ -66,9 +72,10 @@ export class SortingComponent implements OnInit {
|
|||||||
unsortedArrayCopy: SortData[] = [];
|
unsortedArrayCopy: SortData[] = [];
|
||||||
arraySize = 50;
|
arraySize = 50;
|
||||||
maxArrayValue = 100;
|
maxArrayValue = 100;
|
||||||
animationSpeed = 50; // Milliseconds per step
|
animationSpeed = 100; // Milliseconds per step
|
||||||
selectedAlgorithm: string = this.algoInformation.entries[0].name;
|
selectedAlgorithm: string = this.algoInformation.entries[0].name;
|
||||||
executionTime = 0;
|
executionTime = 0;
|
||||||
|
isSoundEnabled = false;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.generateNewArray();
|
this.generateNewArray();
|
||||||
@@ -113,8 +120,14 @@ export class SortingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSound(): void {
|
||||||
|
this.isSoundEnabled = !this.isSoundEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
async startSorting(): Promise<void> {
|
async startSorting(): Promise<void> {
|
||||||
this.resetSorting();
|
this.resetSorting();
|
||||||
|
// Init the AudioContext on this user gesture so the browser allows sound
|
||||||
|
this.audioService.initOnUserGesture();
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
let snapshots: SortSnapshot[] = [];
|
let snapshots: SortSnapshot[] = [];
|
||||||
|
|
||||||
@@ -131,6 +144,9 @@ export class SortingComponent implements OnInit {
|
|||||||
case 'Cocktail Sort':
|
case 'Cocktail Sort':
|
||||||
snapshots = this.sortingService.cocktailSort(this.sortArray);
|
snapshots = this.sortingService.cocktailSort(this.sortArray);
|
||||||
break;
|
break;
|
||||||
|
case 'Tim Sort':
|
||||||
|
snapshots = this.sortingService.timSort(this.sortArray);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endTime = performance.now();
|
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();
|
this.cdr.detectChanges();
|
||||||
|
|
||||||
if (index === snapshots.length - 1) {
|
if (index === snapshots.length - 1) {
|
||||||
this.sortArray.forEach(item => item.state = 'sorted');
|
this.sortArray.forEach(item => item.state = 'sorted');
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
|
if (this.isSoundEnabled) {
|
||||||
|
this.audioService.playSortedSweep(this.sortArray.map(item => item.value), this.maxArrayValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, index * this.animationSpeed);
|
}, index * this.animationSpeed);
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import {MatButton} from '@angular/material/button';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-project-dialog',
|
selector: 'app-project-dialog',
|
||||||
templateUrl: './project-dialog.component.html',
|
templateUrl: './project-dialog.component.html',
|
||||||
styleUrls: ['./project-dialog.component.scss'],
|
styleUrl: './project-dialog.component.scss',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
MatDialogTitle,
|
MatDialogTitle,
|
||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export interface Projects {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-projects',
|
selector: 'app-projects',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatChipsModule,
|
MatChipsModule,
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ export class LanguageService {
|
|||||||
readonly lang = signal<Lang>(this.getInitial());
|
readonly lang = signal<Lang>(this.getInitial());
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.translate.addLangs(['de', 'en']);
|
// translate service lang and fallback are already configured via provideTranslateService in app.config
|
||||||
this.translate.setFallbackLang('en');
|
// just ensure the stored preference is active on startup
|
||||||
this.lang.set(this.getInitial());
|
this.translate.use(this.lang());
|
||||||
this.use(this.lang());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
use(l: 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({
|
@Component({
|
||||||
selector: 'app-generic-grid',
|
selector: 'app-generic-grid',
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
templateUrl: './generic-grid.html',
|
templateUrl: './generic-grid.html',
|
||||||
styleUrl: './generic-grid.scss',
|
styleUrl: './generic-grid.scss',
|
||||||
@@ -36,7 +35,7 @@ export class GenericGridComponent implements AfterViewInit {
|
|||||||
grid: any[][] = [];
|
grid: any[][] = [];
|
||||||
|
|
||||||
isDrawing = false;
|
isDrawing = false;
|
||||||
private lastCell: GridPos | null = null;
|
protected lastCell: GridPos | null = null;
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.ctx = this.getContextOrThrow();
|
this.ctx = this.getContextOrThrow();
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} 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 {TranslateService} from '@ngx-translate/core';
|
||||||
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
||||||
|
|
||||||
export interface RenderConfig {
|
export interface RenderConfig {
|
||||||
@@ -26,6 +28,8 @@ export interface SceneEventData {
|
|||||||
})
|
})
|
||||||
export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||||
readonly ngZone = inject(NgZone);
|
readonly ngZone = inject(NgZone);
|
||||||
|
private readonly snackBar = inject(MatSnackBar);
|
||||||
|
private readonly translate = inject(TranslateService);
|
||||||
|
|
||||||
@ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
@ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
@@ -72,9 +76,11 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
|||||||
engine: this.engine
|
engine: this.engine
|
||||||
});
|
});
|
||||||
this.addRenderLoop(canvas);
|
this.addRenderLoop(canvas);
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
alert("WebGPU could not be started. Please check your browser if it supports WebGPU.");
|
const message = this.translate.instant('WEBGPU.NOT_SUPPORTED');
|
||||||
|
this.snackBar.open(message, 'OK', { duration: 8000, horizontalPosition: "center", verticalPosition: "top" });
|
||||||
this.engine = null!;
|
this.engine = null!;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,6 +364,8 @@
|
|||||||
"START": "Sortierung starten",
|
"START": "Sortierung starten",
|
||||||
"RESET": "Zurücksetzen",
|
"RESET": "Zurücksetzen",
|
||||||
"GENERATE_NEW_ARRAY": "Neues Array generieren",
|
"GENERATE_NEW_ARRAY": "Neues Array generieren",
|
||||||
|
"SOUND_ON": "Ton an",
|
||||||
|
"SOUND_OFF": "Ton aus",
|
||||||
"EXECUTION_TIME": "Ausführungszeit",
|
"EXECUTION_TIME": "Ausführungszeit",
|
||||||
"ARRAY_SIZE": "Anzahl der Balken",
|
"ARRAY_SIZE": "Anzahl der Balken",
|
||||||
"EXPLANATION": {
|
"EXPLANATION": {
|
||||||
@@ -371,6 +373,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.",
|
"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).",
|
"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.",
|
"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.",
|
"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": "Die Wahl des \"besten\" Sortieralgorithmus hängt stark von den Daten und den Rahmenbedingungen ab. In der Informatik betrachtet man oft drei Szenarien:",
|
||||||
"DISCLAIMER_1": "Best Case: Die Daten sind schon fast sortiert (hier glänzt z.B. Bubble Sort).",
|
"DISCLAIMER_1": "Best Case: Die Daten sind schon fast sortiert (hier glänzt z.B. Bubble Sort).",
|
||||||
@@ -483,6 +486,9 @@
|
|||||||
"WIND_OFF": "Wind Ausschalten",
|
"WIND_OFF": "Wind Ausschalten",
|
||||||
"OUTLINE_ON": "Mesh anzeigen",
|
"OUTLINE_ON": "Mesh anzeigen",
|
||||||
"OUTLINE_OFF": "Mesh ausschalten",
|
"OUTLINE_OFF": "Mesh ausschalten",
|
||||||
|
"STIFFNESS": "Steifigkeit",
|
||||||
|
"ELONGATION": "Dehnung",
|
||||||
|
"RESTART_SIMULATION": "Simulation neu starten",
|
||||||
"EXPLANATION": {
|
"EXPLANATION": {
|
||||||
"TITLE": "Echtzeit-Stoffsimulation auf der GPU",
|
"TITLE": "Echtzeit-Stoffsimulation auf der GPU",
|
||||||
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Stoffsimulation",
|
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Stoffsimulation",
|
||||||
@@ -500,6 +506,9 @@
|
|||||||
"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."
|
"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": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithmen",
|
"TITLE": "Algorithmen",
|
||||||
"PATHFINDING": {
|
"PATHFINDING": {
|
||||||
|
|||||||
@@ -364,6 +364,8 @@
|
|||||||
"START": "Start Sorting",
|
"START": "Start Sorting",
|
||||||
"RESET": "Reset",
|
"RESET": "Reset",
|
||||||
"GENERATE_NEW_ARRAY": "Generate New Array",
|
"GENERATE_NEW_ARRAY": "Generate New Array",
|
||||||
|
"SOUND_ON": "Sound On",
|
||||||
|
"SOUND_OFF": "Sound Off",
|
||||||
"EXECUTION_TIME": "Execution Time",
|
"EXECUTION_TIME": "Execution Time",
|
||||||
"ARRAY_SIZE": "Number of Bars",
|
"ARRAY_SIZE": "Number of Bars",
|
||||||
"EXPLANATION": {
|
"EXPLANATION": {
|
||||||
@@ -371,6 +373,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.",
|
"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).",
|
"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.",
|
"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": "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_1": "Best Case: The data is already nearly sorted (Bubble Sort shines here, for example).",
|
||||||
"DISCLAIMER_2": "Average Case: The statistical norm.",
|
"DISCLAIMER_2": "Average Case: The statistical norm.",
|
||||||
@@ -482,6 +485,9 @@
|
|||||||
"WIND_OFF": "Wind Off",
|
"WIND_OFF": "Wind Off",
|
||||||
"OUTLINE_ON": "Show Mesh",
|
"OUTLINE_ON": "Show Mesh",
|
||||||
"OUTLINE_OFF": "Hide Mesh",
|
"OUTLINE_OFF": "Hide Mesh",
|
||||||
|
"STIFFNESS": "Stiffness",
|
||||||
|
"ELONGATION": "Elongation",
|
||||||
|
"RESTART_SIMULATION": "Restart Simulation",
|
||||||
"EXPLANATION": {
|
"EXPLANATION": {
|
||||||
"TITLE": "Real-time Cloth Simulation on the GPU",
|
"TITLE": "Real-time Cloth Simulation on the GPU",
|
||||||
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Cloth Simulation",
|
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Cloth Simulation",
|
||||||
@@ -499,6 +505,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."
|
"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": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithms",
|
"TITLE": "Algorithms",
|
||||||
"PATHFINDING": {
|
"PATHFINDING": {
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ $dark-theme: mat.define-theme((color: (theme-type: dark, primary: mat.$cyan-pale
|
|||||||
--link-color-hover: #9ad2ff;
|
--link-color-hover: #9ad2ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark body {
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, #1e2530 0%, #1a1a1a 65%);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- global background and tests ---- */
|
/* ---- global background and tests ---- */
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@@ -287,12 +291,17 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border: 1px solid lightgray;
|
border: none;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark canvas {
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.legend {
|
.legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -518,6 +527,10 @@ app-root {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-size: clamp(1.5rem, 5vw, 2.5rem);
|
font-size: clamp(1.5rem, 5vw, 2.5rem);
|
||||||
|
background: linear-gradient(135deg, var(--mat-sys-primary), var(--mat-sys-tertiary));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero .intro .lead {
|
.hero .intro .lead {
|
||||||
@@ -706,6 +719,57 @@ app-root {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
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 {
|
.project-card {
|
||||||
|
|||||||
Reference in New Issue
Block a user