Compare commits
82 Commits
16cc8afd4a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 61a2057291 | |||
| 737601636f | |||
| 0037502b00 | |||
| 5485e57bdb | |||
| f4104d02e8 | |||
| 28bde29c8b | |||
| ab3bca4395 | |||
| 12411e58bf | |||
| 14d7a78ac4 | |||
| ed0e370e9d | |||
| f656206691 | |||
| 728dbc047f | |||
| 746022c48d | |||
| 954211b3cf | |||
| 885e609082 | |||
| b61eb4eb73 | |||
| 96d4659652 | |||
| ad43459173 | |||
| 32ecfcb621 | |||
| aceb0ea24e | |||
| c160fb4bc8 | |||
| 45c11e42cd | |||
| e66206c518 | |||
| d0c4ad770b | |||
| 5f8b1de20f | |||
| 30965afcbd | |||
| 6330d45b4e | |||
| c6edc922fe | |||
| 0e78e6b471 | |||
| 8f21b0e6b0 | |||
| cb2ffa2d80 | |||
| 5ebd1d19ea | |||
| eed7e8c0fa | |||
| 24d6d9cdbe | |||
| 34148aade2 | |||
| 5721b2e48e | |||
| 2bfa8ba9a1 | |||
| 13f99ac7ae | |||
| 66df3a7f88 | |||
| 598013a7d0 | |||
| f499b78fd5 | |||
| 0d2e7c97ec | |||
| 13b59d0b36 | |||
| 55ece27e1c | |||
| 68e21489ea | |||
| 796fdf4a79 | |||
| 270716551d | |||
| a494c8156d | |||
| 5691cb408d | |||
| c2ad2ae992 | |||
| 1e8ba020e2 | |||
| 4685a94f1d | |||
| d45b651d2d | |||
| c409cd08b1 | |||
| cc6997e732 | |||
| ea15e66c50 | |||
| 085201913f | |||
| 525bec30fe | |||
| d01a6b0c6b | |||
| 159d82d602 | |||
| ba3dc4d928 | |||
| 12ebbb09ce | |||
| 07b6296294 | |||
| 6cb033bd9f | |||
| 42c86ecb70 | |||
| 5d162b57ab | |||
| dab7c51b90 | |||
| e8354bfecd | |||
| bbec113f5d | |||
| 950ec75f07 | |||
| 854d558e6b | |||
| bc740af0bf | |||
| f6ed2057a4 | |||
| a6d8405916 | |||
| 41ec7a862f | |||
| b9f6564771 | |||
| d8611b0968 | |||
| 4d27643d74 | |||
| 48e74ed3e8 | |||
| e1b1643eb2 | |||
| 0e520ead26 | |||
| 70ed047059 |
@@ -1,12 +1,84 @@
|
|||||||
name: Build & Push Frontend A
|
name: Build, Test & Push Frontend
|
||||||
|
run-name: ${{ gitea.actor }} build and test Angular 🚀
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
# ------------------------------------------------------------------
|
||||||
|
# JOB 1: Code integrity and quality gates (CI)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
quality-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Linux Libs
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libnss3 \
|
||||||
|
libnspr4 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libcups2t64 \
|
||||||
|
libdrm2 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxrandr2 \
|
||||||
|
libgbm1 \
|
||||||
|
libasound2t64 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libcairo2
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
# 1. Linting (Code-Style)
|
||||||
|
- name: Lint & Type Check
|
||||||
|
run: npm run lint --if-present
|
||||||
|
|
||||||
|
# 2. Unit Tests (Logik) Not necessary, because atm no tests written
|
||||||
|
#- name: Unit Tests
|
||||||
|
# run: npx ng test --watch=false --browsers=ChromeHeadless
|
||||||
|
|
||||||
|
# 3. Build Production (necessary for lighthouse)
|
||||||
|
- name: Build Production
|
||||||
|
run: npx ng build --configuration production
|
||||||
|
|
||||||
|
# 4. Lighthouse Audit (Performance & SEO)
|
||||||
|
- name: Install Puppeteer
|
||||||
|
run: npm install puppeteer --no-save
|
||||||
|
|
||||||
|
- name: Lighthouse CI
|
||||||
|
run: |
|
||||||
|
CHROME_PATH=$(node -e 'console.log(require("puppeteer").executablePath())')
|
||||||
|
export CHROME_PATH=$CHROME_PATH
|
||||||
|
npx lhci autorun
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# JOB 2: Docker Build & Push (CD)
|
||||||
|
# Runs only if 'quality-check' are successfully and we are on branch main.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
docker:
|
||||||
|
needs: quality-check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -36,8 +108,7 @@ jobs:
|
|||||||
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/playground:frontend-a-${{ steps.prep.outputs.branch }}
|
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/playground:frontend-a-${{ steps.prep.outputs.branch }}
|
||||||
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/playground:frontend-a-${{ steps.prep.outputs.branch }}-${{ steps.prep.outputs.sha }}
|
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/playground:frontend-a-${{ steps.prep.outputs.branch }}-${{ steps.prep.outputs.sha }}
|
||||||
|
|
||||||
- name: Also push moving main tag (only on main)
|
- name: Also push moving main tag
|
||||||
if: ${{ github.ref == 'refs/heads/main' }}
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -41,3 +41,6 @@ __screenshots__/
|
|||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Lighthouse
|
||||||
|
.lighthouseci/
|
||||||
|
|||||||
@@ -49,8 +49,8 @@
|
|||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "1MB",
|
"maximumWarning": "8MB",
|
||||||
"maximumError": "1MB"
|
"maximumError": "15MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ module.exports = defineConfig([
|
|||||||
],
|
],
|
||||||
processor: angular.processInlineTemplates,
|
processor: angular.processInlineTemplates,
|
||||||
rules: {
|
rules: {
|
||||||
|
"@typescript-eslint/no-inferrable-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/prefer-for-of": "off",
|
||||||
"@angular-eslint/directive-selector": [
|
"@angular-eslint/directive-selector": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|||||||
14
lighthouserc.json
Normal file
14
lighthouserc.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"ci": {
|
||||||
|
"collect": {
|
||||||
|
"numberOfRuns": 1,
|
||||||
|
"staticDistDir": "./dist/playground-frontend/browser",
|
||||||
|
"settings": {
|
||||||
|
"chromeFlags": "--no-sandbox --headless --disable-gpu --disable-dev-shm-usage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"target": "temporary-public-storage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3718
package-lock.json
generated
3718
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "playground-frontend",
|
"name": "playground-frontend",
|
||||||
"version": "0.2.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular-slider/ngx-slider": "^21.0.0",
|
||||||
"@angular/animations": "~21.1.0",
|
"@angular/animations": "~21.1.0",
|
||||||
"@angular/cdk": "~21.1.0",
|
"@angular/cdk": "~21.1.0",
|
||||||
"@angular/common": "~21.1.0",
|
"@angular/common": "~21.1.0",
|
||||||
@@ -20,8 +21,10 @@
|
|||||||
"@angular/material": "~21.1.0",
|
"@angular/material": "~21.1.0",
|
||||||
"@angular/platform-browser": "~21.1.0",
|
"@angular/platform-browser": "~21.1.0",
|
||||||
"@angular/router": "~21.1.0",
|
"@angular/router": "~21.1.0",
|
||||||
|
"@babylonjs/core": "^8.50.5",
|
||||||
"@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",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
"swiper": "~12.1.0",
|
"swiper": "~12.1.0",
|
||||||
"tslib": "~2.8.1"
|
"tslib": "~2.8.1"
|
||||||
@@ -30,11 +33,15 @@
|
|||||||
"@angular/build": "~21.1.0",
|
"@angular/build": "~21.1.0",
|
||||||
"@angular/cli": "~21.1.0",
|
"@angular/cli": "~21.1.0",
|
||||||
"@angular/compiler-cli": "~21.1.0",
|
"@angular/compiler-cli": "~21.1.0",
|
||||||
|
"@lhci/cli": "^0.15.1",
|
||||||
"@types/jasmine": "~5.1.15",
|
"@types/jasmine": "~5.1.15",
|
||||||
"angular-eslint": "21.2.0",
|
"angular-eslint": "21.2.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"jasmine-core": "~6.0.1",
|
"jasmine-core": "~6.0.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "8.50.1"
|
"typescript-eslint": "8.50.1"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"tmp": "^0.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export const routes: Routes = [
|
|||||||
{ path: RouterConstants.PATHFINDING.PATH, component: RouterConstants.PATHFINDING.COMPONENT},
|
{ path: RouterConstants.PATHFINDING.PATH, component: RouterConstants.PATHFINDING.COMPONENT},
|
||||||
{ path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT},
|
{ path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT},
|
||||||
{ path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT},
|
{ path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT},
|
||||||
{ path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.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}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export class AssetsConstants {
|
|||||||
static readonly DIPLOMA = '/assets/projects/diploma/Dahm2010-Diplomarbeit.pdf';
|
static readonly DIPLOMA = '/assets/projects/diploma/Dahm2010-Diplomarbeit.pdf';
|
||||||
|
|
||||||
//project images
|
//project images
|
||||||
|
static readonly PLAYGROUND_IMAGES = [
|
||||||
|
'/assets/projects/playground/1.png'
|
||||||
|
];
|
||||||
|
|
||||||
static readonly EL_MUCHO_IMAGES = [
|
static readonly EL_MUCHO_IMAGES = [
|
||||||
'/assets/projects/el-mucho/1.jpg',
|
'/assets/projects/el-mucho/1.jpg',
|
||||||
'/assets/projects/el-mucho/2.jpg',
|
'/assets/projects/el-mucho/2.jpg',
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import {ImprintComponent} from '../pages/imprint/imprint.component';
|
|||||||
import {AlgorithmsComponent} from '../pages/algorithms/algorithms.component';
|
import {AlgorithmsComponent} from '../pages/algorithms/algorithms.component';
|
||||||
import {PathfindingComponent} from '../pages/algorithms/pathfinding/pathfinding.component';
|
import {PathfindingComponent} from '../pages/algorithms/pathfinding/pathfinding.component';
|
||||||
import {SortingComponent} from '../pages/algorithms/sorting/sorting.component';
|
import {SortingComponent} from '../pages/algorithms/sorting/sorting.component';
|
||||||
import {ConwayGol} from '../pages/algorithms/conway-gol/conway-gol';
|
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 {
|
||||||
|
|
||||||
@@ -41,7 +46,37 @@ export class RouterConstants {
|
|||||||
static readonly GOL = {
|
static readonly GOL = {
|
||||||
PATH: 'algorithms/gol',
|
PATH: 'algorithms/gol',
|
||||||
LINK: '/algorithms/gol',
|
LINK: '/algorithms/gol',
|
||||||
COMPONENT: ConwayGol
|
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 IMPRINT = {
|
static readonly IMPRINT = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export class UrlConstants {
|
export class UrlConstants {
|
||||||
static readonly LINKED_IN = 'https://www.linkedin.com/in/andreas-dahm-2395991ba';
|
static readonly LINKED_IN = 'https://www.linkedin.com/in/andreas-dahm-2395991ba';
|
||||||
static readonly GIT_HUB = 'https://github.com/LoboTheDark';
|
static readonly CODEBERG = 'https://codeberg.org/LoboTheDark';
|
||||||
static readonly DIJKSTRA_WIKI = 'https://de.wikipedia.org/wiki/Dijkstra-Algorithmus'
|
static readonly DIJKSTRA_WIKI = 'https://de.wikipedia.org/wiki/Dijkstra-Algorithmus'
|
||||||
static readonly ASTAR_WIKI = 'https://de.wikipedia.org/wiki/A*-Algorithmus'
|
static readonly ASTAR_WIKI = 'https://de.wikipedia.org/wiki/A*-Algorithmus'
|
||||||
static readonly BUBBLE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Bubblesort'
|
static readonly BUBBLE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Bubblesort'
|
||||||
@@ -8,4 +8,19 @@
|
|||||||
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 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 KRUSKAL_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Kruskal'
|
||||||
|
static readonly MANDELBROT_WIKI = 'https://de.wikipedia.org/wiki/Mandelbrot-Menge'
|
||||||
|
static readonly JULIA_WIKI = 'https://de.wikipedia.org/wiki/Julia-Menge'
|
||||||
|
static readonly NEWTON_FRACTAL_WIKI = 'https://de.wikipedia.org/wiki/Newtonfraktal'
|
||||||
|
static readonly BURNING_SHIP_WIKI = 'https://de.wikipedia.org/wiki/Burning_ship_(Fraktal)'
|
||||||
|
static readonly MANDELBULB_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle'
|
||||||
|
static readonly MANDELBOX_WIKI = 'https://de.wikipedia.org/wiki/Mandelbox'
|
||||||
|
static readonly JULIA3D_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle'
|
||||||
|
static readonly DOUBLE_PENDULUM_WIKI = 'https://de.wikipedia.org/wiki/Doppelpendel'
|
||||||
|
static readonly CLOTH_SIMULATION_WIKI = 'https://en.wikipedia.org/wiki/Cloth_modeling'
|
||||||
|
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'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
<app-particles-background></app-particles-background>
|
||||||
<app-topbar />
|
<app-topbar />
|
||||||
|
<main class="app-container app-surface">
|
||||||
<main class="container app-surface">
|
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
.container { max-width: 1100px; margin: 0 auto; padding: 1rem; }
|
|
||||||
.app-surface {
|
|
||||||
background: var(--app-bg);
|
|
||||||
color: var(--app-fg);
|
|
||||||
transition: background-color 220ms ease, color 220ms ease;
|
|
||||||
}
|
|
||||||
.foot {
|
|
||||||
border-top: 1px solid rgba(0,0,0,.08);
|
|
||||||
padding: 1rem; text-align: center; opacity: .8;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { Component } from '@angular/core';
|
|||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import {TopbarComponent} from '../topbar/topbar.component';
|
import {TopbarComponent} from '../topbar/topbar.component';
|
||||||
import {TranslatePipe} from '@ngx-translate/core';
|
import {TranslatePipe} from '@ngx-translate/core';
|
||||||
|
import {ParticleBackgroundComponent} from '../../shared/components/particles-background/particles-background.component';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, TopbarComponent, TranslatePipe],
|
imports: [RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss'
|
styleUrl: './app.component.scss'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
<mat-toolbar class="topbar" color="primary" (keydown)="onKeydown($event)">
|
<mat-toolbar class="topbar" color="primary" (keydown)="onKeydown($event)">
|
||||||
<a class="brand" routerLink="/">
|
<a class="brand" routerLink="/">
|
||||||
<img class="logo-dot"
|
<img class="logo-dot" src="{{AssetsConstants.LOGO}}" alt="" aria-hidden="true" draggable="false"
|
||||||
src="{{AssetsConstants.LOGO}}"
|
oncontextmenu="return false;">
|
||||||
alt="" aria-hidden="true"
|
|
||||||
draggable="false"
|
|
||||||
oncontextmenu="return false;"
|
|
||||||
>
|
|
||||||
<span class="brand-text">{{ 'APP.TITLE' | translate }}</span>
|
<span class="brand-text">{{ 'APP.TITLE' | translate }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -17,11 +13,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Mobile nav menu button -->
|
<!-- Mobile nav menu button -->
|
||||||
<button
|
<button mat-icon-button class="nav-menu-btn" [matMenuTriggerFor]="navMenu" aria-label="Open navigation">
|
||||||
mat-icon-button
|
|
||||||
class="nav-menu-btn"
|
|
||||||
[matMenuTriggerFor]="navMenu"
|
|
||||||
aria-label="Open navigation">
|
|
||||||
<mat-icon>menu</mat-icon>
|
<mat-icon>menu</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -46,7 +38,8 @@
|
|||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
|
||||||
<!-- Settings: Sprache + Theme -->
|
<!-- Settings: Sprache + Theme -->
|
||||||
<button mat-icon-button [matMenuTriggerFor]="settingsMenu" aria-label="Open settings" matTooltip="{{ 'TOPBAR.SETTINGS' | translate }}">
|
<button mat-icon-button [matMenuTriggerFor]="settingsMenu" aria-label="Open settings"
|
||||||
|
matTooltip="{{ 'TOPBAR.SETTINGS' | translate }}">
|
||||||
<mat-icon>tune</mat-icon>
|
<mat-icon>tune</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -58,7 +51,7 @@
|
|||||||
<span>{{ 'LANG.DE' | translate }}</span>
|
<span>{{ 'LANG.DE' | translate }}</span>
|
||||||
@if (lang.lang() === 'de')
|
@if (lang.lang() === 'de')
|
||||||
{
|
{
|
||||||
<mat-icon >check</mat-icon>
|
<mat-icon>check</mat-icon>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="setLang('en')">
|
<button mat-menu-item (click)="setLang('en')">
|
||||||
@@ -66,7 +59,7 @@
|
|||||||
<span>{{ 'LANG.EN' | translate }}</span>
|
<span>{{ 'LANG.EN' | translate }}</span>
|
||||||
@if (lang.lang() === 'en')
|
@if (lang.lang() === 'en')
|
||||||
{
|
{
|
||||||
<mat-icon>check</mat-icon>
|
<mat-icon>check</mat-icon>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,83 +1,76 @@
|
|||||||
|
/* ---- Topbar Host & Base ---- */
|
||||||
|
:host {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
position: sticky; top: 0; z-index: 100;
|
/* Erzeugt den Milchglas-Effekt */
|
||||||
backdrop-filter: saturate(1.1) blur(8px);
|
backdrop-filter: saturate(1.1) blur(8px);
|
||||||
background:
|
-webkit-backdrop-filter: saturate(1.1) blur(8px);
|
||||||
color-mix(in oklab, var(--app-topbar-bg) 80%, transparent);
|
/* Safari Support */
|
||||||
border-bottom: 1px solid rgba(0,0,0,.08);
|
|
||||||
|
/* Mischt die Variable mit Transparenz. !important überschreibt Material-Vorgaben */
|
||||||
|
background: color-mix(in oklab, var(--app-topbar-bg) 80%, transparent) !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, .08);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: clamp(0.5rem, 1vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Branding ---- */
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(0.4rem, 1vw, 0.6rem);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
.brand {
|
|
||||||
display:flex; align-items:center; gap:.6rem;
|
|
||||||
color: inherit; text-decoration: none;
|
|
||||||
.logo-dot {
|
.logo-dot {
|
||||||
width: 48px; height: 48px; border-radius: 50%;
|
width: clamp(36px, 10vw, 48px);
|
||||||
|
height: clamp(36px, 10vw, 48px);
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.brand-text { font-weight: 600; letter-spacing:.2px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav { display:flex; gap:.25rem; margin-left:.5rem; }
|
.brand-text {
|
||||||
|
font-weight: 600;
|
||||||
.spacer { flex: 1; }
|
letter-spacing: .2px;
|
||||||
|
font-size: clamp(1rem, 3vw, 1.2rem);
|
||||||
.flag-icon { width: 18px; height: 18px; border-radius: 2px; margin-right:.5rem; }
|
}
|
||||||
|
|
||||||
.menu-section { padding:.25rem .5rem .5rem; }
|
|
||||||
.menu-title { font-size:.75rem; opacity:.75; padding:.25rem .75rem .5rem; }
|
|
||||||
|
|
||||||
.kbd {
|
|
||||||
margin-left:auto; font-size:.7rem; opacity:.65; border:1px solid currentColor;
|
|
||||||
border-radius:4px; padding:0 .35rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .mat-mdc-menu-item .mdc-list-item__primary-text {
|
/* ---- Navigation ---- */
|
||||||
display: flex;
|
.nav {
|
||||||
align-items: center;
|
position: absolute;
|
||||||
gap: .5rem;
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: clamp(0.25rem, 1vw, 0.5rem);
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .mat-mdc-menu-item .kbd {
|
|
||||||
margin-left: auto;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 0 .35rem;
|
|
||||||
border: 0px solid currentColor;
|
|
||||||
border-radius: 4px;
|
|
||||||
opacity: .65;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .mat-mdc-menu-item .mat-icon {
|
|
||||||
width: 20px; height: 20px; font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .mat-mdc-menu-item .flag-icon {
|
|
||||||
width: 20px !important;
|
|
||||||
height: 14px !important;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-right: .5rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .mat-mdc-menu-panel {
|
|
||||||
border-radius: 10px !important;
|
|
||||||
border: 1px solid rgba(0,0,0,.14);
|
|
||||||
}
|
|
||||||
.dark ::ng-deep .mat-mdc-menu-panel {
|
|
||||||
border-color: rgba(255,255,255,.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive: Collapse navigation to icon if width is smaller than 760px */
|
|
||||||
.nav-menu-btn {
|
.nav-menu-btn {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.topbar .nav {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu-btn {
|
.spacer {
|
||||||
display: inline-flex;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Mobile Responsiveness ---- */
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
flex: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,42 +1,39 @@
|
|||||||
<section class="about">
|
<section class="about">
|
||||||
<mat-card class="hero">
|
<mat-card class="hero">
|
||||||
<div class="photo">
|
<div class="hero-flex-container">
|
||||||
<img
|
<div class="photo">
|
||||||
[ngSrc]="AssetsConstants.ME"
|
<img [ngSrc]="AssetsConstants.ME" width="421" height="512" alt="{{ 'ABOUT.ALT.PROFILE' | translate }}"
|
||||||
width="421" height="512"
|
draggable="false" oncontextmenu="return false;" priority />
|
||||||
alt="{{ 'ABOUT.ALT.PROFILE' | translate }}"
|
</div>
|
||||||
draggable="false"
|
|
||||||
oncontextmenu="return false;"
|
|
||||||
priority />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="intro">
|
<div class="intro">
|
||||||
<h1>{{ 'ABOUT.HELLO' | translate }}</h1>
|
<h1>{{ 'ABOUT.HELLO' | translate }}</h1>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
{{ 'ABOUT.LEAD' | translate }}
|
{{ 'ABOUT.LEAD' | translate }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<mat-icon aria-hidden="true">work</mat-icon>
|
<mat-icon aria-hidden="true">work</mat-icon>
|
||||||
<span>{{ 'ABOUT.ROLE' | translate }}</span>
|
<span>{{ 'ABOUT.ROLE' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<mat-icon aria-hidden="true">location_on</mat-icon>
|
<mat-icon aria-hidden="true">location_on</mat-icon>
|
||||||
<span>{{ 'ABOUT.LOCATION' | translate }}</span>
|
<span>{{ 'ABOUT.LOCATION' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<mat-icon aria-hidden="true">email</mat-icon>
|
<mat-icon aria-hidden="true">email</mat-icon>
|
||||||
<a href="" (click)="SharedFunctions.openMail($event)">
|
<a href="" (click)="SharedFunctions.openMail($event)">
|
||||||
{{ 'ABOUT.CONTACT_ME' | translate }}
|
{{ 'ABOUT.CONTACT_ME' | translate }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<mat-icon svgIcon="github"></mat-icon>
|
<mat-icon>data_object</mat-icon>
|
||||||
<a href="{{UrlConstants.GIT_HUB}}" target="_blank" rel="noopener">GitHub</a>
|
<a href="{{UrlConstants.CODEBERG}}" target="_blank" rel="noopener">Codeberg</a>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<mat-icon svgIcon="linkedin"></mat-icon>
|
<mat-icon svgIcon="linkedin"></mat-icon>
|
||||||
<a href="{{UrlConstants.LINKED_IN}}" target="_blank" rel="noopener">LinkedIn</a>
|
<a href="{{UrlConstants.LINKED_IN}}" target="_blank" rel="noopener">LinkedIn</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,19 +43,28 @@
|
|||||||
<h2>{{ 'ABOUT.SECTION.SKILLS' | translate }}</h2>
|
<h2>{{ 'ABOUT.SECTION.SKILLS' | translate }}</h2>
|
||||||
<div class="chip-groups">
|
<div class="chip-groups">
|
||||||
<div>
|
<div>
|
||||||
<h3>{{ 'ABOUT.SECTION.PRIMARY' | translate }}</h3>
|
<h3>{{ 'ABOUT.SECTION.BACKEND_ARCH' | translate }}</h3>
|
||||||
<mat-chip-set aria-label="Primary skills">
|
<mat-chip-set aria-label="Backend and Architecture">
|
||||||
@for (s of primarySkills; track s) {
|
@for (s of skillsArchitecture; track s) {
|
||||||
<mat-chip >{{ s | translate }}</mat-chip>
|
<mat-chip>{{ s | translate }}</mat-chip>
|
||||||
}
|
}
|
||||||
</mat-chip-set>
|
</mat-chip-set>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3>{{ 'ABOUT.SECTION.TOOLSET' | translate }}</h3>
|
<h3>{{ 'ABOUT.SECTION.INFRA_CLOUD' | translate }}</h3>
|
||||||
<mat-chip-set aria-label="Toolset">
|
<mat-chip-set aria-label="Infrastructure and Cloud">
|
||||||
@for (t of toolset; track t) {
|
@for (s of skillsCore; track s) {
|
||||||
<mat-chip>{{ t | translate }}</mat-chip>
|
<mat-chip>{{ s | translate }}</mat-chip>
|
||||||
|
}
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>{{ 'ABOUT.SECTION.SIM_ALGO' | translate }}</h3>
|
||||||
|
<mat-chip-set aria-label="Simulation and Algorithms">
|
||||||
|
@for (s of skillsEngineering; track s) {
|
||||||
|
<mat-chip>{{ s | translate }}</mat-chip>
|
||||||
}
|
}
|
||||||
</mat-chip-set>
|
</mat-chip-set>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,39 +75,34 @@
|
|||||||
<h2 style="margin-left: 0.5rem;">{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
|
<h2 style="margin-left: 0.5rem;">{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
|
||||||
<div class="xp-list">
|
<div class="xp-list">
|
||||||
@for (entry of xpKeys; track entry.key) {
|
@for (entry of xpKeys; track entry.key) {
|
||||||
<div class="xp-item">
|
<div class="xp-item">
|
||||||
<div class="xp-head-grid">
|
<div class="xp-head-grid">
|
||||||
<div class="logo-wrap">
|
<div class="logo-wrap">
|
||||||
<img
|
<img src="{{entry.logo}}" alt="" class="company-logo" aria-hidden="true" />
|
||||||
src="{{entry.logo}}"
|
|
||||||
alt=""
|
|
||||||
class="company-logo"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="head-row">
|
|
||||||
<strong>{{ (entry.key + '.ROLE') | translate }}</strong>
|
|
||||||
<span class="time">{{ (entry.key + '.TIME') | translate }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="company-row">
|
|
||||||
{{ (entry.key + '.COMPANY') | translate }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="highlights">
|
<div class="head-row">
|
||||||
<ul>
|
<strong>{{ (entry.key + '.ROLE') | translate }}</strong>
|
||||||
<li>{{ entry.key + '.HIGHLIGHTS.P1' | translate }}</li>
|
<span class="time">{{ (entry.key + '.TIME') | translate }}</span>
|
||||||
<li>{{ entry.key + '.HIGHLIGHTS.P2' | translate }}</li>
|
|
||||||
<li>{{ entry.key + '.HIGHLIGHTS.P3' | translate }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="company-row">
|
||||||
|
{{ (entry.key + '.COMPANY') | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="highlights">
|
||||||
|
<ul>
|
||||||
|
<li>{{ entry.key + '.HIGHLIGHTS.P1' | translate }}</li>
|
||||||
|
<li>{{ entry.key + '.HIGHLIGHTS.P2' | translate }}</li>
|
||||||
|
<li>{{ entry.key + '.HIGHLIGHTS.P3' | translate }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if(entry.key !== xpKeys.at(xpKeys.length-1)?.key)
|
@if(entry.key !== xpKeys.at(xpKeys.length-1)?.key)
|
||||||
{
|
{
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@@ -118,21 +119,16 @@
|
|||||||
{{ (entry.key + '.DESCRIPTION') | translate }}
|
{{ (entry.key + '.DESCRIPTION') | translate }}
|
||||||
</div>
|
</div>
|
||||||
@if (entry.externalLink) {
|
@if (entry.externalLink) {
|
||||||
<div class="link-row">
|
<div class="link-row">
|
||||||
<a class="link-with-icon"
|
<a class="link-with-icon" href="{{entry.externalLink}}" target="_blank" rel="noopener noreferrer">
|
||||||
href="{{entry.externalLink}}"
|
<mat-icon>open_in_new</mat-icon>
|
||||||
target="_blank"
|
{{ (entry.key + '.LINK_EXTERNAL') | translate }}
|
||||||
rel="noopener noreferrer">
|
</a>
|
||||||
<mat-icon>open_in_new</mat-icon>
|
</div>
|
||||||
{{ (entry.key + '.LINK_EXTERNAL') | translate }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
<div class="link-row">
|
<div class="link-row">
|
||||||
<a class="link-with-icon"
|
<a class="link-with-icon" [routerLink]="['/projects']" [queryParams]="{ project: entry.identifier }"
|
||||||
[routerLink]="['/projects']"
|
rel="noopener noreferrer">
|
||||||
[queryParams]="{ project: entry.identifier }"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
<mat-icon>link</mat-icon>
|
<mat-icon>link</mat-icon>
|
||||||
{{ (entry.key + '.LINK_INTERNAL') | translate }}
|
{{ (entry.key + '.LINK_INTERNAL') | translate }}
|
||||||
</a>
|
</a>
|
||||||
@@ -148,9 +144,9 @@
|
|||||||
|
|
||||||
@if(entry.key !== projectKeys.at(projectKeys.length-1)?.key)
|
@if(entry.key !== projectKeys.at(projectKeys.length-1)?.key)
|
||||||
{
|
{
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
@@ -160,18 +156,18 @@
|
|||||||
<div class="xp-list">
|
<div class="xp-list">
|
||||||
<div class="xp-item">
|
<div class="xp-item">
|
||||||
@for (entry of educationKeys; track entry.key) {
|
@for (entry of educationKeys; track entry.key) {
|
||||||
<div class="head-row">
|
<div class="head-row">
|
||||||
<strong>{{ (entry.key + '.WHERE') | translate }}</strong>
|
<strong>{{ (entry.key + '.WHERE') | translate }}</strong>
|
||||||
<span class="time">{{ (entry.key + '.WHEN') | translate }}</span>
|
<span class="time">{{ (entry.key + '.WHEN') | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="company-row">
|
<div class="company-row">
|
||||||
{{ (entry.key + '.WHAT') | translate }}
|
{{ (entry.key + '.WHAT') | translate }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(entry.key !== educationKeys.at(educationKeys.length-1)?.key)
|
@if(entry.key !== educationKeys.at(educationKeys.length-1)?.key)
|
||||||
{
|
{
|
||||||
<mat-divider style="margin-top: .5rem; margin-bottom: .5rem"></mat-divider>
|
<mat-divider style="margin-top: .5rem; margin-bottom: .5rem"></mat-divider>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
.about {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero block: Photo + Intro */
|
|
||||||
.hero {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 425px 1fr;
|
|
||||||
gap: 1.25rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: var(--app-card-background);
|
|
||||||
|
|
||||||
.photo {
|
|
||||||
align-items:flex-start; justify-content:center;
|
|
||||||
img {
|
|
||||||
display:block;
|
|
||||||
width: 100%; height: auto;
|
|
||||||
max-width: 425px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 6px 24px rgba(0,0,0,.25);
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
|
||||||
display:flex; flex-direction:column; gap:.5rem;
|
|
||||||
h1 { margin-top: .25rem }
|
|
||||||
.lead { opacity:.9; margin: .25rem 0 0.5rem; }
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
display:flex; flex-direction:column; gap:.25rem; margin-bottom: 0.5rem;
|
|
||||||
.row {
|
|
||||||
display:flex; align-items:center; gap:.4rem;
|
|
||||||
a { color: inherit; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display:flex; gap:.5rem; flex-wrap:wrap; margin-top:.5rem;
|
|
||||||
.mat-icon { margin-right:.25rem; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Skills block */
|
|
||||||
.skills {
|
|
||||||
padding: 5px;
|
|
||||||
h2 { margin-top: .25rem; margin-left: .25rem; }
|
|
||||||
.chip-groups {
|
|
||||||
margin-left: .25rem;
|
|
||||||
display:grid; gap:1rem;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
h3 { margin: .2rem 0 .4rem; font-size: .95rem; opacity:.85; }
|
|
||||||
mat-chip-set {
|
|
||||||
display:flex; flex-wrap:wrap; gap:.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Experience block */
|
|
||||||
.experience {
|
|
||||||
padding: 5px;
|
|
||||||
h2 { margin-top: .25rem; margin-left: .25rem; }
|
|
||||||
.xp-item {
|
|
||||||
.xp-head {
|
|
||||||
display:flex; align-items:baseline; gap:.5rem;
|
|
||||||
.time { opacity:.75; font-size:.9rem; }
|
|
||||||
}
|
|
||||||
.xp-sub { opacity:.9; margin-bottom:.25rem; }
|
|
||||||
ul { margin: .25rem 0 .5rem 1.15rem; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Experience block */
|
|
||||||
.projects {
|
|
||||||
padding: 5px;
|
|
||||||
h2 { margin-top: .25rem;margin-left: .25rem; }
|
|
||||||
.xp-list {
|
|
||||||
margin-left: .25rem;
|
|
||||||
display: grid; gap: .75rem;
|
|
||||||
}
|
|
||||||
.xp-item {
|
|
||||||
.xp-head {
|
|
||||||
display:flex; align-items:baseline; gap:.5rem;
|
|
||||||
.time { opacity:.75; font-size:.9rem; }
|
|
||||||
}
|
|
||||||
.xp-sub { opacity:.9; margin-bottom:.25rem; }
|
|
||||||
ul { margin: .25rem 0 .5rem 1.15rem; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Experience block */
|
|
||||||
.education {
|
|
||||||
padding: 5px;
|
|
||||||
h2 { margin-top: .25rem;margin-left: .25rem; }
|
|
||||||
.xp-list {
|
|
||||||
margin-left: .25rem;
|
|
||||||
display: grid; gap: .75rem;
|
|
||||||
}
|
|
||||||
.xp-item {
|
|
||||||
.xp-head {
|
|
||||||
display:flex; align-items:baseline; gap:.5rem;
|
|
||||||
.time { opacity:.75; font-size:.9rem; }
|
|
||||||
}
|
|
||||||
.xp-sub { opacity:.9; margin-bottom:.25rem; }
|
|
||||||
ul { margin: .25rem 0 .5rem 1.15rem; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.hero { grid-template-columns: 1fr; }
|
|
||||||
.hero .photo { justify-content: flex-start; }
|
|
||||||
.skills .chip-groups { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.xp-head-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: calc(48px + .75rem) 1fr; /* 1: Logo, 2: Text */
|
|
||||||
grid-template-rows: auto auto; /* 1: Role/Time, 2: Company */
|
|
||||||
column-gap: .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-wrap {
|
|
||||||
grid-row: 1 / span 2;
|
|
||||||
grid-column: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-logo {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
object-fit: contain;
|
|
||||||
opacity: .9;
|
|
||||||
border-radius: 10%;
|
|
||||||
background-color: var(--app-logo-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.head-row {
|
|
||||||
grid-row: 1;
|
|
||||||
grid-column: 2;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: .5rem 1rem;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
opacity: .75; font-size: .9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.company-row {
|
|
||||||
grid-row: 2;
|
|
||||||
grid-column: 2;
|
|
||||||
margin-top: .1rem;
|
|
||||||
opacity: .85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlights {
|
|
||||||
margin-top: .4rem;
|
|
||||||
margin-left: .75rem;
|
|
||||||
padding-left: 1.2rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: .2rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlights-noMargin {
|
|
||||||
margin-top: .4rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: .2rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -126,26 +126,30 @@ export class AboutComponent {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
primarySkills = [
|
skillsCore = [
|
||||||
'ABOUT.SKILLS.JAVA',
|
'ABOUT.SKILLS.JAVA',
|
||||||
'ABOUT.SKILLS.SPRING',
|
'ABOUT.SKILLS.SPRING',
|
||||||
'ABOUT.SKILLS.ANGULAR',
|
'ABOUT.SKILLS.ANGULAR',
|
||||||
'ABOUT.SKILLS.DOCKER',
|
'ABOUT.SKILLS.TYPESCRIPT',
|
||||||
'ABOUT.SKILLS.UNITY',
|
|
||||||
'ABOUT.SKILLS.PYTHON',
|
|
||||||
'ABOUT.SKILLS.CSHARP',
|
'ABOUT.SKILLS.CSHARP',
|
||||||
'ABOUT.SKILLS.TYPESCRIPT'
|
'ABOUT.SKILLS.PYTHON'
|
||||||
];
|
];
|
||||||
|
|
||||||
toolset = [
|
skillsArchitecture = [
|
||||||
'ABOUT.TOOLS.GIT',
|
'ABOUT.SKILLS.ARCH_MICROSERVICES',
|
||||||
'ABOUT.TOOLS.GITHUB',
|
'ABOUT.SKILLS.ARCH_CLOUD',
|
||||||
'ABOUT.TOOLS.GITLAB',
|
'ABOUT.TOOLS.DOCKER',
|
||||||
'ABOUT.TOOLS.JENKINS',
|
|
||||||
'ABOUT.TOOLS.K8S',
|
'ABOUT.TOOLS.K8S',
|
||||||
'ABOUT.TOOLS.POSTGRES',
|
'ABOUT.TOOLS.JENKINS',
|
||||||
'ABOUT.TOOLS.MONGO',
|
'ABOUT.TOOLS.POSTGRES'
|
||||||
'ABOUT.TOOLS.GRAFANA',
|
];
|
||||||
|
|
||||||
|
skillsEngineering = [
|
||||||
|
'ABOUT.SKILLS.ENG_ALGO',
|
||||||
|
'ABOUT.SKILLS.ENG_SIM',
|
||||||
|
'ABOUT.SKILLS.ENG_GPU',
|
||||||
|
'ABOUT.SKILLS.UNITY',
|
||||||
|
'ABOUT.SKILLS.ENG_PERF'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected readonly UrlConstants = UrlConstants;
|
protected readonly UrlConstants = UrlConstants;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<div class="container">
|
<div class="card-grid">
|
||||||
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
|
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
|
||||||
<div class="category-cards">
|
</div>
|
||||||
|
<div class="card-grid">
|
||||||
@for (category of categories$ | async; track category.id) {
|
@for (category of categories$ | async; track category.id) {
|
||||||
<mat-card [routerLink]="[category.routerLink]">
|
<mat-card class="algo-card" [routerLink]="[category.routerLink]">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>{{ category.title | translate }}</mat-card-title>
|
<mat-card-title>{{ category.title | translate }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
@@ -12,4 +13,3 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
.container {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-cards {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
|
|
||||||
mat-card {
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 450px;
|
|
||||||
max-width: 450px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { AlgorithmsService } from './service/algorithms.service';
|
import { AlgorithmsService } from './algorithms.service';
|
||||||
import { AlgorithmCategory } from './models/algorithm-category';
|
import { AlgorithmCategory } from './algorithm-category';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
|||||||
65
src/app/pages/algorithms/algorithms.service.ts
Normal file
65
src/app/pages/algorithms/algorithms.service.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AlgorithmCategory } from './algorithm-category';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import {RouterConstants} from '../../constants/RouterConstants';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AlgorithmsService {
|
||||||
|
|
||||||
|
private readonly categories: AlgorithmCategory[] = [
|
||||||
|
{
|
||||||
|
id: 'pathfinding',
|
||||||
|
title: 'ALGORITHM.PATHFINDING.TITLE',
|
||||||
|
description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.PATHFINDING.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sorting',
|
||||||
|
title: 'ALGORITHM.SORTING.TITLE',
|
||||||
|
description: 'ALGORITHM.SORTING.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.SORTING.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gameOfLife',
|
||||||
|
title: 'ALGORITHM.GOL.TITLE',
|
||||||
|
description: 'ALGORITHM.GOL.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.GOL.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'labyrinth',
|
||||||
|
title: 'ALGORITHM.LABYRINTH.TITLE',
|
||||||
|
description: 'ALGORITHM.LABYRINTH.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.LABYRINTH.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fractal',
|
||||||
|
title: 'ALGORITHM.FRACTAL.TITLE',
|
||||||
|
description: 'ALGORITHM.FRACTAL.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.FRACTAL.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fractal3d',
|
||||||
|
title: 'ALGORITHM.FRACTAL3D.TITLE',
|
||||||
|
description: 'ALGORITHM.FRACTAL3D.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.FRACTAL3d.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pendulum',
|
||||||
|
title: 'ALGORITHM.PENDULUM.TITLE',
|
||||||
|
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.PENDULUM.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cloth',
|
||||||
|
title: 'ALGORITHM.CLOTH.TITLE',
|
||||||
|
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.CLOTH.LINK
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
getCategories(): Observable<AlgorithmCategory[]> {
|
||||||
|
return of(this.categories);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/pages/algorithms/cloth/cloth.component.html
Normal file
23
src/app/pages/algorithms/cloth/cloth.component.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<mat-card class="algo-container">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{ 'CLOTH.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-raised-button color="primary" (click)="toggleWind()">
|
||||||
|
{{ isWindActive ? ('CLOTH.WIND_OFF' | translate) : ('CLOTH.WIND_ON' | translate) }}
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="primary" (click)="toggleMesh()">
|
||||||
|
{{ isOutlineActive ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-babylon-canvas
|
||||||
|
[config]="renderConfig"
|
||||||
|
(sceneReady)="onSceneReady($event)"
|
||||||
|
(sceneResized)="onSceneReady($event)"
|
||||||
|
/>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
0
src/app/pages/algorithms/cloth/cloth.component.scss
Normal file
0
src/app/pages/algorithms/cloth/cloth.component.scss
Normal file
377
src/app/pages/algorithms/cloth/cloth.component.ts
Normal file
377
src/app/pages/algorithms/cloth/cloth.component.ts
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
/**
|
||||||
|
* File: cloth.component.ts
|
||||||
|
* Description: Component for cloth simulation using WebGPU compute shaders.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
|
||||||
|
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';
|
||||||
|
import {
|
||||||
|
CLOTH_FRAGMENT_SHADER_WGSL,
|
||||||
|
CLOTH_INTEGRATE_COMPUTE_WGSL,
|
||||||
|
CLOTH_SOLVE_COMPUTE_WGSL,
|
||||||
|
CLOTH_VELOCITY_COMPUTE_WGSL,
|
||||||
|
CLOTH_VERTEX_SHADER_WGSL
|
||||||
|
} from './cloth.shader';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {ClothBuffers, ClothConfig, ClothData, ClothPipelines} from './cloth.model';
|
||||||
|
import {Information} from '../information/information';
|
||||||
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-cloth',
|
||||||
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
TranslatePipe,
|
||||||
|
BabylonCanvas,
|
||||||
|
MatButton,
|
||||||
|
Information
|
||||||
|
],
|
||||||
|
templateUrl: './cloth.component.html',
|
||||||
|
styleUrl: './cloth.component.scss',
|
||||||
|
})
|
||||||
|
export class ClothComponent {
|
||||||
|
private currentSceneData: SceneEventData | null = null;
|
||||||
|
private simulationTime: number = 0;
|
||||||
|
private clothMesh: GroundMesh | null = null;
|
||||||
|
public isWindActive: boolean = false;
|
||||||
|
public isOutlineActive: boolean = false;
|
||||||
|
|
||||||
|
public renderConfig: RenderConfig = {
|
||||||
|
mode: '3D',
|
||||||
|
initialViewSize: 20,
|
||||||
|
shaderLanguage: ShaderLanguage.WGSL
|
||||||
|
};
|
||||||
|
|
||||||
|
algoInformation: AlgorithmInformation = {
|
||||||
|
title: 'CLOTH.EXPLANATION.TITLE',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
name: 'CLOTH.EXPLANATION.CLOTH_SIMULATION_EXPLANATION_TITLE',
|
||||||
|
description: 'CLOTH.EXPLANATION.CLOTH_SIMULATION_EXPLANATION',
|
||||||
|
link: UrlConstants.CLOTH_SIMULATION_WIKI,
|
||||||
|
translateName: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CLOTH.EXPLANATION.XPBD_EXPLANATION_TITLE',
|
||||||
|
description: 'CLOTH.EXPLANATION.XPBD_EXPLANATION',
|
||||||
|
link: UrlConstants.XPBD_WIKI,
|
||||||
|
translateName: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CLOTH.EXPLANATION.GPU_PARALLELIZATION_EXPLANATION_TITLE',
|
||||||
|
description: 'CLOTH.EXPLANATION.GPU_PARALLELIZATION_EXPLANATION',
|
||||||
|
link: UrlConstants.GPU_COMPUTING_WIKI,
|
||||||
|
translateName: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CLOTH.EXPLANATION.DATA_STRUCTURES_EXPLANATION_TITLE',
|
||||||
|
description: 'CLOTH.EXPLANATION.DATA_STRUCTURES_EXPLANATION',
|
||||||
|
link: UrlConstants.DATA_STRUCTURE_WIKI,
|
||||||
|
translateName: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
disclaimer: 'CLOTH.EXPLANATION.DISCLAIMER',
|
||||||
|
disclaimerBottom: '',
|
||||||
|
disclaimerListEntry: ['CLOTH.EXPLANATION.DISCLAIMER_1', 'CLOTH.EXPLANATION.DISCLAIMER_2', 'CLOTH.EXPLANATION.DISCLAIMER_3', 'CLOTH.EXPLANATION.DISCLAIMER_4']
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the Babylon scene is ready.
|
||||||
|
* @param event The scene event data.
|
||||||
|
*/
|
||||||
|
public onSceneReady(event: SceneEventData): void {
|
||||||
|
this.currentSceneData = event;
|
||||||
|
this.createSimulation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleWind(): void {
|
||||||
|
this.isWindActive = !this.isWindActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleMesh(): void {
|
||||||
|
this.isOutlineActive = !this.isOutlineActive;
|
||||||
|
if (!this.clothMesh?.material) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clothMesh.material.wireframe = this.isOutlineActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes and starts the cloth simulation.
|
||||||
|
*/
|
||||||
|
private createSimulation(): void {
|
||||||
|
if (!this.currentSceneData) return;
|
||||||
|
|
||||||
|
const { engine, scene } = this.currentSceneData;
|
||||||
|
|
||||||
|
// 1. Define physics parameters
|
||||||
|
const config = this.getClothConfig();
|
||||||
|
|
||||||
|
// 2. Generate initial CPU data (positions, constraints)
|
||||||
|
const clothData = this.generateClothData(config);
|
||||||
|
|
||||||
|
// 3. Upload to GPU
|
||||||
|
const buffers = this.createStorageBuffers(engine, clothData);
|
||||||
|
|
||||||
|
// 4. Create Compute Shaders
|
||||||
|
const pipelines = this.setupComputePipelines(engine, buffers);
|
||||||
|
|
||||||
|
// 5. Setup Rendering (Mesh, Material, Camera)
|
||||||
|
this.setupRenderMesh(scene, config, buffers.positions);
|
||||||
|
|
||||||
|
// 6. Start the physics loop
|
||||||
|
this.startRenderLoop(engine, scene, config, buffers, pipelines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 1. CONFIGURATION
|
||||||
|
// ========================================================================
|
||||||
|
private getClothConfig(): ClothConfig {
|
||||||
|
const gridWidth = 100;
|
||||||
|
const gridHeight = 100;
|
||||||
|
const spacing = 0.05;
|
||||||
|
const density = 1.0;
|
||||||
|
const particleArea = spacing * spacing;
|
||||||
|
const particleMass = density * particleArea;
|
||||||
|
|
||||||
|
return {
|
||||||
|
gridWidth,
|
||||||
|
gridHeight,
|
||||||
|
spacing,
|
||||||
|
density,
|
||||||
|
numVertices: gridWidth * gridHeight,
|
||||||
|
particleInvMass: 1.0 / particleMass
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 2. DATA GENERATION (CPU)
|
||||||
|
// ========================================================================
|
||||||
|
private generateClothData(config: ClothConfig): ClothData {
|
||||||
|
const positionsData = new Float32Array(config.numVertices * 4);
|
||||||
|
const prevPositionsData = new Float32Array(config.numVertices * 4);
|
||||||
|
const velocitiesData = new Float32Array(config.numVertices * 4);
|
||||||
|
|
||||||
|
const constraintsP0: number[] = [];
|
||||||
|
const constraintsP1: number[] = [];
|
||||||
|
const constraintsP2: number[] = [];
|
||||||
|
const constraintsP3: number[] = [];
|
||||||
|
|
||||||
|
const addConstraint = (arr: number[], a: number, b: number): void => {
|
||||||
|
arr.push(a, b, config.spacing, 1.0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill positions (Pin top row)
|
||||||
|
for (let y = 0; y < config.gridHeight; y++) {
|
||||||
|
for (let x = 0; x < config.gridWidth; x++) {
|
||||||
|
const idx = (y * config.gridWidth + x) * 4;
|
||||||
|
positionsData[idx + 0] = (x - config.gridWidth / 2) * config.spacing;
|
||||||
|
positionsData[idx + 1] = 5.0 - (y * config.spacing);
|
||||||
|
positionsData[idx + 2] = 0.0;
|
||||||
|
positionsData[idx + 3] = (y === 0) ? 0.0 : config.particleInvMass;
|
||||||
|
|
||||||
|
prevPositionsData[idx + 0] = positionsData[idx + 0];
|
||||||
|
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
||||||
|
prevPositionsData[idx + 2] = positionsData[idx + 2];
|
||||||
|
prevPositionsData[idx + 3] = positionsData[idx + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraintsP4: number[] = [];
|
||||||
|
const constraintsP5: number[] = [];
|
||||||
|
const constraintsP6: number[] = [];
|
||||||
|
const constraintsP7: number[] = [];
|
||||||
|
|
||||||
|
const diagSpacing = config.spacing * Math.SQRT2;
|
||||||
|
const addDiagConstraint = (arr: number[], a: number, b: number): void => {
|
||||||
|
arr.push(a, b, diagSpacing, 1.0);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||||
|
const arr = (y % 2 === 0) ? constraintsP4 : constraintsP5;
|
||||||
|
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||||
|
addDiagConstraint(arr, y * config.gridWidth + x, (y + 1) * config.gridWidth + (x + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||||
|
const arr = (y % 2 === 0) ? constraintsP6 : constraintsP7;
|
||||||
|
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||||
|
addDiagConstraint(arr, y * config.gridWidth + (x + 1), (y + 1) * config.gridWidth + x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
positions: positionsData,
|
||||||
|
prevPositions: prevPositionsData,
|
||||||
|
velocities: velocitiesData,
|
||||||
|
constraints: [
|
||||||
|
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||||
|
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||||
|
],
|
||||||
|
params: new Float32Array(8)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 3. BUFFER CREATION (GPU)
|
||||||
|
// ========================================================================
|
||||||
|
private createStorageBuffers(engine: WebGPUEngine, data: ClothData): ClothBuffers {
|
||||||
|
const createBuffer = (arrayData: Float32Array | number[]): StorageBuffer => {
|
||||||
|
const buffer = new StorageBuffer(engine, arrayData.length * 4);
|
||||||
|
buffer.update(arrayData instanceof Float32Array ? arrayData : new Float32Array(arrayData));
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
positions: createBuffer(data.positions),
|
||||||
|
prevPositions: createBuffer(data.prevPositions),
|
||||||
|
velocities: createBuffer(data.velocities),
|
||||||
|
params: createBuffer(data.params),
|
||||||
|
constraints: data.constraints.map(cData => createBuffer(cData))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 4. COMPUTE SHADERS
|
||||||
|
// ========================================================================
|
||||||
|
private setupComputePipelines(engine: WebGPUEngine, buffers: ClothBuffers): ClothPipelines {
|
||||||
|
|
||||||
|
// Helper for integrating & velocity
|
||||||
|
const createBasicShader = (name: string, source: string) => {
|
||||||
|
const cs = new ComputeShader(name, engine, { computeSource: source }, {
|
||||||
|
bindingsMapping: {
|
||||||
|
"p": { group: 0, binding: 0 },
|
||||||
|
"positions": { group: 0, binding: 1 },
|
||||||
|
"prev_positions": { group: 0, binding: 2 },
|
||||||
|
"velocities": { group: 0, binding: 3 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cs.setStorageBuffer("p", buffers.params);
|
||||||
|
cs.setStorageBuffer("positions", buffers.positions);
|
||||||
|
cs.setStorageBuffer("prev_positions", buffers.prevPositions);
|
||||||
|
cs.setStorageBuffer("velocities", buffers.velocities);
|
||||||
|
return cs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper for solvers
|
||||||
|
const createSolverShader = (name: string, constraintBuffer: StorageBuffer) => {
|
||||||
|
const cs = new ComputeShader(name, engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, {
|
||||||
|
bindingsMapping: {
|
||||||
|
"p": { group: 0, binding: 0 },
|
||||||
|
"positions": { group: 0, binding: 1 },
|
||||||
|
"constraints": { group: 0, binding: 2 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cs.setStorageBuffer("p", buffers.params);
|
||||||
|
cs.setStorageBuffer("positions", buffers.positions);
|
||||||
|
cs.setStorageBuffer("constraints", constraintBuffer);
|
||||||
|
return cs;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
integrate: createBasicShader("integrate", CLOTH_INTEGRATE_COMPUTE_WGSL),
|
||||||
|
solvers: buffers.constraints.map((cBuffer, i) => createSolverShader(`solve${i}`, cBuffer)),
|
||||||
|
velocity: createBasicShader("velocity", CLOTH_VELOCITY_COMPUTE_WGSL)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 5. RENDERING SETUP
|
||||||
|
// ========================================================================
|
||||||
|
private setupRenderMesh(scene: Scene, config: ClothConfig, positionsBuffer: StorageBuffer): void {
|
||||||
|
if (this.clothMesh) {
|
||||||
|
scene.removeMesh(this.clothMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: config.gridWidth - 1 }, scene);
|
||||||
|
|
||||||
|
const clothMaterial = new ShaderMaterial("clothMat", scene, {
|
||||||
|
vertexSource: CLOTH_VERTEX_SHADER_WGSL,
|
||||||
|
fragmentSource: CLOTH_FRAGMENT_SHADER_WGSL
|
||||||
|
}, {
|
||||||
|
attributes: ["position", "uv"],
|
||||||
|
uniforms: ["viewProjection"],
|
||||||
|
storageBuffers: ["positions"],
|
||||||
|
shaderLanguage: ShaderLanguage.WGSL
|
||||||
|
});
|
||||||
|
|
||||||
|
clothMaterial.backFaceCulling = false;
|
||||||
|
clothMaterial.setStorageBuffer("positions", positionsBuffer);
|
||||||
|
this.clothMesh.material = clothMaterial;
|
||||||
|
|
||||||
|
const camera = scene.activeCamera as ArcRotateCamera;
|
||||||
|
if (camera) {
|
||||||
|
camera.alpha = Math.PI / 4;
|
||||||
|
camera.beta = Math.PI / 2.5;
|
||||||
|
camera.radius = 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 6. RENDER LOOP
|
||||||
|
// ========================================================================
|
||||||
|
private startRenderLoop(engine: WebGPUEngine, scene: Scene, config: ClothConfig, buffers: ClothBuffers, pipelines: ClothPipelines): void {
|
||||||
|
const paramsData = new Float32Array(8);
|
||||||
|
|
||||||
|
// 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 dispatchXConstraints = constraintsLength.map(len => Math.ceil(len / 64));
|
||||||
|
const dispatchXVertices = Math.ceil(config.numVertices / 64);
|
||||||
|
const substeps = 15;
|
||||||
|
|
||||||
|
scene.onBeforeRenderObservable.clear();
|
||||||
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
|
this.simulationTime += engine.getDeltaTime() / 1000.0;
|
||||||
|
|
||||||
|
// Update Physics Parameters
|
||||||
|
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;
|
||||||
|
|
||||||
|
paramsData[0] = 0.016; // dt
|
||||||
|
paramsData[1] = -9.81; // gravity
|
||||||
|
paramsData[2] = scaledCompliance;
|
||||||
|
paramsData[3] = config.numVertices;
|
||||||
|
paramsData[4] = windX;
|
||||||
|
paramsData[5] = windY;
|
||||||
|
paramsData[6] = windZ;
|
||||||
|
paramsData[7] = this.simulationTime;
|
||||||
|
|
||||||
|
buffers.params.update(paramsData);
|
||||||
|
|
||||||
|
// 1. Predict positions
|
||||||
|
pipelines.integrate.dispatch(dispatchXVertices, 1, 1);
|
||||||
|
|
||||||
|
// 2. XPBD Solver (Substeps) - Graph Coloring Phase
|
||||||
|
for (let i = 0; i < substeps; i++) {
|
||||||
|
for (let phase = 0; phase < pipelines.solvers.length; phase++) {
|
||||||
|
pipelines.solvers[phase].dispatch(dispatchXConstraints[phase], 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update velocities
|
||||||
|
pipelines.velocity.dispatch(dispatchXVertices, 1, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/app/pages/algorithms/cloth/cloth.model.ts
Normal file
36
src/app/pages/algorithms/cloth/cloth.model.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// --- SIMULATION CONFIGURATION ---
|
||||||
|
import {ComputeShader, StorageBuffer} from '@babylonjs/core';
|
||||||
|
|
||||||
|
export interface ClothConfig {
|
||||||
|
gridWidth: number;
|
||||||
|
gridHeight: number;
|
||||||
|
spacing: number;
|
||||||
|
density: number;
|
||||||
|
numVertices: number;
|
||||||
|
particleInvMass: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RAW CPU DATA ---
|
||||||
|
export interface ClothData {
|
||||||
|
positions: Float32Array;
|
||||||
|
prevPositions: Float32Array;
|
||||||
|
velocities: Float32Array;
|
||||||
|
constraints: number[][]; // Array containing the 4 phases
|
||||||
|
params: Float32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- WEBGPU BUFFERS ---
|
||||||
|
export interface ClothBuffers {
|
||||||
|
positions: StorageBuffer;
|
||||||
|
prevPositions: StorageBuffer;
|
||||||
|
velocities: StorageBuffer;
|
||||||
|
params: StorageBuffer;
|
||||||
|
constraints: StorageBuffer[]; // 4 phase buffers
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- COMPUTE PIPELINES ---
|
||||||
|
export interface ClothPipelines {
|
||||||
|
integrate: ComputeShader;
|
||||||
|
solvers: ComputeShader[]; // 4 solve shaders
|
||||||
|
velocity: ComputeShader;
|
||||||
|
}
|
||||||
213
src/app/pages/algorithms/cloth/cloth.shader.ts
Normal file
213
src/app/pages/algorithms/cloth/cloth.shader.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* File: cloth.shader.ts
|
||||||
|
* Description: WGSL shaders for cloth simulation and rendering.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- SHARED DATA STRUCTURES ---
|
||||||
|
export const CLOTH_SHARED_STRUCTS = `
|
||||||
|
struct Params {
|
||||||
|
dt: f32,
|
||||||
|
gravity_y: f32,
|
||||||
|
compliance: f32,
|
||||||
|
numVertices: f32,
|
||||||
|
wind_x: f32,
|
||||||
|
wind_y: f32,
|
||||||
|
wind_z: f32,
|
||||||
|
time: f32
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// VERTEX SHADER
|
||||||
|
// ==========================================
|
||||||
|
export const CLOTH_VERTEX_SHADER_WGSL = `
|
||||||
|
attribute uv : vec2<f32>;
|
||||||
|
var<storage, read> positions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn main(input : VertexInputs) -> FragmentInputs {
|
||||||
|
var output : FragmentInputs;
|
||||||
|
|
||||||
|
let worldPos = positions[input.vertexIndex].xyz;
|
||||||
|
output.position = uniforms.viewProjection * vec4<f32>(worldPos, 1.0);
|
||||||
|
|
||||||
|
output.vUV = input.uv;
|
||||||
|
output.vWorldPos = worldPos; // Position weitergeben
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// FRAGMENT SHADER
|
||||||
|
// ==========================================
|
||||||
|
export const CLOTH_FRAGMENT_SHADER_WGSL = `
|
||||||
|
varying vUV : vec2<f32>;
|
||||||
|
varying vWorldPos : vec3<f32>;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn main(input: FragmentInputs) -> FragmentOutputs {
|
||||||
|
var output: FragmentOutputs;
|
||||||
|
|
||||||
|
let dx = dpdx(input.vWorldPos);
|
||||||
|
let dy = dpdy(input.vWorldPos);
|
||||||
|
let normal = normalize(cross(dx, dy));
|
||||||
|
let lightDir = normalize(vec3<f32>(1.0, 1.0, 0.5));
|
||||||
|
let diffuse = max(0.0, abs(dot(normal, lightDir)));
|
||||||
|
let ambient = 0.3;
|
||||||
|
let lightIntensity = ambient + (diffuse * 0.7);
|
||||||
|
let grid = (floor(input.vUV.x * 20.0) + floor(input.vUV.y * 20.0)) % 2.0;
|
||||||
|
let baseColor = mix(vec3<f32>(0.8, 0.4, 0.15), vec3<f32>(0.9, 0.5, 0.2), grid);
|
||||||
|
let finalColor = baseColor * lightIntensity;
|
||||||
|
|
||||||
|
output.color = vec4<f32>(finalColor, 1.0);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// PASS 1: INTEGRATION (Apply Forces & Predict Positions)
|
||||||
|
// =====================================================================
|
||||||
|
export const CLOTH_INTEGRATE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
||||||
|
@group(0) @binding(0) var<storage, read> p : Params;
|
||||||
|
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(2) var<storage, read_write> prev_positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(3) var<storage, read_write> velocities : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
if (f32(idx) >= p.numVertices) { return; }
|
||||||
|
|
||||||
|
var pos = positions[idx];
|
||||||
|
var vel = velocities[idx];
|
||||||
|
let invMass = pos.w;
|
||||||
|
|
||||||
|
if (invMass > 0.0) {
|
||||||
|
vel.y = vel.y + (p.gravity_y * p.dt);
|
||||||
|
|
||||||
|
let flutter = sin(pos.x * 2.0 + p.time * 5.0) * cos(pos.y * 2.0 + p.time * 3.0);
|
||||||
|
|
||||||
|
let windForce = vec3<f32>(
|
||||||
|
p.wind_x + (flutter * p.wind_x * 0.8),
|
||||||
|
p.wind_y + (flutter * 2.0), // Leichter Auftrieb durchs Flattern
|
||||||
|
p.wind_z + (flutter * p.wind_z * 0.8)
|
||||||
|
);
|
||||||
|
|
||||||
|
vel.x = vel.x + (windForce.x * p.dt);
|
||||||
|
vel.y = vel.y + (windForce.y * p.dt);
|
||||||
|
vel.z = vel.z + (windForce.z * p.dt);
|
||||||
|
|
||||||
|
prev_positions[idx] = pos;
|
||||||
|
|
||||||
|
pos.x = pos.x + vel.x * p.dt;
|
||||||
|
pos.y = pos.y + vel.y * p.dt;
|
||||||
|
pos.z = pos.z + vel.z * p.dt;
|
||||||
|
|
||||||
|
positions[idx] = pos;
|
||||||
|
velocities[idx] = vel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// PASS 2: SOLVE CONSTRAINTS (The core of XPBD)
|
||||||
|
// =====================================================================
|
||||||
|
export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
||||||
|
@group(0) @binding(0) var<storage, read> p : Params;
|
||||||
|
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(2) var<storage, read> constraints : array<vec4<f32>>; // <--- Read-only as we do not modify them here
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
|
||||||
|
// Query the GPU directly for the length of the passed array
|
||||||
|
if (idx >= arrayLength(&constraints)) { return; }
|
||||||
|
|
||||||
|
let constraint = constraints[idx];
|
||||||
|
let isActive = constraint.w;
|
||||||
|
|
||||||
|
if (isActive < 0.5) { return; }
|
||||||
|
|
||||||
|
let idA = u32(constraint.x);
|
||||||
|
let idB = u32(constraint.y);
|
||||||
|
let restLength = constraint.z;
|
||||||
|
|
||||||
|
var pA = positions[idA];
|
||||||
|
var pB = positions[idB];
|
||||||
|
|
||||||
|
let wA = pA.w;
|
||||||
|
let wB = pB.w;
|
||||||
|
let wSum = wA + wB;
|
||||||
|
|
||||||
|
if (wSum <= 0.0) { return; }
|
||||||
|
|
||||||
|
let dir = pA.xyz - pB.xyz;
|
||||||
|
let dist = length(dir);
|
||||||
|
|
||||||
|
if (dist < 0.0001) { return; }
|
||||||
|
|
||||||
|
let n = dir / dist;
|
||||||
|
let C = dist - restLength;
|
||||||
|
|
||||||
|
let alpha = p.compliance / (p.dt * p.dt);
|
||||||
|
let lambda = -C / (wSum + alpha);
|
||||||
|
|
||||||
|
let corrA = n * (lambda * wA);
|
||||||
|
let corrB = n * (-lambda * wB);
|
||||||
|
|
||||||
|
// This is because we are using graph coloring to be thread safe
|
||||||
|
if (wA > 0.0) {
|
||||||
|
positions[idA].x = positions[idA].x + corrA.x;
|
||||||
|
positions[idA].y = positions[idA].y + corrA.y;
|
||||||
|
positions[idA].z = positions[idA].z + corrA.z;
|
||||||
|
}
|
||||||
|
if (wB > 0.0) {
|
||||||
|
positions[idB].x = positions[idB].x + corrB.x;
|
||||||
|
positions[idB].y = positions[idB].y + corrB.y;
|
||||||
|
positions[idB].z = positions[idB].z + corrB.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// PASS 3: VELOCITY UPDATE (Derive velocity from position changes)
|
||||||
|
// =====================================================================
|
||||||
|
export const CLOTH_VELOCITY_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
||||||
|
@group(0) @binding(0) var<storage, read> p : Params;
|
||||||
|
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(2) var<storage, read_write> prev_positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(3) var<storage, read_write> velocities : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
if (f32(idx) >= p.numVertices) { return; }
|
||||||
|
|
||||||
|
let pos = positions[idx];
|
||||||
|
let prev = prev_positions[idx];
|
||||||
|
let invMass = pos.w;
|
||||||
|
|
||||||
|
if (invMass > 0.0) {
|
||||||
|
var vel = velocities[idx];
|
||||||
|
|
||||||
|
// v = (p - p_prev) / dt
|
||||||
|
vel.x = (pos.x - prev.x) / p.dt;
|
||||||
|
vel.y = (pos.y - prev.y) / p.dt;
|
||||||
|
vel.z = (pos.z - prev.z) / p.dt;
|
||||||
|
|
||||||
|
// Optional: Add simple damping here
|
||||||
|
// vel = vel * 0.99;
|
||||||
|
|
||||||
|
velocities[idx] = vel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<mat-card class="container">
|
<mat-card class="algo-container">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>{{ 'GOL.TITLE' | translate }}</mat-card-title>
|
<mat-card-title>{{ 'GOL.TITLE' | translate }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
@@ -33,9 +33,10 @@
|
|||||||
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
|
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-size">
|
<div class="input-container">
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="input-field">
|
||||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
(ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
(ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="input-field">
|
||||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
(ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
(ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="input-field">
|
||||||
<mat-label>{{ 'GOL.SPEED' | translate }}</mat-label>
|
<mat-label>{{ 'GOL.SPEED' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
(keyup.enter)="applySpeed()"
|
(keyup.enter)="applySpeed()"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<span><span class="legend-color alive"></span> {{ 'GOL.ALIVE' | translate }}</span>
|
<span><span class="legend-color alive"></span> {{ 'GOL.ALIVE' | translate }}</span>
|
||||||
@@ -84,7 +86,8 @@
|
|||||||
[createNodeFn]="createConwayNode"
|
[createNodeFn]="createConwayNode"
|
||||||
[getNodeColorFn]="getConwayNodeColor"
|
[getNodeColorFn]="getConwayNodeColor"
|
||||||
[applySelectionFn]="applyConwaySelection"
|
[applySelectionFn]="applyConwaySelection"
|
||||||
(gridChange)="grid = $event"
|
[backgroundColor]="'lightgray'"
|
||||||
|
(gridChange)="readGrid = $event"
|
||||||
></app-generic-grid>
|
></app-generic-grid>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@@ -29,9 +29,9 @@ import {GenericGridComponent, GridPos} from '../../../shared/components/generic-
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
GenericGridComponent
|
GenericGridComponent
|
||||||
],
|
],
|
||||||
templateUrl: './conway-gol.html',
|
templateUrl: './conway-gol.component.html',
|
||||||
})
|
})
|
||||||
export class ConwayGol implements AfterViewInit {
|
export class ConwayGolComponent implements AfterViewInit {
|
||||||
|
|
||||||
algoInformation: AlgorithmInformation = {
|
algoInformation: AlgorithmInformation = {
|
||||||
title: 'GOL.EXPLANATION.TITLE',
|
title: 'GOL.EXPLANATION.TITLE',
|
||||||
@@ -54,7 +54,9 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||||
protected readonly MAX_GRID_PX = MAX_GRID_PX;
|
protected readonly MAX_GRID_PX = MAX_GRID_PX;
|
||||||
|
|
||||||
grid: Node[][] = [];
|
readGrid: Node[][] = [];
|
||||||
|
writeGrid: Node[][] = [];
|
||||||
|
executionTime = 0;
|
||||||
currentScenario: Scenario = Scenario.SIMPLE;
|
currentScenario: Scenario = Scenario.SIMPLE;
|
||||||
readonly gameStarted = signal(false);
|
readonly gameStarted = signal(false);
|
||||||
|
|
||||||
@@ -96,45 +98,42 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getConwayNodeColor = (node: Node): string => {
|
getConwayNodeColor = (node: Node): string => {
|
||||||
if (node.alive) {
|
return node.alive ? 'black' : 'lightgray';
|
||||||
return 'black';
|
|
||||||
}
|
|
||||||
return 'lightgray';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
applyConwaySelection = (pos: GridPos, grid: Node[][]): void => {
|
applyConwaySelection = (pos: GridPos, grid: Node[][]): void => {
|
||||||
this.grid = grid; // Keep internal grid in sync
|
this.readGrid = grid; // Keep internal grid in sync
|
||||||
const node = grid[pos.row][pos.col];
|
const node = grid[pos.row][pos.col];
|
||||||
node.alive = !node.alive; // Toggle alive status
|
node.alive = !node.alive; // Toggle alive status
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeConwayGrid = (grid: Node[][]): void => {
|
initializeConwayGrid = (grid: Node[][]): void => {
|
||||||
this.gameStarted.set(false);
|
this.gameStarted.set(false);
|
||||||
this.grid = grid;
|
this.readGrid = grid;
|
||||||
|
|
||||||
switch(this.currentScenario) {
|
switch(this.currentScenario) {
|
||||||
case Scenario.RANDOM: this.setupRandomLives(); break;
|
case Scenario.RANDOM: this.setupRandomLives(); break;
|
||||||
case Scenario.SIMPLE: this.setupSimpleLive(); break;
|
case Scenario.SIMPLE: this.setupSimpleLive(); break;
|
||||||
case Scenario.PULSAR: this.setupPulsar(); break;
|
case Scenario.PULSAR: this.setupPulsar(); break;
|
||||||
case Scenario.GUN: this.setupGliderGun(); break;
|
case Scenario.GUN: this.setupGliderGun(); break;
|
||||||
}
|
}
|
||||||
|
this.writeGrid = structuredClone(this.readGrid);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Conway-specific logic (kept local) ---
|
// --- Conway-specific logic (kept local) ---
|
||||||
setupRandomLives(): void {
|
setupRandomLives(): void {
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
this.grid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY;
|
this.readGrid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSimpleLive(): void {
|
setupSimpleLive(): void {
|
||||||
this.grid[3][4].alive = true;
|
this.readGrid[3][4].alive = true;
|
||||||
this.grid[4][5].alive = true;
|
this.readGrid[4][5].alive = true;
|
||||||
this.grid[5][3].alive = true;
|
this.readGrid[5][3].alive = true;
|
||||||
this.grid[5][4].alive = true;
|
this.readGrid[5][4].alive = true;
|
||||||
this.grid[5][5].alive = true;
|
this.readGrid[5][5].alive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPulsar(): void {
|
setupPulsar(): void {
|
||||||
@@ -181,39 +180,44 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
this.gameStarted.set(true);
|
this.gameStarted.set(true);
|
||||||
let lifeIsDead = false;
|
let lifeIsDead = false;
|
||||||
while (this.gameStarted()){
|
while (this.gameStarted()){
|
||||||
let gridClone = structuredClone(this.grid);
|
const startTime = performance.now();
|
||||||
lifeIsDead = true;
|
lifeIsDead = true;
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
lifeIsDead = this.checkLifeRules(row, col, gridClone, lifeIsDead);
|
lifeIsDead = this.checkLifeRules(row, col, this.writeGrid) && lifeIsDead;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.swapGrid(gridClone);
|
this.swapGrids();
|
||||||
|
const endTime = performance.now();
|
||||||
|
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4));
|
||||||
if (lifeIsDead){
|
if (lifeIsDead){
|
||||||
this.gameStarted.set(false);
|
this.gameStarted.set(false);
|
||||||
}
|
}
|
||||||
await this.delay(this.lifeSpeed);
|
const delta = Math.max(this.lifeSpeed - this.executionTime, 0);
|
||||||
|
await this.delay(delta);
|
||||||
}
|
}
|
||||||
|
this.executionTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkLifeRules(row: number, col: number, gridClone: Node[][], lifeIsDead: boolean) {
|
private checkLifeRules(row: number, col: number, writeGrid: Node[][]): boolean {
|
||||||
const itsMe = this.grid[row][col];
|
const currentCell = this.readGrid[row][col];
|
||||||
let aliveNeighbors = this.howManyNeighborsAreLiving(row, col);
|
const aliveNeighbors = this.howManyNeighborsAreLiving(row, col);
|
||||||
if (itsMe.alive && (aliveNeighbors < 2 || aliveNeighbors > 3)) {
|
const oldLifeState = currentCell.alive;
|
||||||
gridClone[row][col].alive = false;
|
|
||||||
lifeIsDead = false;
|
const nextStateAlive = (currentCell.alive && (aliveNeighbors === 2 || aliveNeighbors === 3)) || (!currentCell.alive && aliveNeighbors === 3);
|
||||||
} else if (!itsMe.alive && aliveNeighbors === 3) {
|
writeGrid[row][col].alive = nextStateAlive;
|
||||||
gridClone[row][col].alive = true;
|
|
||||||
lifeIsDead = false;
|
//only if at least one cell changes the game is still alive
|
||||||
}
|
return (nextStateAlive == oldLifeState);
|
||||||
return lifeIsDead;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private swapGrid(gridClone: Node[][]) {
|
private swapGrids() {
|
||||||
this.grid = gridClone;
|
const tmp = this.readGrid;
|
||||||
|
this.readGrid = this.writeGrid;
|
||||||
|
this.writeGrid = tmp;
|
||||||
if (this.genericGridComponent) {
|
if (this.genericGridComponent) {
|
||||||
this.genericGridComponent.grid = this.grid;
|
this.genericGridComponent.grid = this.readGrid;
|
||||||
this.genericGridComponent.drawGrid();
|
this.genericGridComponent.drawGrid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +235,7 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
if (nRow == row && nCol == col) {
|
if (nRow == row && nCol == col) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (this.grid[nRow][nCol].alive) {
|
if (this.readGrid[nRow][nCol].alive) {
|
||||||
aliveNeighborCount++;
|
aliveNeighborCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +254,7 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
|
|
||||||
private setAlive(r: number, c: number): void {
|
private setAlive(r: number, c: number): void {
|
||||||
if (r >= 0 && r < this.gridRows && c >= 0 && c < this.gridCols) {
|
if (r >= 0 && r < this.gridRows && c >= 0 && c < this.gridCols) {
|
||||||
this.grid[r][c].alive = true;
|
this.readGrid[r][c].alive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ export const DEFAULT_GRID_ROWS = 50;
|
|||||||
export const DEFAULT_GRID_COLS = 50;
|
export const DEFAULT_GRID_COLS = 50;
|
||||||
|
|
||||||
export const MIN_GRID_SIZE = 20;
|
export const MIN_GRID_SIZE = 20;
|
||||||
export const MAX_GRID_SIZE = 100;
|
export const MAX_GRID_SIZE = 200;
|
||||||
export const DEFAULT_TIME_PER_GENERATION = 30;
|
export const DEFAULT_TIME_PER_GENERATION = 30;
|
||||||
|
|
||||||
export const MIN_TIME_PER_GENERATION = 20;
|
export const MIN_TIME_PER_GENERATION = 20;
|
||||||
|
|||||||
45
src/app/pages/algorithms/fractal/fractal.component.html
Normal file
45
src/app/pages/algorithms/fractal/fractal.component.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<mat-card class="algo-container">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{ 'FRACTAL.TITLE' | translate }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<app-information [algorithmInformation]="algoInformation"/>
|
||||||
|
<div class="controls-container">
|
||||||
|
<div class="controls-panel">
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>{{ 'FRACTAL.ALGORITHM' | translate }}</mat-label>
|
||||||
|
<mat-select [value]="'Mandelbrot'" (selectionChange)="onAlgorithmChange($event.value)">
|
||||||
|
<mat-option value="Mandelbrot">Mandelbrot</mat-option>
|
||||||
|
<mat-option value="Julia">Julia</mat-option>
|
||||||
|
<mat-option value="Burning Ship">Burning Ship</mat-option>
|
||||||
|
<mat-option value="Newton">Newton</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>{{ 'FRACTAL.COLOR_SCHEME' | translate }}</mat-label>
|
||||||
|
<mat-select [value]="'Blue-Gold'" (selectionChange)="onColorChanged($event.value)">
|
||||||
|
<mat-option value="Blue-Gold">Blue-Gold</mat-option>
|
||||||
|
<mat-option value="Greyscale">Greyscale</mat-option>
|
||||||
|
<mat-option value="Fire">Fire</mat-option>
|
||||||
|
<mat-option value="Rainbow">Rainbow</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-raised-button color="primary" (click)="onReset()">
|
||||||
|
<mat-icon>undo</mat-icon> {{ 'FRACTAL.RESET' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="zoom-controls" style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<mat-icon>zoom_out</mat-icon>
|
||||||
|
<ngx-slider [(value)]="sliderValue" [options]="options" (valueChange)="onSliderChange($event)" ></ngx-slider>
|
||||||
|
<mat-icon>zoom_in</mat-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-babylon-canvas
|
||||||
|
[config]="renderConfig"
|
||||||
|
[renderCallback]="onRender"
|
||||||
|
(sceneReady)="onSceneReady($event)"
|
||||||
|
/>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
265
src/app/pages/algorithms/fractal/fractal.component.ts
Normal file
265
src/app/pages/algorithms/fractal/fractal.component.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import {Component, OnInit, signal} from '@angular/core';
|
||||||
|
import {Information} from '../information/information';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {TranslatePipe} from '@ngx-translate/core';
|
||||||
|
import {MatFormField, MatLabel} from '@angular/material/input';
|
||||||
|
import {MatOption} from '@angular/material/core';
|
||||||
|
import {MatSelect} from '@angular/material/select';
|
||||||
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||||
|
import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader';
|
||||||
|
import {PointerEventTypes, PointerInfo, ShaderMaterial, Vector2} from '@babylonjs/core';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-fractal',
|
||||||
|
imports: [
|
||||||
|
Information,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
TranslatePipe,
|
||||||
|
MatFormField,
|
||||||
|
MatLabel,
|
||||||
|
MatOption,
|
||||||
|
MatSelect,
|
||||||
|
FormsModule,
|
||||||
|
BabylonCanvas,
|
||||||
|
MatButton,
|
||||||
|
MatIcon,
|
||||||
|
NgxSliderModule
|
||||||
|
],
|
||||||
|
templateUrl: './fractal.component.html',
|
||||||
|
styleUrl: './fractal.component.scss',
|
||||||
|
})
|
||||||
|
export class FractalComponent implements OnInit {
|
||||||
|
algoInformation: AlgorithmInformation = {
|
||||||
|
title: 'FRACTAL.EXPLANATION.TITLE',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
name: 'Mandelbrot',
|
||||||
|
description: 'FRACTAL.EXPLANATION.MANDELBROT_EXPLANATION',
|
||||||
|
link: UrlConstants.MANDELBROT_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Julia',
|
||||||
|
description: 'FRACTAL.EXPLANATION.JULIA_EXPLANATION',
|
||||||
|
link: UrlConstants.JULIA_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Burning Ship',
|
||||||
|
description: 'FRACTAL.EXPLANATION.BURNING_SHIP_EXPLANATION',
|
||||||
|
link: UrlConstants.BURNING_SHIP_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Newton',
|
||||||
|
description: 'FRACTAL.EXPLANATION.NEWTON_EXPLANATION',
|
||||||
|
link: UrlConstants.NEWTON_FRACTAL_WIKI
|
||||||
|
}
|
||||||
|
],
|
||||||
|
disclaimer: 'FRACTAL.EXPLANATION.DISCLAIMER',
|
||||||
|
disclaimerBottom: 'FRACTAL.EXPLANATION.DISCLAIMER_BOTTOM',
|
||||||
|
disclaimerListEntry: [
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_1',
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_2',
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_3',
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_4'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
renderConfig: RenderConfig = {
|
||||||
|
mode: '2D',
|
||||||
|
initialViewSize: 100,
|
||||||
|
vertexShader: FRACTAL2D_VERTEX,
|
||||||
|
fragmentShader: FRACTAL2D_FRAGMENT,
|
||||||
|
uniformNames: ["worldViewProjection", "time", "targetPosition","center", "zoom", "maxIterations", "algorithm", "colorScheme", "juliaC"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
readonly minZoomValue = 0.2;
|
||||||
|
readonly maxZoomValue = 64000;
|
||||||
|
private isDragging = false;
|
||||||
|
private dragStartPoint: { x: number, y: number } | null = null;
|
||||||
|
|
||||||
|
selectedAlgorithm = 0;
|
||||||
|
selectedColorScheme = 0;
|
||||||
|
|
||||||
|
sliderValue = signal(0);
|
||||||
|
options: Options = {
|
||||||
|
floor: this.minZoomValue,
|
||||||
|
ceil: this.maxZoomValue,
|
||||||
|
logScale: true,
|
||||||
|
step: 0.01,
|
||||||
|
showTicks: false,
|
||||||
|
hideLimitLabels: true,
|
||||||
|
hidePointerLabels: true
|
||||||
|
};
|
||||||
|
zoom = 0;
|
||||||
|
offsetX = 0;
|
||||||
|
offsetY = 0;
|
||||||
|
maxIterations = 0;
|
||||||
|
|
||||||
|
juliaReal = -0.7;
|
||||||
|
juliaImag = 0.27015;
|
||||||
|
|
||||||
|
// --- Render Callback ---
|
||||||
|
onRender: RenderCallback = (material: ShaderMaterial) => {
|
||||||
|
material.setVector2("center", new Vector2(this.offsetX, this.offsetY));
|
||||||
|
material.setFloat("zoom", this.zoom);
|
||||||
|
material.setInt("maxIterations", this.maxIterations);
|
||||||
|
material.setInt("algorithm", this.selectedAlgorithm);
|
||||||
|
material.setInt("colorScheme", this.selectedColorScheme);
|
||||||
|
material.setVector2("juliaC", new Vector2(this.juliaReal, this.juliaImag));
|
||||||
|
};
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.onReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAlgorithmChange(algoName: string): void {
|
||||||
|
switch(algoName) {
|
||||||
|
case 'Mandelbrot': this.selectedAlgorithm = 0; break;
|
||||||
|
case 'Julia': this.selectedAlgorithm = 1; break;
|
||||||
|
case 'Burning Ship': this.selectedAlgorithm = 2; break;
|
||||||
|
case 'Newton': this.selectedAlgorithm = 3; break;
|
||||||
|
}
|
||||||
|
this.onReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
onColorChanged(schemeName: string): void {
|
||||||
|
switch(schemeName) {
|
||||||
|
case 'Blue-Gold': this.selectedColorScheme = 0; break;
|
||||||
|
case 'Greyscale': this.selectedColorScheme = 1; break;
|
||||||
|
case 'Fire': this.selectedColorScheme = 2; break;
|
||||||
|
case 'Rainbow': this.selectedColorScheme = 3; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset(): void {
|
||||||
|
|
||||||
|
this.offsetY = 0.0;
|
||||||
|
this.zoom = 0.2
|
||||||
|
this.maxIterations = 100;
|
||||||
|
|
||||||
|
switch(this.selectedAlgorithm) {
|
||||||
|
case 0: this.offsetX = -0.5; break;
|
||||||
|
case 1: this.offsetX = 0; break;
|
||||||
|
case 2: this.offsetX = -1.75; this.zoom = 8; this.offsetY = -0.03;break;
|
||||||
|
case 3: this.offsetX = 0; break;
|
||||||
|
default: this.offsetX = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSceneReady(event: SceneEventData): void {
|
||||||
|
event.scene.onPointerObservable.add((pointerInfo) => {
|
||||||
|
switch (pointerInfo.type) {
|
||||||
|
|
||||||
|
case PointerEventTypes.POINTERDOWN:
|
||||||
|
this.onPointerDown(pointerInfo);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PointerEventTypes.POINTERUP:
|
||||||
|
this.onPointerUp();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PointerEventTypes.POINTERMOVE:
|
||||||
|
this.onPointerMove(pointerInfo);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PointerEventTypes.POINTERWHEEL:
|
||||||
|
this.onPointerWheel(pointerInfo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPointerDown(info: PointerInfo): void {
|
||||||
|
if (info.event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isDragging = true;
|
||||||
|
this.dragStartPoint = { x: info.event.clientX, y: info.event.clientY };
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPointerUp(): void {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.dragStartPoint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPointerMove(info: PointerInfo): void {
|
||||||
|
if (!this.isDragging || !this.dragStartPoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = info.event as PointerEvent;
|
||||||
|
|
||||||
|
const deltaX = event.clientX - this.dragStartPoint.x;
|
||||||
|
const deltaY = event.clientY - this.dragStartPoint.y;
|
||||||
|
|
||||||
|
const element = event.target as HTMLElement;
|
||||||
|
const height = element.clientHeight;
|
||||||
|
|
||||||
|
const scaleFactor = 1 / (height * this.zoom);
|
||||||
|
|
||||||
|
this.offsetX += deltaX * scaleFactor;
|
||||||
|
this.offsetY += deltaY * scaleFactor;
|
||||||
|
|
||||||
|
this.dragStartPoint = { x: event.clientX, y: event.clientY };
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPointerWheel(info: PointerInfo): void {
|
||||||
|
const event = info.event as WheelEvent;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const element = event.target as HTMLElement;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
const mouseXPixels = -(event.clientX - rect.left - rect.width / 2);
|
||||||
|
const mouseYPixels = -(event.clientY - rect.top - rect.height / 2);
|
||||||
|
|
||||||
|
const mouseXView = mouseXPixels / rect.height;
|
||||||
|
const mouseYView = mouseYPixels / rect.height;
|
||||||
|
|
||||||
|
const mouseXWorld = mouseXView / this.zoom + this.offsetX;
|
||||||
|
const mouseYWorld = mouseYView / this.zoom + this.offsetY;
|
||||||
|
|
||||||
|
const zoomFactor = 1.1;
|
||||||
|
if (event.deltaY < 0) {
|
||||||
|
this.zoom *= zoomFactor;
|
||||||
|
} else {
|
||||||
|
this.zoom /= zoomFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zoom = Math.max(Math.min(this.zoom, this.maxZoomValue), this.minZoomValue);
|
||||||
|
this.sliderValue.set(this.zoom);
|
||||||
|
const optimalIterations = this.getIterationsForZoom(this.zoom);
|
||||||
|
this.maxIterations = Math.min(optimalIterations, 3000);
|
||||||
|
|
||||||
|
this.offsetX = mouseXWorld - mouseXView / this.zoom;
|
||||||
|
this.offsetY = mouseYWorld - mouseYView / this.zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSliderChange(newValue: number): void {
|
||||||
|
|
||||||
|
this.zoom = newValue;
|
||||||
|
this.zoom = Math.max(Math.min(this.zoom, this.maxZoomValue), this.minZoomValue);
|
||||||
|
this.maxIterations = this.getIterationsForZoom(this.zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIterationsForZoom(zoom: number): number {
|
||||||
|
const baseIterations = 100;
|
||||||
|
const factor = 200;
|
||||||
|
|
||||||
|
if (zoom <= 1) {
|
||||||
|
return baseIterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(baseIterations + Math.log10(zoom) * factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/app/pages/algorithms/fractal/fractal.shader.ts
Normal file
155
src/app/pages/algorithms/fractal/fractal.shader.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
export const FRACTAL2D_VERTEX = `
|
||||||
|
precision highp float;
|
||||||
|
attribute vec3 position;
|
||||||
|
attribute vec2 uv;
|
||||||
|
uniform mat4 worldViewProjection;
|
||||||
|
varying vec2 vUV;
|
||||||
|
void main() {
|
||||||
|
gl_Position = worldViewProjection * vec4(position, 1.0);
|
||||||
|
vUV = uv;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FRACTAL2D_FRAGMENT = `
|
||||||
|
precision highp float;
|
||||||
|
varying vec2 vUV;
|
||||||
|
|
||||||
|
uniform vec2 resolution;
|
||||||
|
uniform vec2 center; // OffsetX, OffsetY
|
||||||
|
uniform float zoom;
|
||||||
|
uniform int maxIterations;
|
||||||
|
uniform int algorithm; // 0:Mandel, 1:Julia, 2:Ship, 3:Newton
|
||||||
|
uniform int colorScheme; // 0:BlueGold, 1:Greyscale, 2:Fire, 3:Rainbow
|
||||||
|
uniform vec2 juliaC;
|
||||||
|
|
||||||
|
vec3 hsv2rgb(vec3 c) {
|
||||||
|
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||||
|
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||||
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Color Schemes ---
|
||||||
|
vec3 getColor(float t, int iter, int maxIter, int root) {
|
||||||
|
if (iter >= maxIter) return vec3(0.0);
|
||||||
|
|
||||||
|
// special newton coloring
|
||||||
|
if (algorithm == 3) {
|
||||||
|
float val = 1.0 - (float(iter) / 20.0); // Weicheres Shading für Newton
|
||||||
|
val = clamp(val, 0.0, 1.0);
|
||||||
|
if (root == 1) return vec3(val, 0.0, 0.0); // Rot
|
||||||
|
if (root == 2) return vec3(0.0, val, 0.0); // Grün
|
||||||
|
if (root == 3) return vec3(0.0, 0.0, val); // Blau
|
||||||
|
return vec3(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// default color
|
||||||
|
if (colorScheme == 0) { // Blue-Gold
|
||||||
|
return vec3(
|
||||||
|
9.0 * (1.0-t)*t*t*t,
|
||||||
|
15.0 * (1.0-t)*(1.0-t)*t*t,
|
||||||
|
8.5 * (1.0-t)*(1.0-t)*(1.0-t)*t
|
||||||
|
) * 3.0;
|
||||||
|
}
|
||||||
|
if (colorScheme == 1) { // Greyscale
|
||||||
|
return vec3(t);
|
||||||
|
}
|
||||||
|
if (colorScheme == 2) { // Fire
|
||||||
|
float f = sqrt(t);
|
||||||
|
return vec3(f * 2.0, (f - 0.3) * 3.0, (f - 0.6) * 6.0);
|
||||||
|
}
|
||||||
|
if (colorScheme == 3) { // Rainbow
|
||||||
|
return hsv2rgb(vec3(t * 5.0, 1.0, 1.0));
|
||||||
|
}
|
||||||
|
return vec3(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
float aspect = resolution.x / resolution.y;
|
||||||
|
vec2 uv = (vUV - 0.5) * vec2(aspect, 1.0);
|
||||||
|
vec2 c = uv / zoom + center;
|
||||||
|
|
||||||
|
vec2 z = c;
|
||||||
|
// For Julia is c fix, z changes. For Mandel is z=0, c changes.
|
||||||
|
if (algorithm == 1) {
|
||||||
|
z = c;
|
||||||
|
c = juliaC;
|
||||||
|
} else if (algorithm != 3) {
|
||||||
|
z = vec2(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int iter = 0;
|
||||||
|
int root = 0;
|
||||||
|
|
||||||
|
// --- Algorithms ---
|
||||||
|
if (algorithm == 3) { // Newton: z^3 - 1
|
||||||
|
z = c;
|
||||||
|
for(int i=0; i<100; i++) {
|
||||||
|
if (i >= maxIterations) break;
|
||||||
|
|
||||||
|
// z^3 - 1
|
||||||
|
// z_new = z - (z^3 - 1) / (3*z^2)
|
||||||
|
// simplified: z_new = (2*z^3 + 1) / (3*z^2)
|
||||||
|
|
||||||
|
float zx2 = z.x * z.x;
|
||||||
|
float zy2 = z.y * z.y;
|
||||||
|
float denom = 3.0 * (zx2 + zy2) * (zx2 + zy2); // |3z^2|^2 simplified
|
||||||
|
|
||||||
|
// z -= (z^3-1)/(3z^2)
|
||||||
|
|
||||||
|
vec2 z2 = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y);
|
||||||
|
vec2 z3 = vec2(z2.x*z.x - z2.y*z.y, z2.x*z.y + z2.y*z.x);
|
||||||
|
|
||||||
|
vec2 num = z3 - vec2(1.0, 0.0);
|
||||||
|
vec2 den = 3.0 * z2;
|
||||||
|
|
||||||
|
// Division Complex: (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c^2+d^2)
|
||||||
|
float d = den.x*den.x + den.y*den.y;
|
||||||
|
if(d < 0.000001) { iter=maxIterations; break; }
|
||||||
|
|
||||||
|
vec2 div = vec2(
|
||||||
|
(num.x*den.x + num.y*den.y)/d,
|
||||||
|
(num.y*den.x - num.x*den.y)/d
|
||||||
|
);
|
||||||
|
|
||||||
|
z -= div;
|
||||||
|
iter++;
|
||||||
|
|
||||||
|
// Roots check
|
||||||
|
// 1. (1, 0)
|
||||||
|
if (distance(z, vec2(1.0, 0.0)) < 0.001) { root = 1; break; }
|
||||||
|
// 2. (-0.5, 0.866)
|
||||||
|
if (distance(z, vec2(-0.5, 0.866)) < 0.001) { root = 2; break; }
|
||||||
|
// 3. (-0.5, -0.866)
|
||||||
|
if (distance(z, vec2(-0.5, -0.866)) < 0.001) { root = 3; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // Mandelbrot (0), Julia (1), Burning Ship (2)
|
||||||
|
for(int i=0; i<2000; i++) {
|
||||||
|
if (i >= maxIterations) break;
|
||||||
|
|
||||||
|
float x2 = z.x * z.x;
|
||||||
|
float y2 = z.y * z.y;
|
||||||
|
|
||||||
|
if (x2 + y2 > 4.0) {
|
||||||
|
iter = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
iter = i;
|
||||||
|
|
||||||
|
if (algorithm == 2) { // Burning Ship
|
||||||
|
z.y = abs(z.y);
|
||||||
|
z.x = abs(z.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// z = z^2 + c
|
||||||
|
float nextX = x2 - y2 + c.x;
|
||||||
|
z.y = 2.0 * z.x * z.y + c.y;
|
||||||
|
z.x = nextX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float t = float(iter) / float(maxIterations);
|
||||||
|
vec3 color = getColor(t, iter, maxIterations, root);
|
||||||
|
gl_FragColor = vec4(color, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
19
src/app/pages/algorithms/fractal3d/fractal3d.component.html
Normal file
19
src/app/pages/algorithms/fractal3d/fractal3d.component.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<mat-card class="algo-container">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{ 'FRACTAL3D.TITLE' | translate }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<app-information [algorithmInformation]="algoInformation"/>
|
||||||
|
<div class="controls-container">
|
||||||
|
<div class="controls-panel">
|
||||||
|
<button matButton="filled" (click)="onFractalTypeChange(0)">{{ 'FRACTAL3D.MANDELBULB' | translate }}</button>
|
||||||
|
<button matButton="filled" (click)="onFractalTypeChange(1)">{{ 'FRACTAL3D.MANDELBOX' | translate }}</button>
|
||||||
|
<button matButton="filled" (click)="onFractalTypeChange(2)">{{ 'FRACTAL3D.JULIA' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-babylon-canvas
|
||||||
|
[config]="fractalConfig"
|
||||||
|
[renderCallback]="onRender"
|
||||||
|
/>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
85
src/app/pages/algorithms/fractal3d/fractal3d.component.ts
Normal file
85
src/app/pages/algorithms/fractal3d/fractal3d.component.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core';
|
||||||
|
import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader';
|
||||||
|
import {Information} from '../information/information';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {TranslatePipe} from '@ngx-translate/core';
|
||||||
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-fractal3d',
|
||||||
|
imports: [
|
||||||
|
Information,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
TranslatePipe,
|
||||||
|
MatButton,
|
||||||
|
BabylonCanvas
|
||||||
|
],
|
||||||
|
templateUrl: './fractal3d.component.html',
|
||||||
|
styleUrl: './fractal3d.component.scss',
|
||||||
|
})
|
||||||
|
export class Fractal3dComponent {
|
||||||
|
|
||||||
|
algoInformation: AlgorithmInformation = {
|
||||||
|
title: 'FRACTAL3D.EXPLANATION.TITLE',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
name: 'Mandel-Bulb',
|
||||||
|
description: 'FRACTAL3D.EXPLANATION.MANDELBULB_EXPLANATION',
|
||||||
|
link: UrlConstants.MANDELBULB_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mandelbox',
|
||||||
|
description: 'FRACTAL3D.EXPLANATION.MANDELBOX_EXPLANATION',
|
||||||
|
link: UrlConstants.MANDELBOX_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Julia-Bulb',
|
||||||
|
description: 'FRACTAL3D.EXPLANATION.JULIA_EXPLANATION',
|
||||||
|
link: UrlConstants.JULIA3D_WIKI
|
||||||
|
}
|
||||||
|
],
|
||||||
|
disclaimer: 'FRACTAL3D.EXPLANATION.DISCLAIMER',
|
||||||
|
disclaimerBottom: '',
|
||||||
|
disclaimerListEntry: ['FRACTAL3D.EXPLANATION.DISCLAIMER_1', 'FRACTAL3D.EXPLANATION.DISCLAIMER_2', 'FRACTAL3D.EXPLANATION.DISCLAIMER_3', 'FRACTAL3D.EXPLANATION.DISCLAIMER_4']
|
||||||
|
};
|
||||||
|
|
||||||
|
fractalConfig: RenderConfig = {
|
||||||
|
mode: '3D',
|
||||||
|
initialViewSize: 4,
|
||||||
|
vertexShader: MANDELBULB_VERTEX,
|
||||||
|
fragmentShader: MANDELBULB_FRAGMENT,
|
||||||
|
uniformNames: ["time", "power", "fractalType"]
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly fractalPower = 8;
|
||||||
|
private time = 0;
|
||||||
|
private oldType = 0;
|
||||||
|
public currentFractalType = 0;
|
||||||
|
|
||||||
|
|
||||||
|
onRender: RenderCallback = (material: ShaderMaterial, camera: Camera) => {
|
||||||
|
this.time += 0.005;
|
||||||
|
|
||||||
|
if (this.oldType != this.currentFractalType && camera instanceof ArcRotateCamera) {
|
||||||
|
this.oldType = this.currentFractalType;
|
||||||
|
camera.radius = this.currentFractalType == 1 ? 15 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
material.setFloat("time", this.time);
|
||||||
|
material.setFloat("power", this.fractalPower);
|
||||||
|
material.setInt("fractalType", this.currentFractalType);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onFractalTypeChange(type: number): void {
|
||||||
|
this.currentFractalType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
220
src/app/pages/algorithms/fractal3d/fractal3d.shader.ts
Normal file
220
src/app/pages/algorithms/fractal3d/fractal3d.shader.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
export const MANDELBULB_VERTEX = /* glsl */`
|
||||||
|
precision highp float;
|
||||||
|
attribute vec3 position;
|
||||||
|
attribute vec2 uv;
|
||||||
|
varying vec2 vUV;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
gl_Position = vec4(position, 1.0);
|
||||||
|
vUV = uv;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MANDELBULB_FRAGMENT = /* glsl */`
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform float time;
|
||||||
|
uniform vec2 resolution;
|
||||||
|
uniform vec3 cameraPosition;
|
||||||
|
uniform vec3 targetPosition;
|
||||||
|
uniform float power;
|
||||||
|
uniform int fractalType; // 0 = Bulb, 1 = Box, 2 = Julia
|
||||||
|
|
||||||
|
// --- Palettes ---
|
||||||
|
vec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) {
|
||||||
|
return a + b*cos( 6.28318*(c*t+d) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global trap for coloring
|
||||||
|
float minTrap = 1000.0;
|
||||||
|
|
||||||
|
// --- Shape 1: Mandelbulb ---
|
||||||
|
float mapMandelbulb(vec3 pos, out float trap) {
|
||||||
|
vec3 z = pos;
|
||||||
|
float dr = 1.0;
|
||||||
|
float r = 0.0;
|
||||||
|
trap = 1000.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
r = length(z);
|
||||||
|
if (r > 100.0) break;
|
||||||
|
trap = min(trap, r);
|
||||||
|
|
||||||
|
float theta = acos(z.y / r);
|
||||||
|
float phi = atan(z.z, z.x);
|
||||||
|
dr = pow(r, power - 1.0) * power * dr + 1.0;
|
||||||
|
float zr = pow(r, power);
|
||||||
|
theta = theta * power;
|
||||||
|
phi = phi * power;
|
||||||
|
z = zr * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
|
||||||
|
z += pos;
|
||||||
|
}
|
||||||
|
return 0.5 * log(r) * r / dr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Shape 2: Mandelbox ---
|
||||||
|
float mapMandelbox(vec3 pos, out float trap) {
|
||||||
|
vec3 z = pos;
|
||||||
|
float dr = 1.0;
|
||||||
|
float scale = 2.8; // Fixed scale for good look
|
||||||
|
trap = 1000.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 15; i++) {
|
||||||
|
// Box fold
|
||||||
|
z = clamp(z, -1.0, 1.0) * 2.0 - z;
|
||||||
|
|
||||||
|
// Sphere fold
|
||||||
|
float r2 = dot(z, z);
|
||||||
|
trap = min(trap, r2); // Trap based on sphere fold
|
||||||
|
|
||||||
|
if (r2 < 0.25) {
|
||||||
|
z = z * 4.0;
|
||||||
|
dr = dr * 4.0;
|
||||||
|
} else if (r2 < 1.0) {
|
||||||
|
z = z / r2;
|
||||||
|
dr = dr / r2;
|
||||||
|
}
|
||||||
|
|
||||||
|
z = z * scale + pos;
|
||||||
|
dr = dr * abs(scale) + 1.0;
|
||||||
|
}
|
||||||
|
return (length(z) - abs(scale - 1.0)) / dr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Shape 3: Julia Bulb ---
|
||||||
|
float mapJulia(vec3 pos, out float trap) {
|
||||||
|
vec3 z = pos;
|
||||||
|
float dr = 1.0;
|
||||||
|
float r = 0.0;
|
||||||
|
trap = 1000.0;
|
||||||
|
|
||||||
|
// Constant C for Julia set (animating slightly makes it alive)
|
||||||
|
vec3 c = vec3(0.35, 0.45, -0.1) + vec3(sin(time*0.1)*0.2);
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
r = length(z);
|
||||||
|
if (r > 100.0) break; // Higher escape radius for Julia
|
||||||
|
trap = min(trap, r);
|
||||||
|
|
||||||
|
float theta = acos(z.y / r);
|
||||||
|
float phi = atan(z.z, z.x);
|
||||||
|
dr = pow(r, power - 1.0) * power * dr + 1.0;
|
||||||
|
float zr = pow(r, power);
|
||||||
|
theta = theta * power;
|
||||||
|
phi = phi * power;
|
||||||
|
z = zr * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
|
||||||
|
z += c; // Add C instead of pos
|
||||||
|
}
|
||||||
|
return 0.5 * log(r) * r / dr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Map Dispatcher ---
|
||||||
|
float map(vec3 pos) {
|
||||||
|
float d = 0.0;
|
||||||
|
float currentTrap = 0.0;
|
||||||
|
|
||||||
|
if (fractalType == 1) {
|
||||||
|
d = mapMandelbox(pos, currentTrap);
|
||||||
|
} else if (fractalType == 2) {
|
||||||
|
d = mapJulia(pos, currentTrap);
|
||||||
|
} else {
|
||||||
|
d = mapMandelbulb(pos, currentTrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
minTrap = currentTrap; // Update global
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Raymarching ---
|
||||||
|
bool intersectSphere(vec3 ro, vec3 rd, vec3 c, float r, out float t0, out float t1) {
|
||||||
|
vec3 oc = ro - c;
|
||||||
|
float b = dot(oc, rd);
|
||||||
|
float c2 = dot(oc, oc) - r * r;
|
||||||
|
float h = b*b - c2;
|
||||||
|
if (h < 0.0) return false;
|
||||||
|
h = sqrt(h);
|
||||||
|
t0 = -b - h;
|
||||||
|
t1 = -b + h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float raymarch(vec3 ro, vec3 rd) {
|
||||||
|
// Bounding sphere around fractal center (here: origin)
|
||||||
|
vec3 center = vec3(0.0);
|
||||||
|
float radius = 6.0;
|
||||||
|
|
||||||
|
float tEnter, tExit;
|
||||||
|
if (!intersectSphere(ro, rd, center, radius, tEnter, tExit)) {
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float t = max(tEnter, 0.0);
|
||||||
|
float tMax = tExit;
|
||||||
|
|
||||||
|
for (int i = 0; i < 128; i++) {
|
||||||
|
vec3 pos = ro + t * rd;
|
||||||
|
float d = map(pos);
|
||||||
|
|
||||||
|
// distance-based epsilon is more stable for zoom-out
|
||||||
|
float eps = max(0.001, 0.0005 * t);
|
||||||
|
|
||||||
|
if (d < eps) return t;
|
||||||
|
t += d * 0.8; // safety factor against overshoot
|
||||||
|
if (t > tMax) break;
|
||||||
|
}
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 getNormal(vec3 p) {
|
||||||
|
float d = map(p);
|
||||||
|
vec2 e = vec2(0.001, 0.0);
|
||||||
|
return normalize(vec3(
|
||||||
|
d - map(p - e.xyy),
|
||||||
|
d - map(p - e.yxy),
|
||||||
|
d - map(p - e.yyx)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
vec2 uv = (gl_FragCoord.xy - 0.5 * resolution.xy) / resolution.y;
|
||||||
|
vec3 ro = cameraPosition;
|
||||||
|
vec3 ta = targetPosition;
|
||||||
|
|
||||||
|
vec3 fwd = normalize(ta - ro);
|
||||||
|
vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), fwd));
|
||||||
|
vec3 up = normalize(cross(fwd, right));
|
||||||
|
vec3 rd = normalize(fwd + uv.x * right + uv.y * up);
|
||||||
|
|
||||||
|
vec3 color = vec3(0.1);
|
||||||
|
|
||||||
|
float t = raymarch(ro, rd);
|
||||||
|
|
||||||
|
if(t > 0.0) {
|
||||||
|
vec3 pos = ro + t * rd;
|
||||||
|
vec3 nor = getNormal(pos);
|
||||||
|
|
||||||
|
// Different colors for different shapes
|
||||||
|
vec3 colParamsA = vec3(0.5, 0.5, 0.5);
|
||||||
|
vec3 colParamsB = vec3(0.5, 0.5, 0.5);
|
||||||
|
vec3 colParamsC = vec3(1.0, 1.0, 1.0);
|
||||||
|
vec3 colParamsD = vec3(0.80, 0.90, 0.30);
|
||||||
|
|
||||||
|
if (fractalType == 1) { // Box: Sci-Fi Blue/Grey
|
||||||
|
colParamsD = vec3(0.0, 0.1, 0.2);
|
||||||
|
}
|
||||||
|
if (fractalType == 2) { // Julia: Alien Green/Purple
|
||||||
|
colParamsD = vec3(0.8, 0.0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 materialColor = palette(minTrap, colParamsA, colParamsB, colParamsC, colParamsD);
|
||||||
|
|
||||||
|
float camLight = max(0.0, dot(nor, -rd));
|
||||||
|
float ambient = 0.4;
|
||||||
|
vec3 lighting = vec3(1.0) * (camLight * 0.7 + ambient);
|
||||||
|
color = materialColor * lighting;
|
||||||
|
}
|
||||||
|
|
||||||
|
color = pow(color, vec3(0.8));
|
||||||
|
gl_FragColor = vec4(color, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -5,7 +5,14 @@
|
|||||||
@for (algo of algorithmInformation.entries; track algo)
|
@for (algo of algorithmInformation.entries; track algo)
|
||||||
{
|
{
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ algo.name }}</strong> {{ algo.description | translate }}
|
<strong>
|
||||||
|
@if(algo.translateName){
|
||||||
|
{{ algo.name | translate}}
|
||||||
|
} @else {
|
||||||
|
{{ algo.name }}
|
||||||
|
}
|
||||||
|
</strong>
|
||||||
|
{{ algo.description | translate }}
|
||||||
<a href="{{algo.link}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
<a href="{{algo.link}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ export interface AlgorithmEntry {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
translateName?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<mat-card class="algo-container">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{ 'LABYRINTH.TITLE' | translate }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<app-information [algorithmInformation]="algoInformation"/>
|
||||||
|
<div class="controls-container">
|
||||||
|
<div class="controls-panel">
|
||||||
|
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
||||||
|
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="controls-panel">
|
||||||
|
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="createRandom(true)">{{ 'LABYRINTH.PRIM' | translate }}</button>
|
||||||
|
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="createRandom(false)">{{ 'LABYRINTH.KRUSKAL' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<span><span class="legend-color start"></span> {{ 'PATHFINDING.START_NODE' | translate }}</span>
|
||||||
|
<span><span class="legend-color end"></span> {{ 'PATHFINDING.END_NODE' | translate }}</span>
|
||||||
|
<span><span class="legend-color wall"></span> {{ 'PATHFINDING.WALL' | translate }}</span>
|
||||||
|
<span><span class="legend-color visited"></span> {{ 'PATHFINDING.VISITED' | translate }}</span>
|
||||||
|
<span><span class="legend-color path"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls-panel">
|
||||||
|
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
|
||||||
|
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-generic-grid
|
||||||
|
[gridRows]="gridRows"
|
||||||
|
[gridCols]="gridCols"
|
||||||
|
[minGridSize]="MIN_GRID_SIZE"
|
||||||
|
[maxGridSize]="MAX_GRID_SIZE"
|
||||||
|
[maxGridPx]="MAX_GRID_PX"
|
||||||
|
[createNodeFn]="createMazeNode"
|
||||||
|
[getNodeColorFn]="getMazeColor"
|
||||||
|
[applySelectionFn]="applyNoSelection"
|
||||||
|
[backgroundColor]="'lightgray'"
|
||||||
|
(gridChange)="grid = $event"
|
||||||
|
></app-generic-grid>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core';
|
||||||
|
import {Information} from '../../information/information';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {TranslatePipe} from '@ngx-translate/core';
|
||||||
|
import {GenericGridComponent, GridPos} from '../../../../shared/components/generic-grid/generic-grid';
|
||||||
|
import {AlgorithmInformation} from '../../information/information.models';
|
||||||
|
import {UrlConstants} from '../../../../constants/UrlConstants';
|
||||||
|
import {Node} from '../pathfinding.models';
|
||||||
|
import {SharedFunctions} from '../../../../shared/SharedFunctions';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {DecimalPipe} from '@angular/common';
|
||||||
|
import {PathfindingService} from '../service/pathfinding.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-labyrinth',
|
||||||
|
imports: [
|
||||||
|
Information,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
TranslatePipe,
|
||||||
|
GenericGridComponent,
|
||||||
|
MatButton,
|
||||||
|
DecimalPipe
|
||||||
|
],
|
||||||
|
templateUrl: './labyrinth.component.html',
|
||||||
|
styleUrl: './labyrinth.component.scss',
|
||||||
|
})
|
||||||
|
export class LabyrinthComponent implements AfterViewInit {
|
||||||
|
|
||||||
|
protected readonly gridRows = 101;
|
||||||
|
protected readonly gridCols = 101;
|
||||||
|
protected readonly MAX_GRID_SIZE = 101;
|
||||||
|
protected readonly MAX_GRID_PX = 1000;
|
||||||
|
protected readonly MIN_GRID_SIZE = 101;
|
||||||
|
private readonly pathfindingService = inject(PathfindingService);
|
||||||
|
|
||||||
|
|
||||||
|
algoInformation: AlgorithmInformation = {
|
||||||
|
title: 'LABYRINTH.EXPLANATION.TITLE',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
name: 'Prim’s',
|
||||||
|
description: 'LABYRINTH.EXPLANATION.PRIM_EXPLANATION',
|
||||||
|
link: UrlConstants.PRIMS_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Kruskal’s',
|
||||||
|
description: 'LABYRINTH.EXPLANATION.KRUSKAL_EXPLANATION',
|
||||||
|
link: UrlConstants.KRUSKAL_WIKI
|
||||||
|
}
|
||||||
|
],
|
||||||
|
disclaimer: 'LABYRINTH.EXPLANATION.DISCLAIMER',
|
||||||
|
disclaimerBottom: '',
|
||||||
|
disclaimerListEntry: ['LABYRINTH.EXPLANATION.DISCLAIMER_1', 'LABYRINTH.EXPLANATION.DISCLAIMER_2', 'LABYRINTH.EXPLANATION.DISCLAIMER_3', 'LABYRINTH.EXPLANATION.DISCLAIMER_4']
|
||||||
|
};
|
||||||
|
|
||||||
|
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
|
||||||
|
|
||||||
|
grid: Node[][] = [];
|
||||||
|
startNode: Node | null = null;
|
||||||
|
endNode: Node | null = null;
|
||||||
|
animationSpeed = 3;
|
||||||
|
mazeAnimationSpeed = 1;
|
||||||
|
pathLength = "0";
|
||||||
|
executionTime = 0;
|
||||||
|
|
||||||
|
private timeoutIds: number[] = [];
|
||||||
|
protected mazeNodesInOrder: Node[] = [];
|
||||||
|
readonly isAnimationRunning = signal(false);
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
if (this.genericGridComponent) {
|
||||||
|
this.genericGridComponent.initializationFn = this.initializeMazeGrid;
|
||||||
|
this.genericGridComponent.createNodeFn = this.createMazeNode;
|
||||||
|
this.genericGridComponent.getNodeColorFn = this.getMazeColor;
|
||||||
|
this.genericGridComponent.applySelectionFn = this.applyNoSelection;
|
||||||
|
this.genericGridComponent.gridRows = this.gridRows;
|
||||||
|
this.genericGridComponent.gridCols = this.gridCols;
|
||||||
|
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
|
||||||
|
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
|
||||||
|
this.genericGridComponent.maxGridPx = 1000;
|
||||||
|
this.genericGridComponent.applyGridSize();
|
||||||
|
this.genericGridComponent.initializeGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeMazeGrid = (grid: Node[][]): void => {
|
||||||
|
this.grid = grid;
|
||||||
|
this.createRandom(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
createRandom(prim: boolean): void {
|
||||||
|
this.isAnimationRunning.set(true);
|
||||||
|
this.stopAnimations();
|
||||||
|
this.clearPath();
|
||||||
|
this.startNode = null;
|
||||||
|
this.endNode = null;
|
||||||
|
if (prim)
|
||||||
|
{
|
||||||
|
this.createPrimMaze();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.createKruskalMaze();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupGrid();
|
||||||
|
this.genericGridComponent.drawGrid();
|
||||||
|
}
|
||||||
|
// ------- Kuskal -------
|
||||||
|
private createKruskalMaze(): void {
|
||||||
|
this.initKuskal();
|
||||||
|
this.mazeNodesInOrder = [];
|
||||||
|
const walls = this.findWallsWithADistanceOfTwoRooms();
|
||||||
|
SharedFunctions.shuffleArray(walls);
|
||||||
|
for (const wallInfo of walls) {
|
||||||
|
const { row, col, roomA, roomB } = wallInfo;
|
||||||
|
|
||||||
|
if (roomA.nodeData !== roomB.nodeData) {
|
||||||
|
const wallNode = this.grid[row][col];
|
||||||
|
wallNode.isWall = true;
|
||||||
|
this.mazeNodesInOrder.push(wallNode);
|
||||||
|
|
||||||
|
const oldId = roomB.nodeData;
|
||||||
|
const newId = roomA.nodeData;
|
||||||
|
|
||||||
|
this.mergeSets(oldId, newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setRandomStartAndEnd();
|
||||||
|
this.animateMazeGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initKuskal() {
|
||||||
|
let roomId = 0;
|
||||||
|
|
||||||
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
|
const node = this.grid[row][col];
|
||||||
|
node.isStart = false;
|
||||||
|
node.isEnd = false;
|
||||||
|
|
||||||
|
if (row % 2 === 0 && col % 2 === 0) {
|
||||||
|
node.isWall = false;
|
||||||
|
node.nodeData = roomId++;
|
||||||
|
} else {
|
||||||
|
node.isWall = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeSets(oldId: number, newId: number): void {
|
||||||
|
for (let r = 0; r < this.gridRows; r += 2) {
|
||||||
|
for (let c = 0; c < this.gridCols; c += 2) {
|
||||||
|
if (this.grid[r][c].nodeData === oldId) {
|
||||||
|
this.grid[r][c].nodeData = newId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findWallsWithADistanceOfTwoRooms() {
|
||||||
|
const walls: { row: number, col: number, roomA: Node, roomB: Node }[] = [];
|
||||||
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
|
if (row % 2 === 0 && col % 2 !== 0 && col > 0 && col < this.gridCols - 1) {
|
||||||
|
walls.push({
|
||||||
|
row, col,
|
||||||
|
roomA: this.grid[row][col - 1],
|
||||||
|
roomB: this.grid[row][col + 1]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (row % 2 !== 0 && col % 2 === 0 && row > 0 && row < this.gridRows - 1) {
|
||||||
|
walls.push({
|
||||||
|
row, col,
|
||||||
|
roomA: this.grid[row - 1][col],
|
||||||
|
roomB: this.grid[row + 1][col]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return walls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setRandomStartAndEnd(): void {
|
||||||
|
const lastRow = Math.floor((this.gridRows - 1) / 2) * 2;
|
||||||
|
const lastCol = Math.floor((this.gridCols - 1) / 2) * 2;
|
||||||
|
|
||||||
|
const corners = [
|
||||||
|
{ r: 0, c: 0 },
|
||||||
|
{ r: 0, c: lastCol },
|
||||||
|
{ r: lastRow, c: 0 },
|
||||||
|
{ r: lastRow, c: lastCol }
|
||||||
|
];
|
||||||
|
|
||||||
|
const startIndex = Math.floor(Math.random() * corners.length);
|
||||||
|
let endIndex = Math.floor(Math.random() * corners.length);
|
||||||
|
while (endIndex === startIndex) {
|
||||||
|
endIndex = Math.floor(Math.random() * corners.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = corners[startIndex];
|
||||||
|
const end = corners[endIndex];
|
||||||
|
|
||||||
|
this.startNode = this.grid[start.r][start.c];
|
||||||
|
this.startNode.isStart = true;
|
||||||
|
this.startNode.isWall = false;
|
||||||
|
|
||||||
|
this.endNode =this.grid[end.r][end.c];
|
||||||
|
this.endNode.isEnd = true;
|
||||||
|
this.endNode.isWall = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------- PRIM -------
|
||||||
|
private createPrimMaze(): void {
|
||||||
|
this.initPrim();
|
||||||
|
this.mazeNodesInOrder = [];
|
||||||
|
const frontier: Node[] = [];
|
||||||
|
|
||||||
|
const {startRow, startCol, startNode} = this.findStartNode();
|
||||||
|
this.mazeNodesInOrder.push(startNode);
|
||||||
|
|
||||||
|
this.getNeighborWalls(startRow, startCol, frontier);
|
||||||
|
|
||||||
|
while (frontier.length > 0) {
|
||||||
|
const randomIndex = SharedFunctions.randomIntFromInterval(0, frontier.length - 1);
|
||||||
|
const lastIndex = frontier.length - 1;
|
||||||
|
[frontier[randomIndex], frontier[lastIndex]] = [frontier[lastIndex], frontier[randomIndex]];
|
||||||
|
|
||||||
|
const wallNode = frontier.pop()!;
|
||||||
|
const target = wallNode.linkedNode;
|
||||||
|
|
||||||
|
if (!target || target.isVisited) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
wallNode.isVisited = true;
|
||||||
|
target.isVisited = true;
|
||||||
|
|
||||||
|
this.mazeNodesInOrder.push(wallNode, target);
|
||||||
|
|
||||||
|
this.getNeighborWalls(target.row, target.col, frontier);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setRandomStartAndEnd();
|
||||||
|
this.animateMazeGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initPrim() {
|
||||||
|
for (let row = 0; row < this.grid.length; row++) {
|
||||||
|
for (let col = 0; col < this.grid[row].length; col++) {
|
||||||
|
this.grid[row][col].isWall = true;
|
||||||
|
this.grid[row][col].isStart = false;
|
||||||
|
this.grid[row][col].isEnd = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupGrid() {
|
||||||
|
for (let row = 0; row < this.grid.length; row++) {
|
||||||
|
for (let col = 0; col < this.grid[row].length; col++) {
|
||||||
|
this.grid[row][col].isVisited = false;
|
||||||
|
this.grid[row][col].linkedNode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findStartNode() {
|
||||||
|
const startRow: number = SharedFunctions.randomEventIntFromInterval(this.gridRows - 1);
|
||||||
|
const startCol: number = SharedFunctions.randomEventIntFromInterval(this.gridCols - 1);
|
||||||
|
|
||||||
|
const startNode = this.grid[startRow][startCol];
|
||||||
|
startNode.isWall = false;
|
||||||
|
startNode.isVisited = true;
|
||||||
|
return {startRow, startCol, startNode};
|
||||||
|
}
|
||||||
|
|
||||||
|
visualize(algorithm: string): void {
|
||||||
|
this.stopAnimations();
|
||||||
|
this.clearPath();
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (algorithm) {
|
||||||
|
case 'dijkstra': result = this.pathfindingService.dijkstra(
|
||||||
|
this.grid,
|
||||||
|
this.grid[this.startNode!.row][this.startNode!.col],
|
||||||
|
this.grid[this.endNode!.row][this.endNode!.col]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'astar': result = this.pathfindingService.aStar(
|
||||||
|
this.grid,
|
||||||
|
this.grid[this.startNode!.row][this.startNode!.col],
|
||||||
|
this.grid[this.endNode!.row][this.endNode!.col]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = performance.now();
|
||||||
|
const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
|
||||||
|
if (lengthOfShortestPath === 0)
|
||||||
|
{
|
||||||
|
this.pathLength = "∞"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.pathLength = result.nodesInShortestPathOrder.length + "";
|
||||||
|
}
|
||||||
|
this.executionTime = endTime - startTime;
|
||||||
|
|
||||||
|
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
createMazeNode = (row: number, col: number): Node => {
|
||||||
|
return {
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
isStart: false,
|
||||||
|
isEnd: false,
|
||||||
|
isWall: false,
|
||||||
|
isVisited: false,
|
||||||
|
isPath: false,
|
||||||
|
nodeData: Infinity,
|
||||||
|
linkedNode: null,
|
||||||
|
hScore: 0,
|
||||||
|
fScore: Infinity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getMazeColor = (node: Node): string => {
|
||||||
|
if (node.isStart) return 'green';
|
||||||
|
if (node.isEnd) return 'red';
|
||||||
|
if (node.isPath) return 'gold';
|
||||||
|
if (node.isVisited) return 'skyblue';
|
||||||
|
if (node.isWall) return 'black';
|
||||||
|
return 'lightgray';
|
||||||
|
};
|
||||||
|
|
||||||
|
applyNoSelection = (pos: GridPos, grid: Node[][]): void => {
|
||||||
|
this.grid = grid;
|
||||||
|
//dont need a selection for the maze case
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Animation (adapted to use genericGridComponent for redraw) ---
|
||||||
|
private stopAnimations(): void {
|
||||||
|
for (const id of this.timeoutIds) {
|
||||||
|
clearTimeout(id);
|
||||||
|
}
|
||||||
|
this.timeoutIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearPath(): void {
|
||||||
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
|
const node = this.grid[row][col];
|
||||||
|
node.isVisited = false;
|
||||||
|
node.isPath = false;
|
||||||
|
node.nodeData = Infinity;
|
||||||
|
node.linkedNode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component
|
||||||
|
}
|
||||||
|
|
||||||
|
private animateAlgorithm(visited: Node[], path: Node[]): void {
|
||||||
|
for (let i = 0; i <= visited.length; i++) {
|
||||||
|
if (i === visited.length) {
|
||||||
|
const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
|
||||||
|
this.timeoutIds.push(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = visited[i];
|
||||||
|
const id = globalThis.setTimeout(() => {
|
||||||
|
if (!node.isStart && !node.isEnd) {
|
||||||
|
node.isVisited = true;
|
||||||
|
this.genericGridComponent?.drawNode(node);
|
||||||
|
}
|
||||||
|
}, this.animationSpeed * i);
|
||||||
|
|
||||||
|
this.timeoutIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private animateShortestPath(path: Node[]): void {
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
const node = path[i];
|
||||||
|
const id = globalThis.setTimeout(() => {
|
||||||
|
if (!node.isStart && !node.isEnd) {
|
||||||
|
node.isPath = true;
|
||||||
|
this.genericGridComponent?.drawNode(node); // Redraw single node
|
||||||
|
}
|
||||||
|
}, this.animationSpeed * i);
|
||||||
|
|
||||||
|
this.timeoutIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private animateMazeGeneration(): void {
|
||||||
|
for (let i = 0; i < this.mazeNodesInOrder.length; i++) {
|
||||||
|
const id = globalThis.setTimeout(() => {
|
||||||
|
const node = this.mazeNodesInOrder[i];
|
||||||
|
node.isWall = false;
|
||||||
|
this.genericGridComponent?.drawNode(node);
|
||||||
|
if (i === this.mazeNodesInOrder.length - 1) {
|
||||||
|
this.cleanupGrid();
|
||||||
|
if (this.startNode) {
|
||||||
|
this.genericGridComponent?.drawNode(this.startNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.endNode) {
|
||||||
|
this.genericGridComponent?.drawNode(this.endNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i == this.mazeNodesInOrder.length - 1) {
|
||||||
|
this.isAnimationRunning.set(false);
|
||||||
|
}
|
||||||
|
}, this.mazeAnimationSpeed * i);
|
||||||
|
|
||||||
|
this.timeoutIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//utility
|
||||||
|
private getNeighborWalls(row: number, col: number, frontier: Node[]): void{
|
||||||
|
|
||||||
|
const directions = [
|
||||||
|
[0, 2], [0, -2], [2, 0], [-2, 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [dr, dc] of directions) {
|
||||||
|
const nextRow = row + dr;
|
||||||
|
const nextCol = col + dc;
|
||||||
|
|
||||||
|
|
||||||
|
if (this.isValid(nextRow, nextCol) && this.grid[nextRow][nextCol].isWall && !this.grid[nextRow][nextCol].isVisited) {
|
||||||
|
const wallRow = row + dr / 2;
|
||||||
|
const wallCol = col + dc / 2;
|
||||||
|
|
||||||
|
const node = this.grid[wallRow][wallCol];
|
||||||
|
node.linkedNode = this.grid[nextRow][nextCol];
|
||||||
|
frontier.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = (row: number, col: number): boolean => {
|
||||||
|
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,79 +1,80 @@
|
|||||||
<mat-card class="container">
|
<mat-card class="algo-container">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>{{ 'PATHFINDING.TITLE' | translate }}</mat-card-title>
|
<mat-card-title>{{ 'PATHFINDING.TITLE' | translate }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<app-information [algorithmInformation]="algoInformation"/>
|
<app-information [algorithmInformation]="algoInformation"/>
|
||||||
|
|
||||||
<div class="controls-container">
|
<div class="controls-container">
|
||||||
<div class="controls-panel">
|
<div class="controls-panel">
|
||||||
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
||||||
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-panel">
|
<div class="controls-panel">
|
||||||
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'normal'})">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
|
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'normal'})">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
|
||||||
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'random'})">{{ 'PATHFINDING.RANDOM_CASE' | translate }}</button>
|
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'random'})">{{ 'PATHFINDING.RANDOM_CASE' | translate }}</button>
|
||||||
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'edge'})">{{ 'PATHFINDING.EDGE_CASE' | translate }}</button>
|
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'edge'})">{{ 'PATHFINDING.EDGE_CASE' | translate }}</button>
|
||||||
<button matButton="filled" (click)="createCase({withWalls: false, scenario: 'normal'})">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
|
<button matButton="filled" (click)="createCase({withWalls: false, scenario: 'normal'})">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls-panel">
|
<div class="controls-panel">
|
||||||
<mat-button-toggle-group [(ngModel)]="selectedNodeType" aria-label="Node Type Selection">
|
<mat-button-toggle-group [(ngModel)]="selectedNodeType" aria-label="Node Type Selection">
|
||||||
<mat-button-toggle [value]="NodeType.Start">{{ 'PATHFINDING.START_NODE' | translate }}</mat-button-toggle>
|
<mat-button-toggle [value]="NodeType.Start">{{ 'PATHFINDING.START_NODE' | translate }}</mat-button-toggle>
|
||||||
<mat-button-toggle [value]="NodeType.End">{{ 'PATHFINDING.END_NODE' | translate }}</mat-button-toggle>
|
<mat-button-toggle [value]="NodeType.End">{{ 'PATHFINDING.END_NODE' | translate }}</mat-button-toggle>
|
||||||
<mat-button-toggle [value]="NodeType.Wall">{{ 'PATHFINDING.WALL' | translate }}</mat-button-toggle>
|
<mat-button-toggle [value]="NodeType.Wall">{{ 'PATHFINDING.WALL' | translate }}</mat-button-toggle>
|
||||||
<mat-button-toggle [value]="NodeType.None">{{ 'PATHFINDING.CLEAR_NODE' | translate }}</mat-button-toggle>
|
<mat-button-toggle [value]="NodeType.None">{{ 'PATHFINDING.CLEAR_NODE' | translate }}</mat-button-toggle>
|
||||||
</mat-button-toggle-group>
|
</mat-button-toggle-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-panel">
|
<div class="controls-panel">
|
||||||
<div class="grid-size">
|
<div class="input-container">
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="input-field">
|
||||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_GRID_SIZE"
|
[max]="MAX_GRID_SIZE"
|
||||||
[(ngModel)]="gridRows"
|
[(ngModel)]="gridRows"
|
||||||
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||||
/> </mat-form-field>
|
/> </mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="input-field">
|
||||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_GRID_SIZE"
|
[max]="MAX_GRID_SIZE"
|
||||||
[(ngModel)]="gridCols"
|
[(ngModel)]="gridCols"
|
||||||
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||||
/> </mat-form-field>
|
/> </mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<span><span class="legend-color start"></span> {{ 'PATHFINDING.START_NODE' | translate }}</span>
|
||||||
|
<span><span class="legend-color end"></span> {{ 'PATHFINDING.END_NODE' | translate }}</span>
|
||||||
|
<span><span class="legend-color wall"></span> {{ 'PATHFINDING.WALL' | translate }}</span>
|
||||||
|
<span><span class="legend-color visited"></span> {{ 'PATHFINDING.VISITED' | translate }}</span>
|
||||||
|
<span><span class="legend-color path"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="controls-panel">
|
||||||
|
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
|
||||||
|
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legend">
|
<app-generic-grid
|
||||||
<span><span class="legend-color start"></span> {{ 'PATHFINDING.START_NODE' | translate }}</span>
|
[gridRows]="gridRows"
|
||||||
<span><span class="legend-color end"></span> {{ 'PATHFINDING.END_NODE' | translate }}</span>
|
[gridCols]="gridCols"
|
||||||
<span><span class="legend-color wall"></span> {{ 'PATHFINDING.WALL' | translate }}</span>
|
[minGridSize]="MIN_GRID_SIZE"
|
||||||
<span><span class="legend-color visited"></span> {{ 'PATHFINDING.VISITED' | translate }}</span>
|
[maxGridSize]="MAX_GRID_SIZE"
|
||||||
<span><span class="legend-color path"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
|
[maxGridPx]="MAX_GRID_PX"
|
||||||
</div>
|
[createNodeFn]="createPathfindingNode"
|
||||||
<div class="controls-panel">
|
[getNodeColorFn]="getPathfindingNodeColor"
|
||||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
|
[applySelectionFn]="applyPathfindingSelection"
|
||||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
|
[backgroundColor]="'lightgray'"
|
||||||
</div>
|
(gridChange)="grid = $event"
|
||||||
</div>
|
></app-generic-grid>
|
||||||
|
</mat-card-content>
|
||||||
<app-generic-grid
|
</mat-card>
|
||||||
[gridRows]="gridRows"
|
|
||||||
[gridCols]="gridCols"
|
|
||||||
[minGridSize]="MIN_GRID_SIZE"
|
|
||||||
[maxGridSize]="MAX_GRID_SIZE"
|
|
||||||
[maxGridPx]="MAX_GRID_PX"
|
|
||||||
[createNodeFn]="createPathfindingNode"
|
|
||||||
[getNodeColorFn]="getPathfindingNodeColor"
|
|
||||||
[applySelectionFn]="applyPathfindingSelection"
|
|
||||||
(gridChange)="grid = $event"
|
|
||||||
></app-generic-grid>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/mat
|
|||||||
import {Information} from '../information/information';
|
import {Information} from '../information/information';
|
||||||
import {AlgorithmInformation} from '../information/information.models';
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||||
|
import {SharedFunctions} from '../../../shared/SharedFunctions';
|
||||||
|
|
||||||
enum NodeType {
|
enum NodeType {
|
||||||
Start = 'start',
|
Start = 'start',
|
||||||
@@ -118,8 +119,8 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
isWall: false,
|
isWall: false,
|
||||||
isVisited: false,
|
isVisited: false,
|
||||||
isPath: false,
|
isPath: false,
|
||||||
distance: Infinity,
|
nodeData: Infinity,
|
||||||
previousNode: null,
|
linkedNode: null,
|
||||||
hScore: 0,
|
hScore: 0,
|
||||||
fScore: Infinity,
|
fScore: Infinity,
|
||||||
};
|
};
|
||||||
@@ -326,16 +327,16 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
private createRandomStartEndPosition(): { start: GridPos; end: GridPos } {
|
private createRandomStartEndPosition(): { start: GridPos; end: GridPos } {
|
||||||
const midCol = Math.floor(this.gridCols / 2);
|
const midCol = Math.floor(this.gridCols / 2);
|
||||||
|
|
||||||
const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
const startRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||||
const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1);
|
const startCol: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||||
|
|
||||||
const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
const endRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||||
let endCol: number;
|
let endCol: number;
|
||||||
|
|
||||||
if (startCol <= midCol) {
|
if (startCol <= midCol) {
|
||||||
endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1);
|
endCol = SharedFunctions.randomIntFromInterval(midCol + 1, this.gridCols - 1);
|
||||||
} else {
|
} else {
|
||||||
endCol = this.randomIntFromInterval(0, midCol);
|
endCol = SharedFunctions.randomIntFromInterval(0, midCol);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -359,8 +360,8 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
|
|
||||||
for (let wall = 0; wall < maxNumberOfWalls; wall++) {
|
for (let wall = 0; wall < maxNumberOfWalls; wall++) {
|
||||||
|
|
||||||
const row: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
const row: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||||
const col: number = this.randomIntFromInterval(0, this.gridCols - 1);
|
const col: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||||
|
|
||||||
if (!this.grid[row][col]) { // Use the grid passed from GenericGrid
|
if (!this.grid[row][col]) { // Use the grid passed from GenericGrid
|
||||||
wall--;
|
wall--;
|
||||||
@@ -435,8 +436,8 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
const node = this.grid[row][col];
|
const node = this.grid[row][col];
|
||||||
node.isVisited = false;
|
node.isVisited = false;
|
||||||
node.isPath = false;
|
node.isPath = false;
|
||||||
node.distance = Infinity;
|
node.nodeData = Infinity;
|
||||||
node.previousNode = null;
|
node.linkedNode = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component
|
this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component
|
||||||
@@ -486,8 +487,4 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Utility ---
|
|
||||||
private randomIntFromInterval(min: number, max: number): number {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export interface Node {
|
|||||||
isWall: boolean;
|
isWall: boolean;
|
||||||
isVisited: boolean;
|
isVisited: boolean;
|
||||||
isPath: boolean;
|
isPath: boolean;
|
||||||
distance: number;
|
nodeData: number; //can be used as distance or id or something
|
||||||
previousNode: Node | null;
|
linkedNode: Node | null;
|
||||||
fScore: number;
|
fScore: number;
|
||||||
hScore: number;
|
hScore: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class PathfindingService {
|
|||||||
let currentNode: Node | null = endNode;
|
let currentNode: Node | null = endNode;
|
||||||
while (currentNode !== null) {
|
while (currentNode !== null) {
|
||||||
shortestPathNodes.unshift(currentNode);
|
shortestPathNodes.unshift(currentNode);
|
||||||
currentNode = currentNode.previousNode;
|
currentNode = currentNode.linkedNode;
|
||||||
}
|
}
|
||||||
return shortestPathNodes;
|
return shortestPathNodes;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ export class PathfindingService {
|
|||||||
// Dijkstra's Algorithm
|
// Dijkstra's Algorithm
|
||||||
dijkstra(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
|
dijkstra(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
|
||||||
const visitedNodesInOrder: Node[] = [];
|
const visitedNodesInOrder: Node[] = [];
|
||||||
startNode.distance = 0;
|
startNode.nodeData = 0;
|
||||||
const unvisitedNodes: Node[] = this.getAllNodes(grid);
|
const unvisitedNodes: Node[] = this.getAllNodes(grid);
|
||||||
|
|
||||||
while (unvisitedNodes.length > 0) {
|
while (unvisitedNodes.length > 0) {
|
||||||
@@ -44,7 +44,7 @@ export class PathfindingService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTrapped = closestNode.distance === Infinity;
|
const isTrapped = closestNode.nodeData === Infinity;
|
||||||
if (isTrapped)
|
if (isTrapped)
|
||||||
{
|
{
|
||||||
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
|
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
|
||||||
@@ -65,24 +65,24 @@ export class PathfindingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sortNodesByDistance(unvisitedNodes: Node[]): void {
|
private sortNodesByDistance(unvisitedNodes: Node[]): void {
|
||||||
unvisitedNodes.sort((nodeA, nodeB) => nodeA.distance - nodeB.distance);
|
unvisitedNodes.sort((nodeA, nodeB) => nodeA.nodeData - nodeB.nodeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateUnvisitedNeighbors(node: Node, grid: Node[][]): void {
|
private updateUnvisitedNeighbors(node: Node, grid: Node[][]): void {
|
||||||
const unvisitedNeighbors = this.getUnvisitedNeighbors(node, grid);
|
const unvisitedNeighbors = this.getUnvisitedNeighbors(node, grid);
|
||||||
for (const neighbor of unvisitedNeighbors) {
|
for (const neighbor of unvisitedNeighbors) {
|
||||||
neighbor.distance = node.distance + 1;
|
neighbor.nodeData = node.nodeData + 1;
|
||||||
neighbor.previousNode = node;
|
neighbor.linkedNode = node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A* Search Algorithm
|
// A* Search Algorithm
|
||||||
aStar(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
|
aStar(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
|
||||||
const visitedNodesInOrder: Node[] = [];
|
const visitedNodesInOrder: Node[] = [];
|
||||||
startNode.distance = 0;
|
startNode.nodeData = 0;
|
||||||
startNode['hScore'] = this.calculateHeuristic(startNode, endNode);
|
startNode['hScore'] = this.calculateHeuristic(startNode, endNode);
|
||||||
// fScore = gScore + hScore
|
// fScore = gScore + hScore
|
||||||
startNode['fScore'] = startNode.distance + startNode['hScore'];
|
startNode['fScore'] = startNode.nodeData + startNode['hScore'];
|
||||||
|
|
||||||
const openSet: Node[] = [startNode];
|
const openSet: Node[] = [startNode];
|
||||||
const allNodes = this.getAllNodes(grid);
|
const allNodes = this.getAllNodes(grid);
|
||||||
@@ -97,7 +97,7 @@ export class PathfindingService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTrapped = currentNode.distance === Infinity;
|
const isTrapped = currentNode.nodeData === Infinity;
|
||||||
if (isTrapped)
|
if (isTrapped)
|
||||||
{
|
{
|
||||||
return {visitedNodesInOrder, nodesInShortestPathOrder: []};
|
return {visitedNodesInOrder, nodesInShortestPathOrder: []};
|
||||||
@@ -114,9 +114,9 @@ export class PathfindingService {
|
|||||||
|
|
||||||
const neighbors = this.getUnvisitedNeighbors(currentNode, grid);
|
const neighbors = this.getUnvisitedNeighbors(currentNode, grid);
|
||||||
for (const neighbor of neighbors) {
|
for (const neighbor of neighbors) {
|
||||||
const tentativeGScore = currentNode.distance + 1; // Distance from start to neighbor
|
const tentativeGScore = currentNode.nodeData + 1; // Distance from start to neighbor
|
||||||
|
|
||||||
if (tentativeGScore < neighbor.distance) {
|
if (tentativeGScore < neighbor.nodeData) {
|
||||||
this.updateNeighborNode(neighbor, currentNode, tentativeGScore, endNode, openSet);
|
this.updateNeighborNode(neighbor, currentNode, tentativeGScore, endNode, openSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,11 +136,11 @@ export class PathfindingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateNeighborNode(neighbor: Node, currentNode: Node, tentativeGScore: number, endNode: Node, openSet: Node[]) {
|
private updateNeighborNode(neighbor: Node, currentNode: Node, tentativeGScore: number, endNode: Node, openSet: Node[]) {
|
||||||
neighbor.previousNode = currentNode;
|
neighbor.linkedNode = currentNode;
|
||||||
neighbor.distance = tentativeGScore;
|
neighbor.nodeData = tentativeGScore;
|
||||||
neighbor['distance'] = this.calculateHeuristic(neighbor, endNode);
|
neighbor['nodeData'] = this.calculateHeuristic(neighbor, endNode);
|
||||||
neighbor['hScore'] = this.calculateHeuristic(neighbor, endNode);
|
neighbor['hScore'] = this.calculateHeuristic(neighbor, endNode);
|
||||||
neighbor['fScore'] = neighbor.distance + neighbor['hScore'];
|
neighbor['fScore'] = neighbor.nodeData + neighbor['hScore'];
|
||||||
|
|
||||||
if (!openSet.includes(neighbor)) {
|
if (!openSet.includes(neighbor)) {
|
||||||
openSet.push(neighbor);
|
openSet.push(neighbor);
|
||||||
@@ -151,7 +151,7 @@ export class PathfindingService {
|
|||||||
for (const node of allNodes) {
|
for (const node of allNodes) {
|
||||||
if (node !== startNode) {
|
if (node !== startNode) {
|
||||||
node['fScore'] = Infinity;
|
node['fScore'] = Infinity;
|
||||||
node.distance = Infinity; // gScore
|
node.nodeData = Infinity; // gScore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/app/pages/algorithms/pendulum/pendulum.component.html
Normal file
67
src/app/pages/algorithms/pendulum/pendulum.component.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<mat-card class="algo-container">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{ 'PENDULUM.TITLE' | translate }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<app-information [algorithmInformation]="algoInformation"/>
|
||||||
|
<div class="controls-container">
|
||||||
|
<div class="sliders-grid">
|
||||||
|
<div class="slider-item">
|
||||||
|
<p>{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}</p>
|
||||||
|
<ngx-slider [(value)]="simParams.trailDecay" [options]="trailDecayOptions" ></ngx-slider>
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<p>{{ 'PENDULUM.ATTRACTION' | translate }}</p>
|
||||||
|
<ngx-slider [(value)]="simParams.g" [options]="gravityOptions" ></ngx-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="slider-item">
|
||||||
|
<p>{{ 'PENDULUM.L1_LENGTH' | translate }}</p>
|
||||||
|
<ngx-slider [(value)]="simParams.l1" [options]="lengthOptions" ></ngx-slider>
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<p>{{ 'PENDULUM.L2_LENGTH' | translate }}</p>
|
||||||
|
<ngx-slider [(value)]="simParams.l2" [options]="lengthOptions" ></ngx-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="slider-item">
|
||||||
|
<p>{{ 'PENDULUM.M1_MASS' | translate }}</p>
|
||||||
|
<ngx-slider [(value)]="simParams.m1" [options]="massOptions" ></ngx-slider>
|
||||||
|
</div>
|
||||||
|
<div class="slider-item">
|
||||||
|
<p>{{ 'PENDULUM.M2_MASS' | translate }}</p>
|
||||||
|
<ngx-slider [(value)]="simParams.m2" [options]="massOptions" ></ngx-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="slider-item full-width">
|
||||||
|
<p>{{ 'PENDULUM.DAMPING' | translate }}</p>
|
||||||
|
<ngx-slider [(value)]="simParams.damping" [options]="dampingOptions" ></ngx-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-container">
|
||||||
|
<button mat-raised-button color="primary" (click)="pushPendulum(true)">
|
||||||
|
{{ 'PENDULUM.POKE_M1' | translate }}
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="primary" (click)="pushPendulum(false)">
|
||||||
|
{{ 'PENDULUM.POKE_M2' | translate }}
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="primary" (click)="resetPendulum()">
|
||||||
|
{{ 'PENDULUM.RESET' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend" style="margin-top: 10px">
|
||||||
|
<span><span class="legend-color L1"></span> L1</span>
|
||||||
|
<span><span class="legend-color L2"></span> L2</span>
|
||||||
|
<span><span class="legend-color M1"></span> M1</span>
|
||||||
|
<span><span class="legend-color M2"></span> M2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-babylon-canvas
|
||||||
|
[config]="renderConfig"
|
||||||
|
(sceneReady)="onSceneReady($event)"
|
||||||
|
(sceneResized)="onSceneReady($event)"
|
||||||
|
/>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
41
src/app/pages/algorithms/pendulum/pendulum.component.scss
Normal file
41
src/app/pages/algorithms/pendulum/pendulum.component.scss
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.sliders-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
.slider-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
|
||||||
|
p {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngx-slider {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.sliders-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
240
src/app/pages/algorithms/pendulum/pendulum.component.ts
Normal file
240
src/app/pages/algorithms/pendulum/pendulum.component.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {ComputeShader, ShaderLanguage, StorageBuffer} from '@babylonjs/core';
|
||||||
|
import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PENDULUM_RENDER_COMPUTE_SHADER_WGSL, PENDULUM_VERTEX_SHADER_WGSL} from './pendulum.shader';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||||
|
import {DEFAULT_DAMPING, DEFAULT_G, DEFAULT_L1_LENGTH, DEFAULT_M1_MASS, DEFAULT_L2_LENGTH, DEFAULT_M2_MASS, DEFAULT_TRAIL_DECAY, MAX_DAMPING, MAX_G, MAX_LENGTH, MAX_MASS, MAX_TRAIL_DECAY, MIN_DAMPING, MIN_G, MIN_LENGTH, MIN_MASS, MIN_TRAIL_DECAY, IMPULSE_M2, IMPULSE_M1} from './pendulum.model';
|
||||||
|
import {TranslatePipe} from '@ngx-translate/core';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {Information} from '../information/information';
|
||||||
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-pendulum',
|
||||||
|
imports: [
|
||||||
|
BabylonCanvas,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
FormsModule,
|
||||||
|
NgxSliderModule,
|
||||||
|
TranslatePipe,
|
||||||
|
MatButton,
|
||||||
|
Information,
|
||||||
|
],
|
||||||
|
templateUrl: './pendulum.component.html',
|
||||||
|
styleUrl: './pendulum.component.scss',
|
||||||
|
})
|
||||||
|
class PendulumComponent {
|
||||||
|
|
||||||
|
// --- CONFIGURATION ---
|
||||||
|
algoInformation: AlgorithmInformation = {
|
||||||
|
title: 'PENDULUM.EXPLANATION.TITLE',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
description: 'PENDULUM.EXPLANATION.EXPLANATION',
|
||||||
|
link: UrlConstants.DOUBLE_PENDULUM_WIKI
|
||||||
|
}
|
||||||
|
],
|
||||||
|
disclaimer: 'PENDULUM.EXPLANATION.DISCLAIMER',
|
||||||
|
disclaimerBottom: 'PENDULUM.EXPLANATION.DISCLAIMER_BOTTOM',
|
||||||
|
disclaimerListEntry: ['PENDULUM.EXPLANATION.DISCLAIMER_1', 'PENDULUM.EXPLANATION.DISCLAIMER_2', 'PENDULUM.EXPLANATION.DISCLAIMER_3', 'PENDULUM.EXPLANATION.DISCLAIMER_4']
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
renderConfig: RenderConfig = {
|
||||||
|
mode: '2D',
|
||||||
|
initialViewSize: 2,
|
||||||
|
shaderLanguage: ShaderLanguage.WGSL,
|
||||||
|
vertexShader: PENDULUM_VERTEX_SHADER_WGSL,
|
||||||
|
fragmentShader: PENDULUM_FRAGMENT_SHADER_WGSL,
|
||||||
|
uniformNames: [],
|
||||||
|
uniformBufferNames: []
|
||||||
|
};
|
||||||
|
|
||||||
|
trailDecayOptions: Options = {
|
||||||
|
floor: MIN_TRAIL_DECAY,
|
||||||
|
ceil: MAX_TRAIL_DECAY,
|
||||||
|
logScale: false,
|
||||||
|
step: 0.001,
|
||||||
|
showTicks: false,
|
||||||
|
hideLimitLabels: false,
|
||||||
|
hidePointerLabels: false
|
||||||
|
};
|
||||||
|
|
||||||
|
gravityOptions: Options = {
|
||||||
|
floor: MIN_G,
|
||||||
|
ceil: MAX_G,
|
||||||
|
logScale: false,
|
||||||
|
step: 0.01,
|
||||||
|
showTicks: false,
|
||||||
|
hideLimitLabels: false,
|
||||||
|
hidePointerLabels: false
|
||||||
|
};
|
||||||
|
|
||||||
|
dampingOptions: Options = {
|
||||||
|
floor: MAX_DAMPING,
|
||||||
|
ceil: MIN_DAMPING,
|
||||||
|
logScale: false,
|
||||||
|
step: 0.001,
|
||||||
|
showTicks: false,
|
||||||
|
hideLimitLabels: false,
|
||||||
|
hidePointerLabels: false
|
||||||
|
};
|
||||||
|
|
||||||
|
lengthOptions: Options = {
|
||||||
|
floor: MIN_LENGTH,
|
||||||
|
ceil: MAX_LENGTH,
|
||||||
|
logScale: false,
|
||||||
|
step: 0.1,
|
||||||
|
showTicks: false,
|
||||||
|
hideLimitLabels: false,
|
||||||
|
hidePointerLabels: false
|
||||||
|
};
|
||||||
|
|
||||||
|
massOptions: Options = {
|
||||||
|
floor: MIN_MASS,
|
||||||
|
ceil: MAX_MASS,
|
||||||
|
logScale: false,
|
||||||
|
step: 0.1,
|
||||||
|
showTicks: false,
|
||||||
|
hideLimitLabels: false,
|
||||||
|
hidePointerLabels: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Central management of physics parameters
|
||||||
|
readonly simParams = {
|
||||||
|
time: 0,
|
||||||
|
dt: 0.015,
|
||||||
|
g: DEFAULT_G,
|
||||||
|
m1: DEFAULT_M1_MASS,
|
||||||
|
m2: DEFAULT_M2_MASS,
|
||||||
|
l1: DEFAULT_L1_LENGTH,
|
||||||
|
l2: DEFAULT_L2_LENGTH,
|
||||||
|
damping: DEFAULT_DAMPING,
|
||||||
|
trailDecay: DEFAULT_TRAIL_DECAY,
|
||||||
|
impulseM1: 0.0,
|
||||||
|
impulseM2: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
private currentSceneData: SceneEventData | null = null;
|
||||||
|
|
||||||
|
onSceneReady(event: SceneEventData) {
|
||||||
|
this.currentSceneData = event;
|
||||||
|
this.createSimulation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSimulation() {
|
||||||
|
if (!this.currentSceneData){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {engine, scene} = this.currentSceneData;
|
||||||
|
engine.resize();
|
||||||
|
|
||||||
|
const width = engine.getRenderWidth();
|
||||||
|
const height = engine.getRenderHeight();
|
||||||
|
const totalPixels = width * height;
|
||||||
|
|
||||||
|
// --- 1. BUFFERS ---
|
||||||
|
const pixelBuffer = new StorageBuffer(engine, totalPixels * 4);
|
||||||
|
|
||||||
|
const stateBuffer = new StorageBuffer(engine, 4 * 4);
|
||||||
|
stateBuffer.update(new Float32Array([Math.PI / 4, Math.PI / 2, 0, 0])); // Initial angles
|
||||||
|
|
||||||
|
const paramsBuffer = new StorageBuffer(engine, 14 * 4);
|
||||||
|
const paramsData = new Float32Array(14);
|
||||||
|
|
||||||
|
// --- 2. SHADERS ---
|
||||||
|
const csPhysics = new ComputeShader("physics", engine,
|
||||||
|
{computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL},
|
||||||
|
{bindingsMapping: {"state": {group: 0, binding: 0}, "p": {group: 0, binding: 1}}}
|
||||||
|
);
|
||||||
|
csPhysics.setStorageBuffer("state", stateBuffer);
|
||||||
|
csPhysics.setStorageBuffer("p", paramsBuffer);
|
||||||
|
|
||||||
|
const csRender = new ComputeShader("render", engine,
|
||||||
|
{computeSource: PENDULUM_RENDER_COMPUTE_SHADER_WGSL},
|
||||||
|
{bindingsMapping: {"pixelBuffer": {group: 0, binding: 0}, "p": {group: 0, binding: 1}, "state": {group: 0, binding: 2}}}
|
||||||
|
);
|
||||||
|
csRender.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||||
|
csRender.setStorageBuffer("p", paramsBuffer);
|
||||||
|
csRender.setStorageBuffer("state", stateBuffer);
|
||||||
|
|
||||||
|
// --- 3. MATERIAL ---
|
||||||
|
const plane = scene.getMeshByName("plane");
|
||||||
|
if (plane?.material) {
|
||||||
|
const mat = plane.material as any;
|
||||||
|
mat.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||||
|
mat.setStorageBuffer("p", paramsBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
//remove old observables if available
|
||||||
|
scene.onBeforeRenderObservable.clear();
|
||||||
|
// --- 4. RENDER LOOP ---
|
||||||
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
|
this.simParams.time += this.simParams.dt;
|
||||||
|
|
||||||
|
const currentWidth = engine.getRenderWidth();
|
||||||
|
const currentHeight = engine.getRenderHeight();
|
||||||
|
|
||||||
|
// Fill parameter array (must match the exact order of the WGSL struct!)
|
||||||
|
paramsData[0] = currentWidth;
|
||||||
|
paramsData[1] = currentHeight;
|
||||||
|
paramsData[2] = this.simParams.time;
|
||||||
|
paramsData[3] = this.simParams.dt;
|
||||||
|
paramsData[4] = this.simParams.g;
|
||||||
|
paramsData[5] = this.simParams.m1;
|
||||||
|
paramsData[6] = this.simParams.m2;
|
||||||
|
paramsData[7] = this.simParams.l1;
|
||||||
|
paramsData[8] = this.simParams.l2;
|
||||||
|
paramsData[9] = this.simParams.damping;
|
||||||
|
paramsData[10] = this.simParams.trailDecay;
|
||||||
|
paramsData[11] = this.simParams.impulseM1;
|
||||||
|
paramsData[12] = this.simParams.impulseM2;
|
||||||
|
paramsData[13] = 0; // Pad
|
||||||
|
|
||||||
|
this.resetImpulses();
|
||||||
|
|
||||||
|
paramsBuffer.update(paramsData);
|
||||||
|
|
||||||
|
// Trigger simulation and rendering
|
||||||
|
csPhysics.dispatch(1, 1, 1);
|
||||||
|
|
||||||
|
const dispatchCount = Math.ceil((currentWidth * currentHeight) / 64);
|
||||||
|
csRender.dispatch(dispatchCount, 1, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetImpulses() {
|
||||||
|
if (this.simParams.impulseM1 !== 0.0) {
|
||||||
|
this.simParams.impulseM1 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.simParams.impulseM2 !== 0.0) {
|
||||||
|
this.simParams.impulseM2 = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushPendulum(m1: boolean) {
|
||||||
|
if (m1)
|
||||||
|
{
|
||||||
|
this.simParams.impulseM1 = IMPULSE_M1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.simParams.impulseM2 = IMPULSE_M2;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPendulum() {
|
||||||
|
this.createSimulation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default PendulumComponent
|
||||||
24
src/app/pages/algorithms/pendulum/pendulum.model.ts
Normal file
24
src/app/pages/algorithms/pendulum/pendulum.model.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export const DEFAULT_G = 9.81;
|
||||||
|
export const MIN_G = 2;
|
||||||
|
export const MAX_G = 15;
|
||||||
|
|
||||||
|
export const DEFAULT_DAMPING = 0.999;
|
||||||
|
export const MIN_DAMPING = 1;
|
||||||
|
export const MAX_DAMPING = 0.7;
|
||||||
|
|
||||||
|
export const DEFAULT_TRAIL_DECAY = 0.96;
|
||||||
|
export const MIN_TRAIL_DECAY = 0.2;
|
||||||
|
export const MAX_TRAIL_DECAY = 0.9999;
|
||||||
|
|
||||||
|
export const DEFAULT_L1_LENGTH = 1.5;
|
||||||
|
export const DEFAULT_L2_LENGTH = 1.2;
|
||||||
|
export const MIN_LENGTH = 0.2;
|
||||||
|
export const MAX_LENGTH = 3;
|
||||||
|
|
||||||
|
export const DEFAULT_M1_MASS = 2;
|
||||||
|
export const DEFAULT_M2_MASS = 1;
|
||||||
|
export const MIN_MASS = 0.1;
|
||||||
|
export const MAX_MASS = 5;
|
||||||
|
|
||||||
|
export const IMPULSE_M1 = 7;
|
||||||
|
export const IMPULSE_M2 = 15;
|
||||||
236
src/app/pages/algorithms/pendulum/pendulum.shader.ts
Normal file
236
src/app/pages/algorithms/pendulum/pendulum.shader.ts
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
//Simple Pass-Through Shader
|
||||||
|
export const PENDULUM_VERTEX_SHADER_WGSL = `
|
||||||
|
attribute position : vec3<f32>;
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn main(input : VertexInputs) -> FragmentInputs {
|
||||||
|
var output : FragmentInputs;
|
||||||
|
output.position = vec4<f32>(input.position, 1.0);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// --- SHARED DATA STRUCTURES ---
|
||||||
|
// These structs map exactly to the Float32Array in the TypeScript code.
|
||||||
|
const SHARED_STRUCTS = `
|
||||||
|
struct Params {
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
time: f32,
|
||||||
|
dt: f32,
|
||||||
|
g: f32,
|
||||||
|
m1: f32,
|
||||||
|
m2: f32,
|
||||||
|
l1: f32,
|
||||||
|
l2: f32,
|
||||||
|
damping: f32,
|
||||||
|
trailDecay: f32,
|
||||||
|
impulseM1: f32,
|
||||||
|
impulseM2: f32,
|
||||||
|
pad: f32 // <-- Padding for safe 16-byte memory alignment
|
||||||
|
};
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
theta1: f32,
|
||||||
|
theta2: f32,
|
||||||
|
v1: f32,
|
||||||
|
v2: f32
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
//Fragment Shader to display the pixel buffer
|
||||||
|
export const PENDULUM_FRAGMENT_SHADER_WGSL = SHARED_STRUCTS + `
|
||||||
|
var<storage, read> pixelBuffer : array<f32>;
|
||||||
|
var<storage, read> p : Params;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn main(input : FragmentInputs) -> FragmentOutputs {
|
||||||
|
let width = u32(p.width);
|
||||||
|
let height = u32(p.height);
|
||||||
|
|
||||||
|
if (width == 0u || height == 0u) {
|
||||||
|
fragmentOutputs.color = vec4<f32>(0.5, 0.0, 0.0, 1.0);
|
||||||
|
return fragmentOutputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = u32(input.position.x);
|
||||||
|
let y = u32(input.position.y);
|
||||||
|
|
||||||
|
if (x >= width || y >= height) {
|
||||||
|
fragmentOutputs.color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||||
|
return fragmentOutputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = y * width + x;
|
||||||
|
|
||||||
|
// --- THE MAGIC DECODING ---
|
||||||
|
var val = pixelBuffer[index];
|
||||||
|
var isLine1 = false;
|
||||||
|
var isLine2 = false;
|
||||||
|
|
||||||
|
// 1. Check for overlays (Lines)
|
||||||
|
if (val >= 20.0) {
|
||||||
|
isLine2 = true;
|
||||||
|
val = val - 20.0;
|
||||||
|
} else if (val >= 10.0) {
|
||||||
|
isLine1 = true;
|
||||||
|
val = val - 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check which trail it is
|
||||||
|
var isTrail2 = false;
|
||||||
|
if (val >= 2.0) {
|
||||||
|
isTrail2 = true;
|
||||||
|
val = val - 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. What remains is purely the fading intensity (0.0 to 1.0)
|
||||||
|
let trailIntensity = val;
|
||||||
|
|
||||||
|
// --- COLORS ---
|
||||||
|
let bgColor = vec3<f32>(0.1, 0.1, 0.15);
|
||||||
|
let mass1Color = vec3<f32>(1.0, 0.0, 0.0); // Red
|
||||||
|
let mass2Color = vec3<f32>(0.0, 1.0, 0.0); // Green
|
||||||
|
let line1Color = vec3<f32>(1.0, 1.0, 0.0); // Yellow
|
||||||
|
let line2Color = vec3<f32>(1.0, 0.0, 1.0); // Magenta
|
||||||
|
|
||||||
|
var massColor = mass1Color;
|
||||||
|
if (isTrail2) {
|
||||||
|
massColor = mass2Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate background blending with the trail
|
||||||
|
var finalColor = mix(bgColor, massColor, clamp(trailIntensity, 0.0, 1.0));
|
||||||
|
|
||||||
|
// Overwrite with the line colors if necessary
|
||||||
|
if (isLine1) { finalColor = line1Color; }
|
||||||
|
if (isLine2) { finalColor = line2Color; }
|
||||||
|
|
||||||
|
fragmentOutputs.color = vec4<f32>(finalColor, 1.0);
|
||||||
|
return fragmentOutputs;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
//Math for the double pendulum
|
||||||
|
//https://en.wikipedia.org/wiki/Double_pendulum
|
||||||
|
export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + `
|
||||||
|
@group(0) @binding(0) var<storage, read_write> state : State;
|
||||||
|
@group(0) @binding(1) var<storage, read> p : Params;
|
||||||
|
|
||||||
|
@compute @workgroup_size(1)
|
||||||
|
fn main() {
|
||||||
|
let t1 = state.theta1;
|
||||||
|
let t2 = state.theta2;
|
||||||
|
let v1 = state.v1;
|
||||||
|
let v2 = state.v2;
|
||||||
|
|
||||||
|
let delta_t = t1 - t2;
|
||||||
|
|
||||||
|
let num1 = -p.g * (2.0 * p.m1 + p.m2) * sin(t1)
|
||||||
|
- p.m2 * p.g * sin(t1 - 2.0 * t2)
|
||||||
|
- 2.0 * sin(delta_t) * p.m2 * (v2 * v2 * p.l2 + v1 * v1 * p.l1 * cos(delta_t));
|
||||||
|
let den1 = p.l1 * (2.0 * p.m1 + p.m2 - p.m2 * cos(2.0 * delta_t));
|
||||||
|
let a1 = num1 / den1;
|
||||||
|
|
||||||
|
let num2 = 2.0 * sin(delta_t) * (v1 * v1 * p.l1 * (p.m1 + p.m2) + p.g * (p.m1 + p.m2) * cos(t1) + v2 * v2 * p.l2 * p.m2 * cos(delta_t));
|
||||||
|
let den2 = p.l2 * (2.0 * p.m1 + p.m2 - p.m2 * cos(2.0 * delta_t));
|
||||||
|
let a2 = num2 / den2;
|
||||||
|
|
||||||
|
let new_v1 = (v1 + a1 * p.dt) * p.damping + p.impulseM1;
|
||||||
|
let new_v2 = (v2 + a2 * p.dt) * p.damping + p.impulseM2;
|
||||||
|
|
||||||
|
state.v1 = new_v1;
|
||||||
|
state.v2 = new_v2;
|
||||||
|
state.theta1 = t1 + new_v1 * p.dt;
|
||||||
|
state.theta2 = t2 + new_v2 * p.dt;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
//Pixel data to visualize the pendulum
|
||||||
|
export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + `
|
||||||
|
@group(0) @binding(0) var<storage, read_write> pixelBuffer : array<f32>;
|
||||||
|
@group(0) @binding(1) var<storage, read> p : Params;
|
||||||
|
@group(0) @binding(2) var<storage, read> state : State;
|
||||||
|
|
||||||
|
fn sdSegment(point: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
|
||||||
|
let pa = point - a;
|
||||||
|
let ba = b - a;
|
||||||
|
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||||
|
return length(pa - ba * h);
|
||||||
|
}
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
let index = global_id.x;
|
||||||
|
let width = u32(p.width);
|
||||||
|
let height = u32(p.height);
|
||||||
|
|
||||||
|
if (index >= width * height) { return; }
|
||||||
|
|
||||||
|
let x = f32(index % width);
|
||||||
|
let y = f32(index / width);
|
||||||
|
let uv = vec2<f32>(x / p.width, y / p.height);
|
||||||
|
|
||||||
|
let aspect = p.width / p.height;
|
||||||
|
let uv_corr = vec2<f32>(uv.x * aspect, uv.y);
|
||||||
|
|
||||||
|
// --- 1. EXTRACT & DECAY OLD MEMORY ---
|
||||||
|
var memory = pixelBuffer[index];
|
||||||
|
|
||||||
|
// Strip line overlays from the previous frame
|
||||||
|
if (memory >= 20.0) { memory = memory - 20.0; }
|
||||||
|
else if (memory >= 10.0) { memory = memory - 10.0; }
|
||||||
|
|
||||||
|
// Check if the memory belongs to Trail 2
|
||||||
|
var isTrail2 = false;
|
||||||
|
if (memory >= 2.0) {
|
||||||
|
isTrail2 = true;
|
||||||
|
memory = memory - 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply decay to the pure intensity
|
||||||
|
memory = memory * p.trailDecay;
|
||||||
|
|
||||||
|
// --- 2. CALCULATE GEOMETRY ---
|
||||||
|
let origin = vec2<f32>(0.5 * aspect, 0.3);
|
||||||
|
let displayScale = 0.15;
|
||||||
|
|
||||||
|
let p1 = origin + vec2<f32>(sin(state.theta1), cos(state.theta1)) * p.l1 * displayScale;
|
||||||
|
let p2 = p1 + vec2<f32>(sin(state.theta2), cos(state.theta2)) * p.l2 * displayScale;
|
||||||
|
|
||||||
|
let dLine1 = sdSegment(uv_corr, origin, p1);
|
||||||
|
let dLine2 = sdSegment(uv_corr, p1, p2);
|
||||||
|
let dMass1 = length(uv_corr - p1);
|
||||||
|
let dMass2 = length(uv_corr - p2);
|
||||||
|
|
||||||
|
// --- 3. SMART LAYERING ---
|
||||||
|
var baseVal = 0.0;
|
||||||
|
|
||||||
|
// Base Layer (Masses & Trails)
|
||||||
|
if (dMass1 < 0.02) {
|
||||||
|
baseVal = 1.0; // Mass 1 = 1.0 (Trail 1 Max)
|
||||||
|
} else if (dMass2 < 0.02) {
|
||||||
|
baseVal = 3.0; // Mass 2 = 2.0 (Flag) + 1.0 (Trail 2 Max)
|
||||||
|
} else {
|
||||||
|
// Write fading memory back
|
||||||
|
if (isTrail2) {
|
||||||
|
baseVal = memory + 2.0;
|
||||||
|
} else {
|
||||||
|
baseVal = memory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay Layer (Lines)
|
||||||
|
var overlay = 0.0;
|
||||||
|
// Don't draw lines over the masses (Clean Z-Index)
|
||||||
|
if (dMass1 < 0.02 || dMass2 < 0.02) {
|
||||||
|
overlay = 0.0;
|
||||||
|
} else if (dLine1 < 0.003) {
|
||||||
|
overlay = 10.0;
|
||||||
|
} else if (dLine2 < 0.003) {
|
||||||
|
overlay = 20.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pixelBuffer[index] = baseVal + overlay;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { AlgorithmCategory } from '../models/algorithm-category';
|
|
||||||
import { Observable, of } from 'rxjs';
|
|
||||||
import {RouterConstants} from '../../../constants/RouterConstants';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AlgorithmsService {
|
|
||||||
|
|
||||||
private readonly categories: AlgorithmCategory[] = [
|
|
||||||
{
|
|
||||||
id: 'pathfinding',
|
|
||||||
title: 'ALGORITHM.PATHFINDING.TITLE',
|
|
||||||
description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
|
|
||||||
routerLink: RouterConstants.PATHFINDING.LINK
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sorting',
|
|
||||||
title: 'ALGORITHM.SORTING.TITLE',
|
|
||||||
description: 'ALGORITHM.SORTING.DESCRIPTION',
|
|
||||||
routerLink: RouterConstants.SORTING.LINK
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gameOfLife',
|
|
||||||
title: 'ALGORITHM.GOL.TITLE',
|
|
||||||
description: 'ALGORITHM.GOL.DESCRIPTION',
|
|
||||||
routerLink: RouterConstants.GOL.LINK
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
getCategories(): Observable<AlgorithmCategory[]> {
|
|
||||||
return of(this.categories);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<mat-card class="container sorting-card">
|
<mat-card class="algo-container sorting-card">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>{{ 'SORTING.TITLE' | translate }}</mat-card-title>
|
<mat-card-title>{{ 'SORTING.TITLE' | translate }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
.sorting-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.controls-panel {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
mat-form-field {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.visualization-area {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
height: 300px; /* Max height for bars */
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
gap: 1px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
flex-grow: 1;
|
|
||||||
background-color: #424242; /* Default unsorted color */
|
|
||||||
transition: height 0.05s ease-in-out, background-color 0.05s ease-in-out;
|
|
||||||
width: 10px; /* Default width, flex-grow will adjust */
|
|
||||||
min-width: 1px; /* Ensure bars are always visible */
|
|
||||||
|
|
||||||
&.unsorted {
|
|
||||||
background-color: #424242;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.comparing {
|
|
||||||
background-color: #ffeb3b; /* Yellow for comparing */
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sorted {
|
|
||||||
background-color: #4caf50; /* Green for sorted */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ export class SortingComponent implements OnInit {
|
|||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4));
|
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4));
|
||||||
|
|
||||||
console.log(snapshots.length);
|
|
||||||
this.animateSorting(snapshots);
|
this.animateSorting(snapshots);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
.imprint {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imprint-card {
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imprint-title {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imprint-section {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.25rem;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.imprint-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--mat-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,63 +1,90 @@
|
|||||||
<h2 mat-dialog-title>{{ project.title | translate }}</h2>
|
<h2 mat-dialog-title>{{ project.title | translate }}</h2>
|
||||||
<mat-dialog-content #dialogContent>
|
<mat-dialog-content #dialogContent>
|
||||||
<p>{{ project.introduction | translate }}</p>
|
<div class="project-dialog-layout">
|
||||||
|
<div class="project-info">
|
||||||
|
<p class="introduction">{{ project.introduction | translate }}</p>
|
||||||
|
|
||||||
<ul>
|
<div class="features-list">
|
||||||
@for(bullet of project.bulletPoints; track bullet) {
|
<ul>
|
||||||
<li>{{ bullet | translate }}</li>
|
@for(bullet of project.bulletPoints; track bullet) {
|
||||||
}
|
<li>{{ bullet | translate }}</li>
|
||||||
</ul>
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (project.images.length > 0)
|
<div class="insight-grid">
|
||||||
{
|
<div class="insight-card technical">
|
||||||
<swiper-container
|
<div class="insight-header">
|
||||||
class="my-swiper"
|
<mat-icon>settings_suggest</mat-icon>
|
||||||
[attr.slides-per-view]="1.2"
|
<h3>{{ 'PROJECTS.SECTION.TECHNICAL' | translate }}</h3>
|
||||||
[attr.space-between]="12"
|
</div>
|
||||||
[attr.navigation]="true"
|
<ul>
|
||||||
[attr.pagination]="true"
|
@for(challenge of project.challenges; track challenge) {
|
||||||
[attr.keyboard]="true"
|
<li>{{ challenge | translate }}</li>
|
||||||
style="width: 100%;"
|
}
|
||||||
>
|
</ul>
|
||||||
@for (img of project.images; track img) {
|
</div>
|
||||||
|
|
||||||
|
<div class="insight-card softskills">
|
||||||
|
<div class="insight-header">
|
||||||
|
<mat-icon>psychology</mat-icon>
|
||||||
|
<h3>{{ 'PROJECTS.SECTION.LEARNINGS' | translate }}</h3>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
@for(learning of project.learnings; track learning) {
|
||||||
|
<li>{{ learning | translate }}</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (project.images.length > 0)
|
||||||
|
{
|
||||||
|
<div class="media-section">
|
||||||
|
<swiper-container class="my-swiper" [attr.slides-per-view]="1" [attr.space-between]="12" [attr.navigation]="true"
|
||||||
|
[attr.pagination]="true" [attr.keyboard]="true" style="width: 100%;">
|
||||||
|
@for (img of project.images; track img) {
|
||||||
<swiper-slide>
|
<swiper-slide>
|
||||||
<img
|
<img class="slide-img" [src]="img.url" [alt]="project.title | translate" />
|
||||||
class="slide-img"
|
|
||||||
[src]="img.url"
|
|
||||||
[alt]="project.title | translate"
|
|
||||||
/>
|
|
||||||
@if (img.source) {
|
@if (img.source) {
|
||||||
<div class="slide-source">
|
<div class="slide-source">
|
||||||
{{ img.source }}
|
{{ img.source }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</swiper-slide>
|
</swiper-slide>
|
||||||
}
|
}
|
||||||
</swiper-container>
|
</swiper-container>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
<mat-chip-set aria-label="Technologies">
|
|
||||||
@for(tech of project.technologies; track tech) {
|
|
||||||
<mat-chip>{{tech}}</mat-chip>
|
|
||||||
}
|
|
||||||
</mat-chip-set>
|
|
||||||
|
|
||||||
<div class="link-section">
|
|
||||||
@for(link of project.links; track link)
|
|
||||||
{
|
|
||||||
<a mat-button href="{{link.url}}" target="_blank" rel="noopener noreferrer">
|
|
||||||
<mat-icon>open_in_new</mat-icon>
|
|
||||||
{{ link.name | translate }}
|
|
||||||
</a>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@if(project.assets)
|
<div class="footer-details">
|
||||||
{
|
<div class="tech-stack">
|
||||||
<a mat-button href="{{project.assets}}" rel="noopener noreferrer">
|
<mat-chip-set aria-label="Technologies">
|
||||||
<mat-icon>download</mat-icon>
|
@for(tech of project.technologies; track tech) {
|
||||||
{{ 'PROJECTS.DOWNLOAD' | translate}}
|
<mat-chip>{{tech}}</mat-chip>
|
||||||
</a>
|
}
|
||||||
}
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="link-section">
|
||||||
|
@for(link of project.links; track link)
|
||||||
|
{
|
||||||
|
<a mat-button href="{{link.url}}" target="_blank" rel="noopener noreferrer">
|
||||||
|
<mat-icon>open_in_new</mat-icon>
|
||||||
|
{{ link.name | translate }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if(project.assets)
|
||||||
|
{
|
||||||
|
<a mat-button href="{{project.assets}}" rel="noopener noreferrer">
|
||||||
|
<mat-icon>download</mat-icon>
|
||||||
|
{{ 'PROJECTS.DOWNLOAD' | translate}}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|||||||
@@ -1,81 +1,117 @@
|
|||||||
.my-swiper::part(button-prev),
|
.project-dialog-layout {
|
||||||
.my-swiper::part(button-next) {
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(0,0,0,.5);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-swiper::part(button-prev):hover,
|
|
||||||
.my-swiper::part(button-next):hover {
|
|
||||||
background: rgba(0,0,0,.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-swiper {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-swiper::part(pagination) {
|
|
||||||
bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
swiper-slide {
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #222;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-img {
|
.introduction {
|
||||||
width: 100%;
|
font-size: 1.1rem;
|
||||||
height: auto;
|
line-height: 1.6;
|
||||||
max-height: 512px !important;
|
opacity: 0.9;
|
||||||
object-fit: contain;
|
margin-bottom: 1rem;
|
||||||
display: block;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-source {
|
.features-list {
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #aaa;
|
|
||||||
background: #2a2a2a;
|
|
||||||
padding: 0.5rem;
|
|
||||||
text-align: right;
|
|
||||||
border-top: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding-left: 20px;
|
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
ul {
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
.insight-grid {
|
||||||
margin-bottom: 0.5rem;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-chip-set {
|
.insight-card {
|
||||||
margin-top: 1.5rem;
|
padding: 1.25rem;
|
||||||
margin-bottom: 1.5rem;
|
border-radius: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.insight-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--link-color);
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .insight-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-section {
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .footer-details {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-section {
|
.link-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-dialog-actions {
|
@media (max-width: 600px) {
|
||||||
justify-content: flex-end;
|
.insight-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="project-grid">
|
<div class="card-grid">
|
||||||
@if (featuredProject(); as project) {
|
@if (featuredProject(); as project) {
|
||||||
<mat-card class="project-card featured">
|
<mat-card class="project-card featured">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
.project-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 1.5rem;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card {
|
|
||||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.featured {
|
|
||||||
grid-column: 1 / -1; // Span full width
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-header {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-content {
|
|
||||||
flex-grow: 1; // Ensure content area expands
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-chip-set {
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-actions {
|
|
||||||
margin-top: auto; // Push actions to the bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 200px; /* Or a height that fits your design */
|
|
||||||
background-color: #f0f0f0; /* A light background for the icon */
|
|
||||||
}
|
|
||||||
|
|
||||||
.fallback-icon {
|
|
||||||
font-size: 4rem;
|
|
||||||
width: 4rem;
|
|
||||||
height: 4rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure images don't exceed the card width and maintain aspect ratio
|
|
||||||
img[mat-card-image] {
|
|
||||||
width: 100%;
|
|
||||||
height: 250px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit} from '@angular/core';
|
import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {Subscription} from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
import {MatCardModule} from "@angular/material/card";
|
import { MatCardModule } from "@angular/material/card";
|
||||||
import {MatChipsModule} from "@angular/material/chips";
|
import { MatChipsModule } from "@angular/material/chips";
|
||||||
import {MatIcon} from "@angular/material/icon";
|
import { MatIcon } from "@angular/material/icon";
|
||||||
import {TranslatePipe} from "@ngx-translate/core";
|
import { TranslatePipe } from "@ngx-translate/core";
|
||||||
import {MatButtonModule} from "@angular/material/button";
|
import { MatButtonModule } from "@angular/material/button";
|
||||||
import {MatDialog} from "@angular/material/dialog";
|
import { MatDialog } from "@angular/material/dialog";
|
||||||
import {ProjectDialogComponent} from "./dialog/project-dialog.component";
|
import { ProjectDialogComponent } from "./dialog/project-dialog.component";
|
||||||
import {AssetsConstants} from "../../constants/AssetsConstants";
|
import { AssetsConstants } from "../../constants/AssetsConstants";
|
||||||
|
|
||||||
export interface Projects {
|
export interface Projects {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
@@ -26,6 +26,8 @@ export interface Projects {
|
|||||||
url: string
|
url: string
|
||||||
}[],
|
}[],
|
||||||
bulletPoints: string[],
|
bulletPoints: string[],
|
||||||
|
challenges: string[],
|
||||||
|
learnings: string[],
|
||||||
isFeatured: boolean,
|
isFeatured: boolean,
|
||||||
technologies: string[]
|
technologies: string[]
|
||||||
}
|
}
|
||||||
@@ -46,124 +48,165 @@ export interface Projects {
|
|||||||
})
|
})
|
||||||
export class ProjectsComponent implements OnInit, OnDestroy {
|
export class ProjectsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly dialog = inject(MatDialog);
|
private readonly dialog = inject(MatDialog);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private queryParamSub: Subscription | undefined;
|
private queryParamSub: Subscription | undefined;
|
||||||
|
|
||||||
allProjects: Projects[] = [
|
allProjects: Projects[] = [
|
||||||
{
|
{
|
||||||
identifier: "playground",
|
identifier: "playground",
|
||||||
title: 'PROJECTS.PLAYGROUND.TITLE',
|
title: 'PROJECTS.PLAYGROUND.TITLE',
|
||||||
shortDescription: 'PROJECTS.PLAYGROUND.SHORT_DESCRIPTION',
|
shortDescription: 'PROJECTS.PLAYGROUND.SHORT_DESCRIPTION',
|
||||||
introduction: 'PROJECTS.PLAYGROUND.INTRODUCTION',
|
introduction: 'PROJECTS.PLAYGROUND.INTRODUCTION',
|
||||||
images: [],
|
images: AssetsConstants.PLAYGROUND_IMAGES.map(url => ({ url, source: '' })),
|
||||||
icon: 'web',
|
icon: 'web',
|
||||||
assets: '',
|
assets: '',
|
||||||
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://andreas-dahm.eu'}],
|
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://andreas-dahm.eu' }],
|
||||||
bulletPoints: [
|
bulletPoints: [
|
||||||
'PROJECTS.PLAYGROUND.BULLET_1',
|
'PROJECTS.PLAYGROUND.BULLET_1',
|
||||||
'PROJECTS.PLAYGROUND.BULLET_2',
|
'PROJECTS.PLAYGROUND.BULLET_2',
|
||||||
'PROJECTS.PLAYGROUND.BULLET_3',
|
'PROJECTS.PLAYGROUND.BULLET_3',
|
||||||
'PROJECTS.PLAYGROUND.BULLET_4',
|
'PROJECTS.PLAYGROUND.BULLET_4',
|
||||||
],
|
],
|
||||||
isFeatured: false,
|
challenges: [
|
||||||
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'GitHub Actions', 'Docker']
|
'PROJECTS.PLAYGROUND.CHALLENGE_1',
|
||||||
},
|
'PROJECTS.PLAYGROUND.CHALLENGE_2',
|
||||||
{
|
],
|
||||||
identifier: "elmucho",
|
learnings: [
|
||||||
title: 'PROJECTS.EL_MUCHO.TITLE',
|
'PROJECTS.PLAYGROUND.LEARNING_1',
|
||||||
shortDescription: 'PROJECTS.EL_MUCHO.SHORT_DESCRIPTION',
|
'PROJECTS.PLAYGROUND.LEARNING_2',
|
||||||
introduction: 'PROJECTS.EL_MUCHO.INTRODUCTION',
|
],
|
||||||
images: AssetsConstants.EL_MUCHO_IMAGES.map(url => ({ url, source: '' })),
|
isFeatured: false,
|
||||||
icon: 'sports_esports',
|
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'GitHub Actions', 'Docker']
|
||||||
assets: '',
|
},
|
||||||
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://store.steampowered.com/app/1532640/El_Mucho/'}],
|
{
|
||||||
bulletPoints: [
|
identifier: "elmucho",
|
||||||
'PROJECTS.EL_MUCHO.BULLET_1',
|
title: 'PROJECTS.EL_MUCHO.TITLE',
|
||||||
'PROJECTS.EL_MUCHO.BULLET_2',
|
shortDescription: 'PROJECTS.EL_MUCHO.SHORT_DESCRIPTION',
|
||||||
'PROJECTS.EL_MUCHO.BULLET_3',
|
introduction: 'PROJECTS.EL_MUCHO.INTRODUCTION',
|
||||||
'PROJECTS.EL_MUCHO.BULLET_4',
|
images: AssetsConstants.EL_MUCHO_IMAGES.map(url => ({ url, source: '' })),
|
||||||
],
|
icon: 'sports_esports',
|
||||||
isFeatured: true,
|
assets: '',
|
||||||
technologies: ['Unity', 'C#', 'Steamworks', 'Git']
|
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://store.steampowered.com/app/1532640/El_Mucho/' }],
|
||||||
},
|
bulletPoints: [
|
||||||
{
|
'PROJECTS.EL_MUCHO.BULLET_1',
|
||||||
identifier: "gamejams",
|
'PROJECTS.EL_MUCHO.BULLET_2',
|
||||||
title: 'PROJECTS.GAME_JAMS.TITLE',
|
'PROJECTS.EL_MUCHO.BULLET_3',
|
||||||
shortDescription: 'PROJECTS.GAME_JAMS.SHORT_DESCRIPTION',
|
'PROJECTS.EL_MUCHO.BULLET_4',
|
||||||
introduction: 'PROJECTS.GAME_JAMS.INTRODUCTION',
|
],
|
||||||
images: AssetsConstants.GAME_JAMS_IMAGES.map(url => ({ url, source: '' })),
|
challenges: [
|
||||||
icon: 'videogame_asset',
|
'PROJECTS.EL_MUCHO.CHALLENGE_1',
|
||||||
assets: '',
|
'PROJECTS.EL_MUCHO.CHALLENGE_2',
|
||||||
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://itch.io/c/6628860/lobos-collection'}],
|
'PROJECTS.EL_MUCHO.CHALLENGE_3',
|
||||||
bulletPoints: [
|
],
|
||||||
'PROJECTS.GAME_JAMS.BULLET_1',
|
learnings: [
|
||||||
'PROJECTS.GAME_JAMS.BULLET_2',
|
'PROJECTS.EL_MUCHO.LEARNING_1',
|
||||||
'PROJECTS.GAME_JAMS.BULLET_3',
|
'PROJECTS.EL_MUCHO.LEARNING_2',
|
||||||
'PROJECTS.GAME_JAMS.BULLET_4',
|
],
|
||||||
],
|
isFeatured: true,
|
||||||
isFeatured: false,
|
technologies: ['Unity', 'C#', 'Steamworks', 'Git']
|
||||||
technologies: ['Unity', 'C#', 'Git']
|
},
|
||||||
},
|
{
|
||||||
{
|
identifier: "gamejams",
|
||||||
identifier: "diploma",
|
title: 'PROJECTS.GAME_JAMS.TITLE',
|
||||||
title: 'PROJECTS.DIPLOMA.TITLE',
|
shortDescription: 'PROJECTS.GAME_JAMS.SHORT_DESCRIPTION',
|
||||||
shortDescription: 'PROJECTS.DIPLOMA.SHORT_DESCRIPTION',
|
introduction: 'PROJECTS.GAME_JAMS.INTRODUCTION',
|
||||||
introduction: 'PROJECTS.DIPLOMA.INTRODUCTION',
|
images: AssetsConstants.GAME_JAMS_IMAGES.map(url => ({ url, source: '' })),
|
||||||
images: AssetsConstants.DIPLOMA_IMAGES.map(url => ({ url, source: '' })),
|
icon: 'videogame_asset',
|
||||||
icon: 'history_edu',
|
assets: '',
|
||||||
assets: AssetsConstants.DIPLOMA,
|
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://itch.io/c/6628860/lobos-collection' }],
|
||||||
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://www.th-bingen.de'}],
|
bulletPoints: [
|
||||||
bulletPoints: [
|
'PROJECTS.GAME_JAMS.BULLET_1',
|
||||||
'PROJECTS.DIPLOMA.BULLET_1',
|
'PROJECTS.GAME_JAMS.BULLET_2',
|
||||||
'PROJECTS.DIPLOMA.BULLET_2',
|
'PROJECTS.GAME_JAMS.BULLET_3',
|
||||||
'PROJECTS.DIPLOMA.BULLET_3',
|
'PROJECTS.GAME_JAMS.BULLET_4',
|
||||||
'PROJECTS.DIPLOMA.BULLET_4',
|
],
|
||||||
],
|
challenges: [
|
||||||
isFeatured: false,
|
'PROJECTS.GAME_JAMS.CHALLENGE_1',
|
||||||
technologies: ['C++', 'OpenGL', 'Qt', '3D-Scanner']
|
'PROJECTS.GAME_JAMS.CHALLENGE_2',
|
||||||
},
|
],
|
||||||
{
|
learnings: [
|
||||||
identifier: "tribble-the-homeserver",
|
'PROJECTS.GAME_JAMS.LEARNING_1',
|
||||||
title: 'PROJECTS.TRIBBLE.TITLE',
|
'PROJECTS.GAME_JAMS.LEARNING_2',
|
||||||
shortDescription: 'PROJECTS.TRIBBLE.SHORT_DESCRIPTION',
|
],
|
||||||
introduction: 'PROJECTS.TRIBBLE.INTRODUCTION',
|
isFeatured: false,
|
||||||
images: [
|
technologies: ['Unity', 'C#', 'Git']
|
||||||
{ url: AssetsConstants.TRIBBLE_IMAGES[0], source: 'https://upload.wikimedia.org/wikipedia/commons/0/03/Hostinger_Logo.png'},
|
},
|
||||||
{ url: AssetsConstants.TRIBBLE_IMAGES[1], source: 'https://dashboardicons.com/icons/docker-engine'},
|
{
|
||||||
{ url: AssetsConstants.TRIBBLE_IMAGES[2], source: 'https://dashboardicons.com/icons/gitea'},
|
identifier: "diploma",
|
||||||
{ url: AssetsConstants.TRIBBLE_IMAGES[3], source: 'https://commons.wikimedia.org/wiki/File:Traefik.logo.png'}
|
title: 'PROJECTS.DIPLOMA.TITLE',
|
||||||
],
|
shortDescription: 'PROJECTS.DIPLOMA.SHORT_DESCRIPTION',
|
||||||
icon: 'dns',
|
introduction: 'PROJECTS.DIPLOMA.INTRODUCTION',
|
||||||
assets: '',
|
images: AssetsConstants.DIPLOMA_IMAGES.map(url => ({ url, source: '' })),
|
||||||
links: [
|
icon: 'history_edu',
|
||||||
{name: 'Ubuntu Server', url: 'https://ubuntu.com/server'},
|
assets: AssetsConstants.DIPLOMA,
|
||||||
{name: 'Docker', url: 'https://www.docker.com/'},
|
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://www.th-bingen.de' }],
|
||||||
{name: 'Traefik', url: 'https://traefik.io/'},
|
bulletPoints: [
|
||||||
{name: 'Gitea', url: 'https://gitea.io/'},
|
'PROJECTS.DIPLOMA.BULLET_1',
|
||||||
{name: 'Jellyfin', url: 'https://jellyfin.org/'},
|
'PROJECTS.DIPLOMA.BULLET_2',
|
||||||
{name: 'AdGuard Home', url: 'https://adguard.com/en/adguard-home/overview.html'},
|
'PROJECTS.DIPLOMA.BULLET_3',
|
||||||
{name: 'Paperless-ngx', url: 'https://paperless-ngx.com/'},
|
'PROJECTS.DIPLOMA.BULLET_4',
|
||||||
{name: 'Tailscale', url: 'https://tailscale.com/'}
|
],
|
||||||
],
|
challenges: [
|
||||||
bulletPoints: [
|
'PROJECTS.DIPLOMA.CHALLENGE_1',
|
||||||
'PROJECTS.TRIBBLE.BULLET_1',
|
'PROJECTS.DIPLOMA.CHALLENGE_2',
|
||||||
'PROJECTS.TRIBBLE.BULLET_2',
|
],
|
||||||
'PROJECTS.TRIBBLE.BULLET_3',
|
learnings: [
|
||||||
'PROJECTS.TRIBBLE.BULLET_4',
|
'PROJECTS.DIPLOMA.LEARNING_1',
|
||||||
],
|
'PROJECTS.DIPLOMA.LEARNING_2',
|
||||||
isFeatured: false,
|
],
|
||||||
technologies: ['Ubuntu Server', 'Docker', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale']
|
isFeatured: false,
|
||||||
}
|
technologies: ['Java', 'Performance', 'Algorithm', 'Simulation']
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
identifier: "tribble-the-homeserver",
|
||||||
|
title: 'PROJECTS.TRIBBLE.TITLE',
|
||||||
|
shortDescription: 'PROJECTS.TRIBBLE.SHORT_DESCRIPTION',
|
||||||
|
introduction: 'PROJECTS.TRIBBLE.INTRODUCTION',
|
||||||
|
images: [
|
||||||
|
{ url: AssetsConstants.TRIBBLE_IMAGES[0], source: 'https://upload.wikimedia.org/wikipedia/commons/0/03/Hostinger_Logo.png' },
|
||||||
|
{ url: AssetsConstants.TRIBBLE_IMAGES[1], source: 'https://dashboardicons.com/icons/docker-engine' },
|
||||||
|
{ url: AssetsConstants.TRIBBLE_IMAGES[2], source: 'https://dashboardicons.com/icons/gitea' },
|
||||||
|
{ url: AssetsConstants.TRIBBLE_IMAGES[3], source: 'https://dashboardicons.com/icons/traefik' }
|
||||||
|
],
|
||||||
|
icon: 'dns',
|
||||||
|
assets: '',
|
||||||
|
links: [
|
||||||
|
{ name: 'Ubuntu Server', url: 'https://ubuntu.com/server' },
|
||||||
|
{ name: 'Docker', url: 'https://www.docker.com/' },
|
||||||
|
{ name: 'Traefik', url: 'https://traefik.io/' },
|
||||||
|
{ name: 'Gitea', url: 'https://gitea.io/' },
|
||||||
|
{ name: 'Jellyfin', url: 'https://jellyfin.org/' },
|
||||||
|
{ name: 'AdGuard Home', url: 'https://adguard.com/en/adguard-home/overview.html' },
|
||||||
|
{ name: 'Paperless-ngx', url: 'https://paperless-ngx.com/' },
|
||||||
|
{ name: 'Tailscale', url: 'https://tailscale.com/' }
|
||||||
|
],
|
||||||
|
bulletPoints: [
|
||||||
|
'PROJECTS.TRIBBLE.BULLET_1',
|
||||||
|
'PROJECTS.TRIBBLE.BULLET_2',
|
||||||
|
'PROJECTS.TRIBBLE.BULLET_3',
|
||||||
|
'PROJECTS.TRIBBLE.BULLET_4',
|
||||||
|
],
|
||||||
|
challenges: [
|
||||||
|
'PROJECTS.TRIBBLE.CHALLENGE_1',
|
||||||
|
'PROJECTS.TRIBBLE.CHALLENGE_2',
|
||||||
|
],
|
||||||
|
learnings: [
|
||||||
|
'PROJECTS.TRIBBLE.LEARNING_1',
|
||||||
|
'PROJECTS.TRIBBLE.LEARNING_2',
|
||||||
|
],
|
||||||
|
isFeatured: false,
|
||||||
|
technologies: ['Ubuntu Server', 'Docker', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
featuredProject = computed(() => this.allProjects.find(p => p.isFeatured));
|
featuredProject = computed(() => this.allProjects.find(p => p.isFeatured));
|
||||||
otherProjects = computed(() => this.allProjects.filter(p => !p.isFeatured));
|
otherProjects = computed(() => this.allProjects.filter(p => !p.isFeatured));
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
setTimeout(() =>{ this.dialogOpenFunction(); },10);
|
setTimeout(() => { this.dialogOpenFunction(); }, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -172,8 +215,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private dialogOpenFunction() : void
|
private dialogOpenFunction(): void {
|
||||||
{
|
|
||||||
this.queryParamSub = this.route.queryParamMap.subscribe(params => {
|
this.queryParamSub = this.route.queryParamMap.subscribe(params => {
|
||||||
const projectIdentifier = params.get('project');
|
const projectIdentifier = params.get('project');
|
||||||
if (projectIdentifier) {
|
if (projectIdentifier) {
|
||||||
|
|||||||
@@ -7,4 +7,19 @@ export class SharedFunctions {
|
|||||||
globalThis.location.href = `mailto:${user}@${domain}`;
|
globalThis.location.href = `mailto:${user}@${domain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static randomIntFromInterval(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
}
|
||||||
|
|
||||||
|
static randomEventIntFromInterval(interval: number): number {
|
||||||
|
return Math.floor(Math.random() * (interval / 2)) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static shuffleArray<T>(array: T[]): T[] {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
<canvas #gridCanvas></canvas>
|
<div class="canvas-container">
|
||||||
|
<canvas #gridCanvas></canvas>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export class GenericGridComponent implements AfterViewInit {
|
|||||||
@Input() minGridSize: number = 5;
|
@Input() minGridSize: number = 5;
|
||||||
@Input() maxGridSize: number = 50;
|
@Input() maxGridSize: number = 50;
|
||||||
@Input() drawNodeBorderColor: string = '#ccc';
|
@Input() drawNodeBorderColor: string = '#ccc';
|
||||||
|
@Input() backgroundColor: string = 'lightgray';
|
||||||
|
|
||||||
// Callbacks from parent component
|
// Callbacks from parent component
|
||||||
@Input() createNodeFn!: (row: number, col: number) => any;
|
@Input() createNodeFn!: (row: number, col: number) => any;
|
||||||
@@ -99,19 +100,70 @@ export class GenericGridComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawGrid(): void {
|
drawGrid(): void {
|
||||||
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
if (!this.ctx || !this.grid.length) return;
|
||||||
|
|
||||||
|
const width = this.canvas.nativeElement.width;
|
||||||
|
const height = this.canvas.nativeElement.height;
|
||||||
|
const size = this.nodeSize;
|
||||||
|
|
||||||
|
this.ctx.fillStyle = this.backgroundColor;
|
||||||
|
this.ctx.fillRect(0, 0, width, height);
|
||||||
|
this.ctx.fillStyle = 'black';
|
||||||
|
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
this.drawNode(this.grid[row][col]);
|
const node = this.grid[row][col];
|
||||||
|
|
||||||
|
const color = this.getNodeColorFn(node);
|
||||||
|
|
||||||
|
if (color !== this.backgroundColor) {
|
||||||
|
if (this.ctx.fillStyle !== color) {
|
||||||
|
this.ctx.fillStyle = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = col * this.nodeSize;
|
||||||
|
const y = row * this.nodeSize;
|
||||||
|
|
||||||
|
this.ctx.fillRect(x, y, this.nodeSize, this.nodeSize);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (size > 2) {
|
||||||
|
this.drawGridLines(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawGridLines(width: number, height: number): void {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.strokeStyle = this.drawNodeBorderColor;
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
|
||||||
|
for (let col = 0; col <= this.gridCols; col++) {
|
||||||
|
const x = col * this.nodeSize;
|
||||||
|
this.ctx.moveTo(x, 0);
|
||||||
|
this.ctx.lineTo(x, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row = 0; row <= this.gridRows; row++) {
|
||||||
|
const y = row * this.nodeSize;
|
||||||
|
this.ctx.moveTo(0, y);
|
||||||
|
this.ctx.lineTo(width, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawNode(node: any): void {
|
drawNode(node: any): void {
|
||||||
this.ctx.fillStyle = this.getNodeColorFn(node);
|
this.ctx.fillStyle = this.getNodeColorFn(node);
|
||||||
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||||
this.ctx.strokeStyle = this.drawNodeBorderColor;
|
|
||||||
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
if (this.nodeSize > 4) {
|
||||||
|
this.ctx.strokeStyle = this.drawNodeBorderColor;
|
||||||
|
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getContextOrThrow(): CanvasRenderingContext2D {
|
private getContextOrThrow(): CanvasRenderingContext2D {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<canvas #canvas></canvas>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, HostListener, inject, NgZone, OnDestroy, ViewChild} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-particles-background',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './particles-background.component.html',
|
||||||
|
styleUrl: './particles-background.component.scss',
|
||||||
|
})
|
||||||
|
export class ParticleBackgroundComponent implements AfterViewInit, OnDestroy {
|
||||||
|
@ViewChild('canvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
private readonly ngZone = inject(NgZone);
|
||||||
|
|
||||||
|
private ctx!: CanvasRenderingContext2D;
|
||||||
|
private particles: any[] = [];
|
||||||
|
private animationFrameId: number = 0;
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
private readonly numParticles = 80;
|
||||||
|
private readonly maxDistance = 150;
|
||||||
|
private readonly particleSpeed = 0.8;
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
const canvas = this.canvasRef.nativeElement;
|
||||||
|
this.ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
this.resizeCanvas();
|
||||||
|
this.initParticles();
|
||||||
|
|
||||||
|
this.ngZone.runOutsideAngular(() => {
|
||||||
|
this.animate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize')
|
||||||
|
resizeCanvas(): void {
|
||||||
|
const canvas = this.canvasRef.nativeElement;
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initParticles(): void {
|
||||||
|
this.particles = [];
|
||||||
|
const canvas = this.canvasRef.nativeElement;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.numParticles; i++) {
|
||||||
|
this.particles.push({
|
||||||
|
x: Math.random() * canvas.width,
|
||||||
|
y: Math.random() * canvas.height,
|
||||||
|
vx: (Math.random() - 0.5) * this.particleSpeed,
|
||||||
|
vy: (Math.random() - 0.5) * this.particleSpeed,
|
||||||
|
radius: Math.random() * 1.5 + 0.5
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly animate = (): void => {
|
||||||
|
const canvas = this.canvasRef.nativeElement;
|
||||||
|
|
||||||
|
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.numParticles; i++) {
|
||||||
|
const p = this.particles[i];
|
||||||
|
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
|
||||||
|
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
|
||||||
|
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
||||||
|
this.ctx.fillStyle = 'rgba(120, 150, 170, 0.4)';
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
for (let j = i + 1; j < this.numParticles; j++) {
|
||||||
|
const p2 = this.particles[j];
|
||||||
|
|
||||||
|
const dx = p.x - p2.x;
|
||||||
|
const dy = p.y - p2.y;
|
||||||
|
const distance = Math.hypot(dx, dy);
|
||||||
|
|
||||||
|
if (distance < this.maxDistance) {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(p.x, p.y);
|
||||||
|
this.ctx.lineTo(p2.x, p2.y);
|
||||||
|
|
||||||
|
const opacity = (1 - (distance / this.maxDistance)) * 0.5;
|
||||||
|
this.ctx.strokeStyle = `rgba(120, 150, 170, ${opacity})`;
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.animate);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="canvas-container">
|
||||||
|
<canvas #renderCanvas></canvas>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
|
||||||
|
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
||||||
|
|
||||||
|
export interface RenderConfig {
|
||||||
|
mode: '2D' | '3D';
|
||||||
|
shaderLanguage?: number; //0 GLSL, 1 WGSL
|
||||||
|
initialViewSize: number;
|
||||||
|
vertexShader?: string;
|
||||||
|
fragmentShader?: string;
|
||||||
|
uniformNames?: string[];
|
||||||
|
uniformBufferNames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void;
|
||||||
|
|
||||||
|
export interface SceneEventData {
|
||||||
|
scene: Scene;
|
||||||
|
engine: WebGPUEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-babylon-canvas',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './babylon-canvas.component.html',
|
||||||
|
styleUrl: './babylon-canvas.component.scss',
|
||||||
|
})
|
||||||
|
export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||||
|
readonly ngZone = inject(NgZone);
|
||||||
|
|
||||||
|
@ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
@Input({ required: true }) config!: RenderConfig;
|
||||||
|
@Input() renderCallback?: RenderCallback;
|
||||||
|
|
||||||
|
@Output() sceneReady = new EventEmitter<SceneEventData>();
|
||||||
|
@Output() sceneResized = new EventEmitter<SceneEventData>();
|
||||||
|
|
||||||
|
private engine!: WebGPUEngine;
|
||||||
|
private scene!: Scene;
|
||||||
|
private shaderMaterial!: ShaderMaterial;
|
||||||
|
private camera!: Camera;
|
||||||
|
|
||||||
|
//Listener
|
||||||
|
private readonly resizeHandler = () => this.handleResize();
|
||||||
|
private readonly wheelHandler = (evt: WheelEvent) => evt.preventDefault();
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.initBabylon().then(() => { console.log("Engine initialized"); });
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
window.removeEventListener('resize', this.resizeHandler);
|
||||||
|
|
||||||
|
const canvas = this.canvasRef?.nativeElement;
|
||||||
|
if (canvas) {
|
||||||
|
canvas.removeEventListener('wheel', this.wheelHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.engine) {
|
||||||
|
this.engine.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initBabylon(): Promise<void> {
|
||||||
|
const canvas = this.canvasRef.nativeElement;
|
||||||
|
this.engine = new WebGPUEngine(canvas);
|
||||||
|
await this.engine.initAsync().then(() => {
|
||||||
|
this.scene = new Scene(this.engine);
|
||||||
|
this.setupCamera(canvas);
|
||||||
|
this.addListener(canvas);
|
||||||
|
this.createShaderMaterial();
|
||||||
|
this.createFullScreenRect();
|
||||||
|
this.sceneReady.emit({
|
||||||
|
scene: this.scene,
|
||||||
|
engine: this.engine
|
||||||
|
});
|
||||||
|
this.addRenderLoop(canvas);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addListener(canvas: HTMLCanvasElement) {
|
||||||
|
canvas.addEventListener('wheel', this.wheelHandler, {passive: false});
|
||||||
|
window.addEventListener('resize', this.resizeHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupCamera(canvas: HTMLCanvasElement) {
|
||||||
|
if (this.config.mode === '3D') {
|
||||||
|
this.setup3dCamera(canvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setup2dCamera(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setup2dCamera(canvas: HTMLCanvasElement) {
|
||||||
|
const cam = new ArcRotateCamera("Camera2D", -Math.PI / 2, Math.PI / 2, 10, Vector3.Zero(), this.scene);
|
||||||
|
cam.mode = Camera.ORTHOGRAPHIC_CAMERA;
|
||||||
|
|
||||||
|
const aspect = canvas.width / canvas.height;
|
||||||
|
const viewSize = this.config?.initialViewSize ?? 10;
|
||||||
|
cam.orthoLeft = -viewSize * aspect / 2;
|
||||||
|
cam.orthoRight = viewSize * aspect / 2;
|
||||||
|
cam.orthoTop = viewSize / 2;
|
||||||
|
cam.orthoBottom = -viewSize / 2;
|
||||||
|
|
||||||
|
this.camera = cam;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setup3dCamera(canvas: HTMLCanvasElement) {
|
||||||
|
const cam = new ArcRotateCamera("Camera", 0, Math.PI / 2, 4, Vector3.Zero(), this.scene);
|
||||||
|
cam.wheelPrecision = 100;
|
||||||
|
cam.pinchPrecision = 200;
|
||||||
|
cam.minZ = 0.1;
|
||||||
|
cam.maxZ = 100;
|
||||||
|
cam.lowerRadiusLimit = 1.5;
|
||||||
|
cam.upperRadiusLimit = 20;
|
||||||
|
cam.radius = this.config?.initialViewSize ?? 1;
|
||||||
|
cam.attachControl(canvas, true);
|
||||||
|
this.camera = cam;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createFullScreenRect() {
|
||||||
|
if (!this.config.vertexShader || !this.config.fragmentShader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
|
||||||
|
if (this.config.mode === '3D') {
|
||||||
|
plane.parent = this.camera;
|
||||||
|
plane.position.z = 1;
|
||||||
|
} else {
|
||||||
|
plane.lookAt(this.camera.position);
|
||||||
|
}
|
||||||
|
plane.alwaysSelectAsActiveMesh = true;
|
||||||
|
|
||||||
|
plane.material = this.shaderMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createShaderMaterial() {
|
||||||
|
if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shaderMaterial = new ShaderMaterial(
|
||||||
|
"shaderMaterial",
|
||||||
|
this.scene,
|
||||||
|
{
|
||||||
|
vertexSource: this.config.vertexShader,
|
||||||
|
fragmentSource: this.config.fragmentShader
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: ["position", "uv"],
|
||||||
|
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames],
|
||||||
|
uniformBuffers: this.config.uniformBufferNames ?? [],
|
||||||
|
shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.shaderMaterial.disableDepthWrite = true;
|
||||||
|
this.shaderMaterial.backFaceCulling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRenderLoop(canvas: HTMLCanvasElement) {
|
||||||
|
this.engine.runRenderLoop(() => {
|
||||||
|
|
||||||
|
// callback call to call specific uniforms
|
||||||
|
if (this.renderCallback) {
|
||||||
|
this.renderCallback(this.shaderMaterial, this.camera, canvas, this.scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
// default uniforms which maybe each scene has
|
||||||
|
if (this.shaderMaterial) {
|
||||||
|
this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
|
||||||
|
this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scene.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleResize(): void {
|
||||||
|
if (this.engine) {
|
||||||
|
this.engine.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.mode === '2D' && this.camera instanceof ArcRotateCamera) {
|
||||||
|
const viewSize = this.config?.initialViewSize ?? 10;
|
||||||
|
this.camera.orthoLeft = -viewSize / 2;
|
||||||
|
this.camera.orthoRight = viewSize / 2;
|
||||||
|
this.camera.orthoTop = viewSize / 2;
|
||||||
|
this.camera.orthoBottom = -viewSize / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sceneResized?.emit({
|
||||||
|
scene: this.scene,
|
||||||
|
engine: this.engine
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"APP": {
|
"APP": {
|
||||||
"TITLE": "Playground",
|
"TITLE": "Playground",
|
||||||
"COPYRIGHT": "Bilder urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!"
|
"COPYRIGHT": "Bilder und Sourcecode sind urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!"
|
||||||
},
|
},
|
||||||
"TOPBAR": {
|
"TOPBAR": {
|
||||||
"ABOUT": "Über mich",
|
"ABOUT": "Über mich",
|
||||||
@@ -25,21 +25,29 @@
|
|||||||
"CONTACT_ME": "Kontaktiere mich",
|
"CONTACT_ME": "Kontaktiere mich",
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
"SKILLS": "Fähigkeiten & Stack",
|
"SKILLS": "Fähigkeiten & Stack",
|
||||||
"PRIMARY": "Schwerpunkte",
|
"BACKEND_ARCH": "Backend & Architektur",
|
||||||
"TOOLSET": "Toolset",
|
"INFRA_CLOUD": "Infrastruktur & Cloud",
|
||||||
|
"SIM_ALGO": "Simulation & Algorithmen",
|
||||||
"EXPERIENCE": "Erfahrung",
|
"EXPERIENCE": "Erfahrung",
|
||||||
"PROJECTS": "Projekte",
|
"PROJECTS": "Projekte",
|
||||||
"EDUCATION": "Ausbildung"
|
"EDUCATION": "Ausbildung"
|
||||||
},
|
},
|
||||||
"SKILLS": {
|
"SKILLS": {
|
||||||
"JAVA": "Java 8/Java 21+",
|
"JAVA": "Java 8/21+",
|
||||||
"SPRING": "Spring Boot 2/3",
|
"SPRING": "Spring Boot 2/3",
|
||||||
"ANGULAR": "Angular 20+",
|
"ANGULAR": "Angular 19+",
|
||||||
"DOCKER": "Docker",
|
"DOCKER": "Docker",
|
||||||
"UNITY": "Unity",
|
"UNITY": "Unity",
|
||||||
"PYTHON": "Python",
|
"PYTHON": "Python",
|
||||||
"CSHARP": "C#",
|
"CSHARP": "C#",
|
||||||
"TYPESCRIPT": "TypeScript"
|
"TYPESCRIPT": "TypeScript",
|
||||||
|
"ARCH_MICROSERVICES": "Microservices",
|
||||||
|
"ARCH_CLOUD": "Cloud Architecture",
|
||||||
|
"ENG_ALGO": "Algorithm Design",
|
||||||
|
"ENG_SIM": "3D Simulation",
|
||||||
|
"ENG_GPU": "WebGPU / OpenGL / GLSL",
|
||||||
|
"ENG_PERF": "Performance Optimization",
|
||||||
|
"ENG_3D": "3D-Scanner Tech"
|
||||||
},
|
},
|
||||||
"TOOLS": {
|
"TOOLS": {
|
||||||
"GIT": "Git",
|
"GIT": "Git",
|
||||||
@@ -49,7 +57,8 @@
|
|||||||
"K8S": "Kubernetes / k3d",
|
"K8S": "Kubernetes / k3d",
|
||||||
"POSTGRES": "PostgreSQL",
|
"POSTGRES": "PostgreSQL",
|
||||||
"MONGO": "MongoDB",
|
"MONGO": "MongoDB",
|
||||||
"GRAFANA": "Grafana/Prometheus"
|
"GRAFANA": "Grafana/Prometheus",
|
||||||
|
"DOCKER": "Docker"
|
||||||
},
|
},
|
||||||
"XP": {
|
"XP": {
|
||||||
"COMPANY8": {
|
"COMPANY8": {
|
||||||
@@ -241,50 +250,75 @@
|
|||||||
"READ_MORE": "Mehr erfahren",
|
"READ_MORE": "Mehr erfahren",
|
||||||
"LINK_TO_PROJECT": "Zum Projekt",
|
"LINK_TO_PROJECT": "Zum Projekt",
|
||||||
"CLOSE": "Schließen",
|
"CLOSE": "Schließen",
|
||||||
|
"SECTION": {
|
||||||
|
"TECHNICAL": "Technische Herausforderungen",
|
||||||
|
"LEARNINGS": "Learnings & Soft Skills"
|
||||||
|
},
|
||||||
"PLAYGROUND": {
|
"PLAYGROUND": {
|
||||||
"TITLE": "Playground Website",
|
"TITLE": "Playground Portfolio",
|
||||||
"SHORT_DESCRIPTION": "Hier geht es um diese Webseite.",
|
"SHORT_DESCRIPTION": "Full-Stack Portfolio mit interaktiven Algorithmus-Visualisierungen.",
|
||||||
"INTRODUCTION": "Dieses Projekt ist hauptsächlich als eine Art 'Spielwiese' gestartet, daher der Name. Es ist geplant, die Seite mit der Zeit weiter auszubauen. Dabei werden hier neue Projekte auftauchen, oder ich werde die Seite an für sich weiter ausbauen, weil ich neue Sachen im Rahmen von Web Technologien ausprobieren möchte.",
|
"INTRODUCTION": "Diese Website dient als lebendiges Portfolio und Testumgebung für moderne Webtechnologien. Ziel ist es, komplexe Algorithmen und mathematische Konzepte (wie WebGPU-Simulationen oder Raymarching) anschaulich im Browser darzustellen.",
|
||||||
"BULLET_1": "Verwendung moderner Technologien und CI/CD-Pipelines (Angular 20+, Spring Boot 4, GitHub).",
|
"BULLET_1": "Entwicklung mit Angular 19+ und Material Design.",
|
||||||
"BULLET_2": "Präsentation persönlicher Projekte und kontinuierliche Verbesserung algorithmischer Fähigkeiten.",
|
"BULLET_2": "Implementierung performanter Visualisierungen (WebGPU, Shader, Canvas).",
|
||||||
"BULLET_3": "Vertiefung von JavaScript/TypeScript-, Angular- und Spring-Boot-Kenntnissen durch praktisches Arbeiten.",
|
"BULLET_3": "Automatisierte CI/CD-Pipelines und Containerisierung mit Docker.",
|
||||||
"BULLET_4": "Die Seite ist Open Source und auf GitHub verfügbar."
|
"BULLET_4": "Internationalisierung (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.",
|
||||||
|
"LEARNING_2": "Modernstes State-Management und reaktive Programmierung in Angular."
|
||||||
},
|
},
|
||||||
"TRIBBLE": {
|
"TRIBBLE": {
|
||||||
"TITLE": "Trouble with Tribble",
|
"TITLE": "Self-Hosted Infrastructure",
|
||||||
"SHORT_DESCRIPTION": "Ein Projekt, das die Einrichtung und Wartung eines Homeservers beschreibt, auf dem verschiedene Docker-Container für Self-Hosting-Dienste laufen.",
|
"SHORT_DESCRIPTION": "Home-Infrastruktur mit Docker, Traefik und sicherer VPN-Anbindung.",
|
||||||
"INTRODUCTION": "Dieses Projekt dokumentiert die Einrichtung eines persönlichen Homeservers mit dem Spitznamen \"Tribble\". Es umfasst die Installation von Ubuntu Server und die Containerisierung von Diensten wie Gitea für die Versionskontrolle, Jellyfin für das Mediastreaming und AdGuard Home für das Blockieren von Werbung im Netzwerk. Der Server ist über Traefik als Reverse-Proxy und Tailscale für eine sichere Netzwerkverbindung mit dem Internet verbunden, was das Self-Hosting der CI/CD-Pipeline dieser Website ermöglicht.",
|
"INTRODUCTION": "Dokumentation und Aufbau einer privaten Cloud-Infrastruktur. Fokus liegt auf Datensouveränität, Automatisierung und Sicherheit.",
|
||||||
"BULLET_1": "Self-Hosting verschiedener Dienste mit Docker.",
|
"BULLET_1": "Zentrale Verwaltung via Docker-Compose und Portainer.",
|
||||||
"BULLET_2": "CI/CD-Pipeline für die persönliche Website mit Gitea.",
|
"BULLET_2": "Automatisches SSL-Management und Reverse-Proxy mit Traefik.",
|
||||||
"BULLET_3": "Sicherer Fernzugriff mit Tailscale und Traefik.",
|
"BULLET_3": "Private Versionskontrolle (Gitea) und Medien-Streaming (Jellyfin).",
|
||||||
"BULLET_4": "Netzwerkweites Blockieren von Werbung mit AdGuard Home."
|
"BULLET_4": "Netzwerkweite Ad-Blocking und DNS-Kontrolle via AdGuard Home.",
|
||||||
|
"CHALLENGE_1": "Konfiguration sicherer Netzwerkschichten und Firewall-Regeln für Remote-Zugriff.",
|
||||||
|
"CHALLENGE_2": "Automatisierung von Backups und Recovery-Strategien für containerisierte Daten.",
|
||||||
|
"LEARNING_1": "Tiefes Verständnis für moderne Netzwerkprotokolle und IT-Sicherheit.",
|
||||||
|
"LEARNING_2": "Effizientes Ressourcen-Management auf limitierten Server-Systemen."
|
||||||
},
|
},
|
||||||
"EL_MUCHO": {
|
"EL_MUCHO": {
|
||||||
"TITLE": "El Mucho",
|
"TITLE": "El Mucho (Steam Release)",
|
||||||
"SHORT_DESCRIPTION": "Hier geht es um mein ersten Spiel auf Steam.",
|
"SHORT_DESCRIPTION": "Rundenbasiertes Taktik-RPG, veröffentlicht auf Steam.",
|
||||||
"INTRODUCTION": "El Mucho ist ein rundenbasiertes taktisches RPG in einer fiktiven Welt namens Liberika. Es ist angelehnt an alte Klassiker wie Langrisser aka Warsong. In El Mucho geht es darum, die Welt gegen die Angriffe der fiesen Monster zu verteidigen.",
|
"INTRODUCTION": "Ein kommerzielles Spieleprojekt, das von der ersten Idee bis zum weltweiten Release auf Steam eigenverantwortlich umgesetzt wurde. Ein taktisches RPG, das klassische Gameplay-Elemente mit modernen Systemen verbindet.",
|
||||||
"BULLET_1": "Veröffentlichung eines Spiels auf Steam und Integration der Steam-API.",
|
"BULLET_1": "Komplette Engine-Entwicklung in Unity (C#).",
|
||||||
"BULLET_2": "Konzeption, Planung und vollständige Entwicklung eines eigenen Spiels.",
|
"BULLET_2": "Integration von Steamworks-Funktionen (Achievements, Cloud Saves).",
|
||||||
"BULLET_3": "Implementierung komplexer Algorithmen wie einer eigenen A*-Pfadfindungslogik und Spiel-KI.",
|
"BULLET_3": "Entwicklung einer eigenen taktischen KI und Pfadfindungs-Logik.",
|
||||||
"BULLET_4": "Das Spiel wurde mit Unity und C# entwickelt."
|
"BULLET_4": "Management des gesamten Asset-Pipelines und Sound-Designs.",
|
||||||
|
"CHALLENGE_1": "Implementierung eines robusten rundenbasierten Systems mit komplexen Abhängigkeiten.",
|
||||||
|
"CHALLENGE_2": "Performance-Optimierung für eine flüssige Darstellung auf verschiedenen Hardware-Profilen.",
|
||||||
|
"CHALLENGE_3": "Umgang mit den strengen Zertifizierungs-Anforderungen von Steam.",
|
||||||
|
"LEARNING_1": "Durchhaltevermögen und Fokus über einen mehrjährigen Entwicklungszyklus.",
|
||||||
|
"LEARNING_2": "Vermarktung und Community-Management für ein digitales Produkt."
|
||||||
},
|
},
|
||||||
"GAME_JAMS": {
|
"GAME_JAMS": {
|
||||||
"TITLE": "Game Jams",
|
"TITLE": "Rapid Prototyping & Game Jams",
|
||||||
"SHORT_DESCRIPTION": "Hier geht es meine Teilnahme an mehreren Game Jams.",
|
"SHORT_DESCRIPTION": "Sammlung innovativer Spielkonzepte, entstanden in unter 48 Stunden.",
|
||||||
"INTRODUCTION": "Da ich mich für die Entwicklung von Spielen interessiert, sind Game Jams für mich optimal, um fokussiert an neuen Ideen zu arbeiten und dabei Prototypen zu entwickeln, um zu sehen, ob Spielideen funktionieren oder nicht. In den letzten Jahren habe ich an einigen Game Jams teilgenommen und fasse das hier zusammen.",
|
"INTRODUCTION": "Teilnahme an nationalen Wettbewerben (z.B. Beansjam). Hier geht es darum, unter extremem Zeitdruck funktionale und spaßige Prototypen zu erschaffen.",
|
||||||
"BULLET_1": "Planung eines realistischen Projektumfangs mit einem Team, der innerhalb von 48 Stunden umsetzbar ist.",
|
"BULLET_1": "Fokus auf 'Core Game Loop' und schnelles Feedback.",
|
||||||
"BULLET_2": "Lernen, fokussiert und effizient unter strengen Zeitvorgaben zu arbeiten.",
|
"BULLET_2": "Kollaborative Entwicklung in kleinen, agilen Teams.",
|
||||||
"BULLET_3": "Die Freude zu erleben, in kurzer Zeit ein spielbares Projekt zu erstellen und andere damit spielen zu sehen.",
|
"BULLET_3": "Effektives Zeitmanagement und Scope-Kontrolle.",
|
||||||
"BULLET_4": "Alle Projekte sind auf Itch.io verfügbar und spielbar."
|
"BULLET_4": "Veröffentlichung und Iteration basierend auf Community-Votings.",
|
||||||
|
"CHALLENGE_1": "Reduzierung komplexer Ideen auf ein in 48h umsetzbares Minimum Viable Product (MVP).",
|
||||||
|
"CHALLENGE_2": "Schnelle Fehlerdiagnose und Bugfixing unter massivem Zeitdruck.",
|
||||||
|
"LEARNING_1": "Radikale Priorisierung von Features ('Kill your darlings').",
|
||||||
|
"LEARNING_2": "Effektive Kommunikation und Entscheidungsfindung im Team-Stress."
|
||||||
},
|
},
|
||||||
"DIPLOMA": {
|
"DIPLOMA": {
|
||||||
"TITLE": "Diplomarbeit",
|
"TITLE": "Wissenschaftliche Diplomarbeit",
|
||||||
"SHORT_DESCRIPTION": "Kollisionserkennung und Behandlung von komplexen Kleidungsstücken.",
|
"SHORT_DESCRIPTION": "Echtzeit-Kollisionserkennung für komplexe, flexible 3D-Objekte.",
|
||||||
"INTRODUCTION": "Die Diplomarbeit handelt von der Erkennung und der Behandlung von Kollisionen zwischen, sowie innerhalb, einzelnen Kleidungsstücken in Echtzeit. Das ist gerade aufgrund der Flexibilität von Stoffen und deren unterschiedlichen Eigenschaften besonders herausfordernd.",
|
"INTRODUCTION": "Forschungsarbeit im Bereich Computergraphik. Entwicklung eines Algorithmus zur physikalisch korrekten Simulation von Stoffen und Kleidung in Echtzeit.",
|
||||||
"BULLET_1": "Echtzeit behandlung von Kollisionserkennung und Behandlung.",
|
"BULLET_1": "Mathematische Modellierung von Mass-Spring-Systemen.",
|
||||||
"BULLET_2": "Verstehen und Einschätzen von wissenschaftlichen Arbeiten.",
|
"BULLET_2": "Low-Level Programmierung mit Java.",
|
||||||
"BULLET_3": "Adaption und Weiterentwicklung von vorausgegangenen Forschungsarbeiten.",
|
"BULLET_3": "Optimierung durch räumliche Datenstrukturen (AABB Trees, Bounding Spheres).",
|
||||||
"BULLET_4": "Die Arbeit wurde mit C++ und OpenGL geschrieben und in die Vidya-Software integriert."
|
"BULLET_4": "Wissenschaftliche Evaluation der Simulations-Präzision.",
|
||||||
|
"CHALLENGE_1": "Behandlung von 'Self-Collisions' bei hochauflösenden Meshes ohne Performance-Einbruch.",
|
||||||
|
"CHALLENGE_2": "Mathematische Stabilisierung der Integrationsverfahren bei hohen Krafteinwirkungen.",
|
||||||
|
"LEARNING_1": "Transfer von theoretischen Forschungsarbeiten in produktiven, performanten Code.",
|
||||||
|
"LEARNING_2": "Präzises Arbeiten und Dokumentation nach wissenschaftlichen Standards."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"IMPRINT": {
|
"IMPRINT": {
|
||||||
@@ -362,6 +396,105 @@
|
|||||||
"DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat."
|
"DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"LABYRINTH": {
|
||||||
|
"TITLE": "Labyrinth-Erzeugung",
|
||||||
|
"PRIM": "Erzeuge Prim's Labyrinth",
|
||||||
|
"KRUSKAL": "Erzeuge Kruskal's Labyrinth",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Algorithmen",
|
||||||
|
"PRIM_EXPLANATION": "startet an einem zufälligen Punkt und erweitert das Labyrinth, indem er immer eine zufällige benachbarte Wand zu einer bereits besuchten Zelle auswählt und diese öffnet. Vorteil: Erzeugt sehr gleichmäßige, natürlich wirkende Labyrinthe mit vielen kurzen Sackgassen. Visuell wirkt es wie ein organisches Wachstum von einem Zentrum aus.",
|
||||||
|
"KRUSKAL_EXPLANATION": "betrachtet alle Wände des Gitters als potenzielle Wege. Er wählt zufällig Wände aus und öffnet sie nur dann, wenn die beiden angrenzenden Zellen noch nicht miteinander verbunden sind (verhindert Kreise). Vorteil: Erzeugt ein sehr komplexes Labyrinth mit vielen langen, verwinkelten Pfaden. Visuell ist es spannend, da das Labyrinth an vielen Stellen gleichzeitig entsteht und am Ende zu einem Ganzen verschmilzt.",
|
||||||
|
"DISCLAIMER": "Beide Algorithmen basieren auf dem Prinzip des 'Minimal Spanning Tree' (Minimaler Spannbaum). Das bedeutet für dein Labyrinth:",
|
||||||
|
"DISCLAIMER_1": "Perfektes Labyrinth: Es gibt keine geschlossenen Kreise (Loops) – jeder Punkt ist erreichbar, aber es gibt immer nur genau einen Weg zwischen zwei Punkten.",
|
||||||
|
"DISCLAIMER_2": "Erreichbarkeit: Da es ein Spannbaum ist, wird garantiert jede Zelle des Gitters Teil des Labyrinths, es gibt keine isolierten Bereiche.",
|
||||||
|
"DISCLAIMER_3": "Zufälligkeit: Durch die Gewichtung der Kanten mit Zufallswerten entstehen bei jedem Durchlauf völlig neue, einzigartige Strukturen.",
|
||||||
|
"DISCLAIMER_4": "Anwendung: Solche Labyrinthe sind die perfekte Testumgebung für Pfadfindungsalgorithmen wie Dijkstra oder A*."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fraktale",
|
||||||
|
"ALGORITHM": "Algorithmen",
|
||||||
|
"RESET": "Reset",
|
||||||
|
"COLOR_SCHEME": "Farbschema",
|
||||||
|
"MAX_ITERATION": "Maximale Auflösung",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Mathematische Kunst",
|
||||||
|
"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.",
|
||||||
|
"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.",
|
||||||
|
"DISCLAIMER_3": "Komplexe Zahlen: Die Berechnung findet nicht in einem normalen Koordinatensystem statt, sondern in der komplexen Ebene mit realen und imaginären Anteilen.",
|
||||||
|
"DISCLAIMER_4": "Rechenintensität: Da für jeden Pixel hunderte Berechnungen durchgeführt werden, sind Fraktale ein klassischer Benchmark für die Performance von Grafikprozessoren (GPUs).",
|
||||||
|
"DISCLAIMER_BOTTOM": "Grafikkarten rechnen standardmäßig mit 32-Bit Fließkommazahlen (float). Diese haben nur etwa 7 Stellen Genauigkeit. Bei sehr hohem Zoom (> 100.000) ist der Unterschied zwischen zwei Pixeln so winzig, dass die Grafikkarte ihn nicht mehr darstellen kann. Sie berechnet für 10 Pixel nebeneinander exakt denselben Wert -> Du siehst Blöcke oder Treppenstufen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FRACTAL3D": {
|
||||||
|
"TITLE": "3D Fraktale",
|
||||||
|
"ALGORITHM": "Algorithmen",
|
||||||
|
"MANDELBULB": "Mandelbulb",
|
||||||
|
"MANDELBOX": "Mandelbox",
|
||||||
|
"JULIA": "Julia",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "3D Fraktale Welten",
|
||||||
|
"MANDELBULB_EXPLANATION": "gilt als der 'Heilige Gral' der 3D-Fraktale. Da komplexe Zahlen nur zweidimensional sind, nutzt dieses Fraktal sphärische Koordinaten und hohe Potenzen (meist v^8 + c), um die Mandelbrot-Menge in den Raum zu projizieren. Vorteil: Es entsteht eine organische, extrem detaillierte Struktur, die an Pflanzen, Korallen oder außerirdische Landschaften erinnert.",
|
||||||
|
"MANDELBOX_EXPLANATION": "basiert nicht auf glatten Kurven, sondern auf geometrischem 'Falten' und Skalieren (Box-Folding & Sphere-Folding). Der Raum wird wie Papier immer wieder gefaltet und gespiegelt. Vorteil: Erzeugt streng geometrische, mechanisch wirkende Strukturen, die wie endlose futuristische Städte, der Borg-Würfel oder komplexe Sci-Fi-Architektur aussehen.",
|
||||||
|
"JULIA_EXPLANATION": "ist das 3D-Pendant zur 2D-Julia-Menge. Während der Mandelbulb eine 'Karte' aller Fraktale ist, fixiert man hier den Parameter 'c' und variiert den Startpunkt zudem variiert es mit der Zeit. Vorteil: Anders als der massive Mandelbulb sind Julia-Bulbs oft hohle, komplexe Tunnelsysteme oder blasenartige Strukturen, die sich perfekt eignen, um mit der Kamera hindurchzufliegen.",
|
||||||
|
"DISCLAIMER": "Diese Visualisierung nutzt eine Technik namens 'Raymarching' (Sphere Tracing). Das bedeutet:",
|
||||||
|
"DISCLAIMER_1": "Keine Polygone: Es gibt keine Dreiecke oder Gitter. Die Form wird rein mathematisch für jeden Pixel in Echtzeit berechnet.",
|
||||||
|
"DISCLAIMER_2": "Distance Estimation: Der Algorithmus 'tastet' sich mit Lichtstrahlen voran, indem er berechnet, wie weit das nächste Objekt entfernt ist, ohne es sofort zu berühren.",
|
||||||
|
"DISCLAIMER_3": "Unendliche Details: Da die Oberfläche mathematisch definiert ist, verpixelt sie nicht beim Zoom – es erscheinen immer neue Strukturen.",
|
||||||
|
"DISCLAIMER_4": "Licht & Schatten: Um die Tiefe sichtbar zu machen, werden Lichtreflexionen und Schatten (Ambient Occlusion) basierend auf der Krümmung der Formel simuliert."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PENDULUM": {
|
||||||
|
"TITLE": "Doppel-Pendel",
|
||||||
|
"TRAIL_DECAY_TIME": "Spurlänge",
|
||||||
|
"DAMPING": "Dämpfung",
|
||||||
|
"ATTRACTION": "Anziehungskraft",
|
||||||
|
"L1_LENGTH": "Länge L1",
|
||||||
|
"L2_LENGTH": "Länge L2",
|
||||||
|
"M1_MASS": "Masse M1",
|
||||||
|
"M2_MASS": "Masse M2",
|
||||||
|
"POKE_M1": "Schubse M1",
|
||||||
|
"POKE_M2": "Schubse M2",
|
||||||
|
"RESET": "Neustarten",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Chaostheorie: Das Doppelpendel",
|
||||||
|
"EXPLANATION": "Das Doppelpendel ist eines der bekanntesten und faszinierendsten Beispiele der Physik für ein dynamisches System, das 'deterministisches Chaos' erzeugt. Es besteht schlicht aus einem einfachen Pendel, an dessen unterem Ende ein zweites Pendel befestigt ist. Obwohl die zugrundeliegenden Bewegungsgesetze der klassischen Mechanik streng mathematisch definiert sind, ist das Verhalten des Doppelpendels auf lange Sicht absolut unvorhersehbar. Es gilt in der Physik als das klassische Vorzeigeobjekt für den sogenannten Schmetterlingseffekt.",
|
||||||
|
"DISCLAIMER": "Diese WebGPU-Simulation berechnet die Bewegungs- und Beschleunigungsgleichungen des Pendels 60-mal pro Sekunde in Echtzeit. Dabei gelten folgende Besonderheiten:",
|
||||||
|
"DISCLAIMER_1": "Extreme Sensitivität: Winzigste Änderungen in den Startbedingungen (z.B. ein Tausendstel Grad Abweichung im Startwinkel oder bei der Masse) führen schon nach kurzer Zeit zu einer völlig anderen, chaotischen Flugbahn.",
|
||||||
|
"DISCLAIMER_2": "Deterministisches Chaos: Die Bewegung wirkt zwar völlig wild und zufällig, ist es aber nicht. Startest du die Simulation mit exakt denselben Werten neu, wird das Pendel zu 100 % denselben Weg fliegen.",
|
||||||
|
"DISCLAIMER_3": "Numerische Integration: Da Computer Zeit nicht stufenlos, sondern in winzigen Schritten (dt) berechnen, entstehen bei jedem Frame winzige mathematische Rundungsfehler. Diese summieren sich auf und beeinflussen das Chaos zusätzlich.",
|
||||||
|
"DISCLAIMER_4": "Energieerhaltung & Reibung: In einem perfekten physikalischen System ohne Widerstand würde das Pendel ewig weiterschwingen. Für eine natürliche Optik nutzt der Algorithmus einen künstlichen Dämpfungsfaktor, der Luftreibung simuliert und das System irgendwann beruhigt.",
|
||||||
|
"DISCLAIMER_BOTTOM": "HINWEIS: Wenn zuviele Impulse in das System gegeben werden, wird die Simulation instabil. Dann hängt das Pendel nur noch runter und es muss neu gestartet werden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CLOTH": {
|
||||||
|
"TITLE": "Stoffsimulation",
|
||||||
|
"WIND_ON": "Wind Einschalten",
|
||||||
|
"WIND_OFF": "Wind Ausschalten",
|
||||||
|
"OUTLINE_ON": "Mesh anzeigen",
|
||||||
|
"OUTLINE_OFF": "Mesh ausschalten",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Echtzeit-Stoffsimulation auf der GPU",
|
||||||
|
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Stoffsimulation",
|
||||||
|
"XPBD_EXPLANATION_TITLE": "XPBD (Extended Position-Based Dynamics)",
|
||||||
|
"GPU_PARALLELIZATION_EXPLANATION_TITLE": "GPU Parallelisierung",
|
||||||
|
"DATA_STRUCTURES_EXPLANATION_TITLE": "Datenstrukturen",
|
||||||
|
"CLOTH_SIMULATION_EXPLANATION": "Stoffsimulationen modellieren Textilien meist als ein Gitter aus Massepunkten (Vertices), die durch unsichtbare Verbindungen zusammengehalten werden. Ziel ist es, physikalische Einflüsse wie Schwerkraft, Wind und Kollisionen in Echtzeit darzustellen, ohne dass das Material zerreißt oder sich unnatürlich wie Gummi dehnt.",
|
||||||
|
"XPBD_EXPLANATION": "XPBD (Extended Position-Based Dynamics) ist ein moderner Algorithmus, der statt Beschleunigungen direkt die Positionen der Punkte manipuliert, um Abstandsbedingungen (Constraints) zu erfüllen. Das 'Extended' bedeutet, dass echte physikalische Steifigkeit unabhängig von der Framerate simuliert wird. Vorteil: Absolut stabil, explodiert nicht und topologische Änderungen (wie das Zerschneiden von Stoff) sind trivial. Nachteil: Es ist ein iteratives Näherungsverfahren und physikalisch minimal weniger akkurat als komplexe Matrix-Löser.",
|
||||||
|
"GPU_PARALLELIZATION_EXPLANATION": "Um zehntausende Punkte parallel auf der Grafikkarte zu berechnen, muss man 'Race Conditions' verhindern – also dass zwei Rechenkerne gleichzeitig denselben Knotenpunkt verschieben. Die Lösung nennt sich 'Independent Sets' (oder Graph Coloring): Die Verbindungen werden in isolierte Gruppen (z. B. 4 Phasen bei einem Gitter) unterteilt, in denen sich kein einziger Punkt überschneidet. So kann die GPU jede Gruppe blind und mit maximaler Geschwindigkeit abarbeiten.",
|
||||||
|
"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_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."
|
||||||
|
}
|
||||||
|
},
|
||||||
"ALGORITHM": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithmen",
|
"TITLE": "Algorithmen",
|
||||||
"PATHFINDING": {
|
"PATHFINDING": {
|
||||||
@@ -376,6 +509,26 @@
|
|||||||
"TITLE": "Conway's Game of Life",
|
"TITLE": "Conway's Game of Life",
|
||||||
"DESCRIPTION": "Das 'Spiel des Lebens' ist ein vom Mathematiker John Horton Conway 1970 entworfenes Spiel."
|
"DESCRIPTION": "Das 'Spiel des Lebens' ist ein vom Mathematiker John Horton Conway 1970 entworfenes Spiel."
|
||||||
},
|
},
|
||||||
|
"LABYRINTH": {
|
||||||
|
"TITLE": "Labyrinth-Erzeugung",
|
||||||
|
"DESCRIPTION": "Visualisierung verschiedener Laybrinth-Erzeugungs-Algorithmen."
|
||||||
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fraktale",
|
||||||
|
"DESCRIPTION": "Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)."
|
||||||
|
},
|
||||||
|
"FRACTAL3D": {
|
||||||
|
"TITLE": "Fraktale 3D",
|
||||||
|
"DESCRIPTION": "3D-Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)."
|
||||||
|
},
|
||||||
|
"PENDULUM": {
|
||||||
|
"TITLE": "Doppel-Pendel",
|
||||||
|
"DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU."
|
||||||
|
},
|
||||||
|
"CLOTH": {
|
||||||
|
"TITLE": "Stoffsimulation",
|
||||||
|
"DESCRIPTION": "Simulation on Stoff mit WebGPU."
|
||||||
|
},
|
||||||
"NOTE": "HINWEIS",
|
"NOTE": "HINWEIS",
|
||||||
"GRID_HEIGHT": "Höhe",
|
"GRID_HEIGHT": "Höhe",
|
||||||
"GRID_WIDTH": "Beite"
|
"GRID_WIDTH": "Beite"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"APP": {
|
"APP": {
|
||||||
"TITLE": "Playground",
|
"TITLE": "Playground",
|
||||||
"COPYRIGHT": "Images protected by copyright, no use without permission!"
|
"COPYRIGHT": "Images and code protected by copyright, no use without permission!"
|
||||||
},
|
},
|
||||||
"TOPBAR": {
|
"TOPBAR": {
|
||||||
"ABOUT": "About me",
|
"ABOUT": "About me",
|
||||||
@@ -25,21 +25,29 @@
|
|||||||
"CONTACT_ME": "Contact me",
|
"CONTACT_ME": "Contact me",
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
"SKILLS": "Skills & Stack",
|
"SKILLS": "Skills & Stack",
|
||||||
"PRIMARY": "Core",
|
"BACKEND_ARCH": "Backend & Architecture",
|
||||||
"TOOLSET": "Toolset",
|
"INFRA_CLOUD": "Infrastructure & Cloud",
|
||||||
|
"SIM_ALGO": "Simulation & Algorithms",
|
||||||
"EXPERIENCE": "Experience",
|
"EXPERIENCE": "Experience",
|
||||||
"PROJECTS": "Projects",
|
"PROJECTS": "Projects",
|
||||||
"EDUCATION": "Education"
|
"EDUCATION": "Education"
|
||||||
},
|
},
|
||||||
"SKILLS": {
|
"SKILLS": {
|
||||||
"JAVA": "Java 8/Java 21+",
|
"JAVA": "Java 8/21+",
|
||||||
"SPRING": "Spring Boot 2/3",
|
"SPRING": "Spring Boot 2/3",
|
||||||
"ANGULAR": "Angular 20+",
|
"ANGULAR": "Angular 19+",
|
||||||
"DOCKER": "Docker",
|
"DOCKER": "Docker",
|
||||||
"UNITY": "Unity",
|
"UNITY": "Unity",
|
||||||
"PYTHON": "Python",
|
"PYTHON": "Python",
|
||||||
"CSHARP": "C#",
|
"CSHARP": "C#",
|
||||||
"TYPESCRIPT": "TypeScript"
|
"TYPESCRIPT": "TypeScript",
|
||||||
|
"ARCH_MICROSERVICES": "Microservices",
|
||||||
|
"ARCH_CLOUD": "Cloud Architecture",
|
||||||
|
"ENG_ALGO": "Algorithm Design",
|
||||||
|
"ENG_SIM": "3D Simulation",
|
||||||
|
"ENG_GPU": "WebGPU / OpenGL / GLSL",
|
||||||
|
"ENG_PERF": "Performance Optimization",
|
||||||
|
"ENG_3D": "3D-Scanner Tech"
|
||||||
},
|
},
|
||||||
"TOOLS": {
|
"TOOLS": {
|
||||||
"GIT": "Git",
|
"GIT": "Git",
|
||||||
@@ -49,7 +57,8 @@
|
|||||||
"K8S": "Kubernetes / k3d",
|
"K8S": "Kubernetes / k3d",
|
||||||
"POSTGRES": "PostgreSQL",
|
"POSTGRES": "PostgreSQL",
|
||||||
"MONGO": "MongoDB",
|
"MONGO": "MongoDB",
|
||||||
"GRAFANA": "Grafana/Prometheus"
|
"GRAFANA": "Grafana/Prometheus",
|
||||||
|
"DOCKER": "Docker"
|
||||||
},
|
},
|
||||||
"XP": {
|
"XP": {
|
||||||
"COMPANY8": {
|
"COMPANY8": {
|
||||||
@@ -138,7 +147,7 @@
|
|||||||
"TIME": "Jul. 2002 – Jun. 2005",
|
"TIME": "Jul. 2002 – Jun. 2005",
|
||||||
"HIGHLIGHTS": {
|
"HIGHLIGHTS": {
|
||||||
"P1": "Development in PERL, PHP and ASP.",
|
"P1": "Development in PERL, PHP and ASP.",
|
||||||
"P2": "Porting, maintenance and reengineering of existing software.",
|
"P2": "Portierung, Wartung und Reengineering von bestender Software.",
|
||||||
"P3": "Regular performance of system tests and quality controls, as well as their documentation."
|
"P3": "Regular performance of system tests and quality controls, as well as their documentation."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +198,7 @@
|
|||||||
},
|
},
|
||||||
"TRIBBLE": {
|
"TRIBBLE": {
|
||||||
"TITLE": "Homeserver 'Tribble'",
|
"TITLE": "Homeserver 'Tribble'",
|
||||||
"DESCRIPTION": "This project is about setting up and maintaining my own homeserver. It runs several Docker containers like Gitea, Jellyfin, and more. It's a great learning experience in self-hosting and system administration.",
|
"DESCRIPTION": "This project is about setting up and maintaining my own homeserver. It runs several Docker containers like Gitea, Jellyfin and more. It's a great learning experience in self-hosting and system administration.",
|
||||||
"LINK_INTERNAL": "Project details",
|
"LINK_INTERNAL": "Project details",
|
||||||
"HIGHLIGHTS": {
|
"HIGHLIGHTS": {
|
||||||
"P1": "Self-hosting of various services using Docker.",
|
"P1": "Self-hosting of various services using Docker.",
|
||||||
@@ -241,50 +250,75 @@
|
|||||||
"READ_MORE": "Read More",
|
"READ_MORE": "Read More",
|
||||||
"LINK_TO_PROJECT": "To the project",
|
"LINK_TO_PROJECT": "To the project",
|
||||||
"CLOSE": "Close",
|
"CLOSE": "Close",
|
||||||
|
"SECTION": {
|
||||||
|
"TECHNICAL": "Technical Challenges",
|
||||||
|
"LEARNINGS": "Learnings & Soft Skills"
|
||||||
|
},
|
||||||
"PLAYGROUND": {
|
"PLAYGROUND": {
|
||||||
"TITLE": "Playground Website",
|
"TITLE": "Playground Portfolio",
|
||||||
"SHORT_DESCRIPTION": "This is about this website.",
|
"SHORT_DESCRIPTION": "Full-stack portfolio with interactive algorithm visualizations.",
|
||||||
"INTRODUCTION": "This project was mainly started as a kind of “playground”, hence the name. The plan is to expand the site over time. New projects will appear here, or I will continue to expand the site itself because I want to try out new things in the field of web technologies.",
|
"INTRODUCTION": "This website serves as a living portfolio and testing ground for modern web technologies. The goal is to clearly represent complex algorithms and mathematical concepts (such as WebGPU simulations or Raymarching) directly in the browser.",
|
||||||
"BULLET_1": "Using modern technologies and CI/CD pipelines (Angular 20+, Spring Boot 4, GitHub).",
|
"BULLET_1": "Development with Angular 19+ and Material Design.",
|
||||||
"BULLET_2": "Showcasing personal projects and improving algorithmic skills over time.",
|
"BULLET_2": "Implementation of performant visualizations (WebGPU, Shader, Canvas).",
|
||||||
"BULLET_3": "Deepening knowledge in JavaScript/TypeScript, Angular, Spring Boot and related technologies through hands-on practice.",
|
"BULLET_3": "Automated CI/CD pipelines and containerization with Docker.",
|
||||||
"BULLET_4": "The site is open source and available on GitHub."
|
"BULLET_4": "Internationalization (i18n) for global reach.",
|
||||||
|
"CHALLENGE_1": "Optimizing render performance for complex 3D fractals in real-time.",
|
||||||
|
"CHALLENGE_2": "Architecting a scalable and maintainable frontend structure for diverse sub-projects.",
|
||||||
|
"LEARNING_1": "Effective UI/UX design for complex data-driven visualizations.",
|
||||||
|
"LEARNING_2": "Advanced state management and reactive programming in Angular."
|
||||||
},
|
},
|
||||||
"TRIBBLE": {
|
"TRIBBLE": {
|
||||||
"TITLE": "Trouble with Tribble",
|
"TITLE": "Self-Hosted Infrastructure",
|
||||||
"SHORT_DESCRIPTION": "A project detailing the setup and maintenance of a home server running various Docker containers for self-hosting services.",
|
"SHORT_DESCRIPTION": "Home infrastructure with Docker, Traefik, and secure VPN connectivity.",
|
||||||
"INTRODUCTION": "This project documents the journey of setting up a personal home server, nicknamed \"Tribble\". It involves installing Ubuntu Server and containerizing services like Gitea for version control, Jellyfin for media streaming, and AdGuard Home for network-wide ad-blocking. The server is connected via Traefik as a reverse proxy and Tailscale for secure networking, enabling the self-hosted CI/CD pipeline for this website.",
|
"INTRODUCTION": "Documentation and construction of a private cloud infrastructure. Focus is on data sovereignty, automation, and security.",
|
||||||
"BULLET_1": "Self-hosting of various services using Docker.",
|
"BULLET_1": "Central management via Docker-Compose and Portainer.",
|
||||||
"BULLET_2": "CI/CD pipeline for the personal website using Gitea.",
|
"BULLET_2": "Automatic SSL management and reverse proxy with Traefik.",
|
||||||
"BULLET_3": "Secure remote access with Tailscale and Traefik.",
|
"BULLET_3": "Private version control (Gitea) and media streaming (Jellyfin).",
|
||||||
"BULLET_4": "Network-wide ad-blocking with AdGuard Home."
|
"BULLET_4": "Network-wide ad-blocking and DNS control via AdGuard Home.",
|
||||||
|
"CHALLENGE_1": "Configuring secure network layers and firewall rules for remote access.",
|
||||||
|
"CHALLENGE_2": "Automating backups and recovery strategies for containerized data.",
|
||||||
|
"LEARNING_1": "Deep understanding of modern network protocols and IT security.",
|
||||||
|
"LEARNING_2": "Efficient resource management on limited server systems."
|
||||||
},
|
},
|
||||||
"EL_MUCHO": {
|
"EL_MUCHO": {
|
||||||
"TITLE": "El Mucho",
|
"TITLE": "El Mucho (Steam Release)",
|
||||||
"SHORT_DESCRIPTION": "This is about my first game on steam.",
|
"SHORT_DESCRIPTION": "Turn-based tactical RPG, published on Steam.",
|
||||||
"INTRODUCTION": "El Mucho is a turn-based tactical RPG set in a fictional world called Liberika. It is inspired by old classics such as Langrisser, also known as Warsong. El Mucho is about defending the world against attacks from nasty monsters.",
|
"INTRODUCTION": "A commercial game project that was independently implemented from the initial idea to the worldwide release on Steam. A tactical RPG that combines classic gameplay elements with modern systems.",
|
||||||
"BULLET_1": "Publishing a game on Steam and integrating the Steam API.",
|
"BULLET_1": "Complete engine development in Unity (C#).",
|
||||||
"BULLET_2": "Designing, planning and developing a complete game from scratch.",
|
"BULLET_2": "Integration of Steamworks features (Achievements, Cloud Saves).",
|
||||||
"BULLET_3": "Implementing complex algorithms, including a custom A* pathfinding system and game AI logic.",
|
"BULLET_3": "Development of a custom tactical AI and pathfinding logic.",
|
||||||
"BULLET_4": "The game was developed with Unity and C#."
|
"BULLET_4": "Management of the entire asset pipeline and sound design.",
|
||||||
|
"CHALLENGE_1": "Implementing a robust turn-based system with complex dependencies.",
|
||||||
|
"CHALLENGE_2": "Performance optimization for a smooth experience across various hardware profiles.",
|
||||||
|
"CHALLENGE_3": "Handling Steam's strict certification requirements.",
|
||||||
|
"LEARNING_1": "Perseverance and focus over a multi-year development cycle.",
|
||||||
|
"LEARNING_2": "Marketing and community management for a digital product."
|
||||||
},
|
},
|
||||||
"GAME_JAMS": {
|
"GAME_JAMS": {
|
||||||
"TITLE": "Game Jams",
|
"TITLE": "Rapid Prototyping & Game Jams",
|
||||||
"SHORT_DESCRIPTION": "This is about my participation at several game jams.",
|
"SHORT_DESCRIPTION": "Collection of innovative game concepts, created in under 48 hours.",
|
||||||
"INTRODUCTION": "Since I am interested in game development, game jams are ideal for me to focus on new ideas and develop prototypes to see whether game ideas work or not. I have participated in several game jams over the past few years and summarise my experiences here.",
|
"INTRODUCTION": "Participation in national competitions (e.g. BeansJam). The focus is on creating functional and fun prototypes under extreme time pressure.",
|
||||||
"BULLET_1": "Planning a realistic project scope with a team that can be built within 48 hours.",
|
"BULLET_1": "Focus on 'Core Game Loop' and fast feedback.",
|
||||||
"BULLET_2": "Learning to stay focused and work effectively under strict time constraints.",
|
"BULLET_2": "Collaborative development in small, agile teams.",
|
||||||
"BULLET_3": "Experiencing the joy of creating a playable game in a short timeframe and seeing others enjoy it.",
|
"BULLET_3": "Effective time management and scope control.",
|
||||||
"BULLET_4": "All projects are available and playable on Itch.io."
|
"BULLET_4": "Publishing and iteration based on community voting.",
|
||||||
|
"CHALLENGE_1": "Reducing complex ideas to a Minimum Viable Product (MVP) achievable in 48h.",
|
||||||
|
"CHALLENGE_2": "Rapid bug diagnosis and fixing under massive time pressure.",
|
||||||
|
"LEARNING_1": "Radical prioritization of features ('Kill your darlings').",
|
||||||
|
"LEARNING_2": "Effective communication and decision-making under team stress."
|
||||||
},
|
},
|
||||||
"DIPLOMA": {
|
"DIPLOMA": {
|
||||||
"TITLE": "Diploma thesis",
|
"TITLE": "Scientific Diploma Thesis",
|
||||||
"SHORT_DESCRIPTION": "Collision detection and handling of complex garments.",
|
"SHORT_DESCRIPTION": "Real-time collision detection for complex, flexible 3D objects.",
|
||||||
"INTRODUCTION": "The thesis deals with the detection and handling of collisions between and within individual items of clothing in real time. This is particularly challenging due to the flexibility of fabrics and their varying properties.",
|
"INTRODUCTION": "Research work in the field of computer graphics. Development of an algorithm for physically correct simulation of fabrics and clothing in real-time.",
|
||||||
"BULLET_1": "Real-time handling of collision detection and response.",
|
"BULLET_1": "Mathematical modeling of mass-spring systems.",
|
||||||
"BULLET_2": "Understanding and evaluating scientific papers.",
|
"BULLET_2": "Low-level programming with Java.",
|
||||||
"BULLET_3": "Adaptation and further development of previous research work.",
|
"BULLET_3": "Optimization through spatial data structures (AABB Trees, Bounding Spheres).",
|
||||||
"BULLET_4": "The thesis was written with C++ and OpenGL and integrated into the Vidya software."
|
"BULLET_4": "Scientific evaluation of simulation precision.",
|
||||||
|
"CHALLENGE_1": "Handling 'self-collisions' in high-resolution meshes without performance loss.",
|
||||||
|
"CHALLENGE_2": "Mathematical stabilization of integration methods under high force impacts.",
|
||||||
|
"LEARNING_1": "Transferring theoretical research into productive, high-performance code.",
|
||||||
|
"LEARNING_2": "Precise working and documentation according to scientific standards."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"IMPRINT": {
|
"IMPRINT": {
|
||||||
@@ -352,13 +386,112 @@
|
|||||||
"DEAD": "Empty",
|
"DEAD": "Empty",
|
||||||
"SPEED": "Time per Generation",
|
"SPEED": "Time per Generation",
|
||||||
"EXPLANATION": {
|
"EXPLANATION": {
|
||||||
"TITLE": "Erklärung",
|
"TITLE": "Explanation",
|
||||||
"EXPLANATION" : "Das Spiel läuft schrittweise ab. Zunächst wird eine Anfangsgeneration von lebenden Zellen auf dem Spielfeld definiert. Aus der vorliegenden Generation (dem Gesamtbild des Spielfeldes) wird die Folgegeneration ermittelt. Der Zustand jeder einzelnen Zelle in der Folgegeneration ergibt sich dabei nach einfachen Regeln aus ihrem aktuellen Zustand sowie den aktuellen Zuständen ihrer acht Nachbarzellen (Moore-Nachbarschaft).",
|
"EXPLANATION": "The game proceeds step by step. First, an initial generation of living cells is defined on the playing field. From the current generation (the overall state of the playing field), the next generation is determined. The state of each individual cell in the next generation is derived from simple rules based on its current state and the current states of its eight neighboring cells (Moore neighborhood).",
|
||||||
"DISCLAIMER": "Nach Conways ursprünglicher Regel lebt eine Zelle in der nächsten Runde, wenn zuvor in ihrer 3x3-Umgebung insgesamt genau drei Zellen leben, wobei sie selbst nur bei Bedarf mitgezählt wird, das heißt:",
|
"DISCLAIMER": "According to Conway’s original rule, a cell is alive in the next round if exactly three cells are alive in its 3x3 neighborhood beforehand, counting itself only if necessary, that is:",
|
||||||
"DISCLAIMER_1": "Eine lebende Zelle lebt auch in der Folgegeneration, wenn sie entweder zwei oder drei lebende Nachbarn hat.",
|
"DISCLAIMER_1": "A living cell remains alive in the next generation if it has either two or three living neighbors.",
|
||||||
"DISCLAIMER_2": "Eine tote Zelle „wird geboren“ (lebt in der Folgegeneration), wenn sie genau drei lebende Nachbarn hat.",
|
"DISCLAIMER_2": "A dead cell is “born” (is alive in the next generation) if it has exactly three living neighbors.",
|
||||||
"DISCLAIMER_3": "Eine lebende Zelle „stirbt“ (ist in der Folgegeneration tot), wenn sie weniger als zwei (Vereinsamung) oder mehr als drei (Übervölkerung) lebende Nachbarn hat.",
|
"DISCLAIMER_3": "A living cell “dies” (is dead in the next generation) if it has fewer than two (underpopulation) or more than three (overpopulation) living neighbors.",
|
||||||
"DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat."
|
"DISCLAIMER_4": "A dead cell remains dead if it does not have exactly three living neighbors."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LABYRINTH": {
|
||||||
|
"TITLE": "Labyrinth Generation",
|
||||||
|
"PRIM": "Generate Prim's Labyrinth",
|
||||||
|
"KRUSKAL": "Generate Kruskal's Labyrinth",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Algorithms",
|
||||||
|
"PRIM_EXPLANATION": "starts at a random point and expands the labyrinth by always selecting a random neighboring wall of an already visited cell and opening it. Advantage: Produces very uniform, natural-looking labyrinths with many short dead ends. Visually, it appears like organic growth from a central point.",
|
||||||
|
"KRUSKAL_EXPLANATION": "considers all walls of the grid as potential paths. It randomly selects walls and opens them only if the two adjacent cells are not yet connected (preventing cycles). Advantage: Produces a very complex labyrinth with many long, winding paths. Visually, it is engaging because the labyrinth emerges simultaneously in many places and eventually merges into a whole.",
|
||||||
|
"DISCLAIMER": "Both algorithms are based on the principle of the 'Minimum Spanning Tree'. This means for your labyrinth:",
|
||||||
|
"DISCLAIMER_1": "Perfect labyrinth: There are no closed loops – every point is reachable, but there is always exactly one path between any two points.",
|
||||||
|
"DISCLAIMER_2": "Reachability: Since it is a spanning tree, every cell in the grid is guaranteed to be part of the labyrinth; there are no isolated areas.",
|
||||||
|
"DISCLAIMER_3": "Randomness: By weighting the edges with random values, each run produces completely new, unique structures.",
|
||||||
|
"DISCLAIMER_4": "Application: Such labyrinths are the perfect test environment for pathfinding algorithms such as Dijkstra or A*."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fractals",
|
||||||
|
"ALGORITHM": "Algorithms",
|
||||||
|
"RESET": "Reset",
|
||||||
|
"COLOR_SCHEME": "Color Scheme",
|
||||||
|
"MAX_ITERATION": "Max. Resolution",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Mathematical Art",
|
||||||
|
"MANDELBROT_EXPLANATION": "is based on the iterative formula 'z_{n+1} = z_n^2 + c'. It checks for every point in the complex plane whether the sequence remains stable or escapes to infinity. Advantage: Known as the 'Apple Man', it is the mother of all fractals, offering infinite variety and self-similar structures to zoom into forever.",
|
||||||
|
"JULIA_EXPLANATION": "uses the same formula as Mandelbrot but fixes the parameter 'c' and varies the starting value. Depending on the choice of 'c', it creates delicate, cloud-like structures or disconnected 'dust'. Advantage: Allows for immense aesthetic variance, as every coordinate in the Mandelbrot set produces its own unique Julia fractal.",
|
||||||
|
"NEWTON_EXPLANATION": "is created by visualizing Newton's method for finding roots of a complex function. Each pixel is colored based on which root the algorithm converges to. Advantage: Produces fascinating star-shaped symmetries and complex boundaries where the attraction basins of the roots meet in a chaotic dance.",
|
||||||
|
"BURNING_SHIP_EXPLANATION": "is a variation of the Mandelbrot set where the absolute values of the real and imaginary parts are taken before each iteration: '(|Re(z)| + i|Im(z)|)^2 + c'. Advantage: Generates a striking, asymmetrical structure resembling a ship on fire. It feels more 'mechanical' and darker compared to the classical sets.",
|
||||||
|
"DISCLAIMER": "All these fractals are based on the principle of iteration and the butterfly effect. This means for your visualization:",
|
||||||
|
"DISCLAIMER_1": "Infinite Depth: No matter how far you zoom in, new complex structures appear that often resemble the whole (self-similarity).",
|
||||||
|
"DISCLAIMER_2": "Escape-Time Algorithm: Colors usually represent how quickly a sequence exceeds a certain threshold—the faster it escapes, the 'hotter' or brighter the color.",
|
||||||
|
"DISCLAIMER_3": "Complex Numbers: Calculations don't happen in a standard coordinate system, but in the complex plane using real and imaginary components.",
|
||||||
|
"DISCLAIMER_4": "Computational Load: Since hundreds of calculations are performed for every single pixel, fractals are a classic benchmark for GPU and processor performance.",
|
||||||
|
"DISCLAIMER_BOTTOM": "Graphics cards calculate with 32-bit floating point numbers (float) by default. These only have about 7 digits of accuracy. At very high zoom levels (> 100.000), the difference between two pixels is so tiny that the graphics card can no longer display it. It calculates exactly the same value for 10 pixels next to each other -> you see blocks or stair steps."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FRACTAL3D": {
|
||||||
|
"TITLE": "3D Fractals",
|
||||||
|
"ALGORITHM": "Algorithms",
|
||||||
|
"MANDELBULB": "Mandelbulb",
|
||||||
|
"MANDELBOX": "Mandelbox",
|
||||||
|
"JULIA": "Julia",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "3D Fractal Worlds",
|
||||||
|
"MANDELBULB_EXPLANATION": "is considered the 'Holy Grail' of 3D fractals. Since complex numbers are only two-dimensional, this fractal uses spherical coordinates and high powers (usually v^8 + c) to project the Mandelbrot set into 3D space. Benefit: Creates an organic, extremely detailed structure reminiscent of plants, coral reefs, or alien landscapes.",
|
||||||
|
"MANDELBOX_EXPLANATION": "is based not on smooth curves, but on geometric 'folding' and scaling (Box-Folding & Sphere-Folding). Space is repeatedly folded and mirrored like origami. Benefit: Produces strictly geometric, mechanical-looking structures that resemble endless futuristic cities, the Borg cube, or complex sci-fi architecture.",
|
||||||
|
"JULIA_EXPLANATION": "is the 3D counterpart to the 2D Julia set. While the Mandelbulb is a 'map' of all fractals, here we fix the parameter 'c' and vary the starting point and it changes over time. Benefit: Unlike the solid Mandelbulb, Julia Bulbs are often hollow, forming complex tunnel systems or bubble-like structures perfect for flying through with the camera.",
|
||||||
|
"DISCLAIMER": "This visualization uses a technique called 'Raymarching' (Sphere Tracing). This means:",
|
||||||
|
"DISCLAIMER_1": "No Polygons: There are no triangles or meshes. The shape is calculated mathematically for every pixel in real-time.",
|
||||||
|
"DISCLAIMER_2": "Distance Estimation: The algorithm 'marches' light rays forward by calculating the safe distance to the nearest object without hitting it immediately.",
|
||||||
|
"DISCLAIMER_3": "Infinite Detail: Since the surface is mathematically defined, it never pixelates when zooming in – new structures always emerge.",
|
||||||
|
"DISCLAIMER_4": "Light & Shadow: To visualize depth, light reflections and shadows (Ambient Occlusion) are simulated based on the curvature of the formula."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PENDULUM": {
|
||||||
|
"TITLE": "Double pendulum",
|
||||||
|
"TRAIL_DECAY_TIME": "Trail length",
|
||||||
|
"DAMPING": "Damping",
|
||||||
|
"ATTRACTION": "Attraction",
|
||||||
|
"L1_LENGTH": "Length L1",
|
||||||
|
"L2_LENGTH": "Length L2",
|
||||||
|
"M1_MASS": "Mass M1",
|
||||||
|
"M2_MASS": "Masse M2",
|
||||||
|
"POKE_M1": "Poke M1",
|
||||||
|
"POKE_M2": "Poke M2",
|
||||||
|
"RESET": "Reset",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Chaos Theory: The Double Pendulum",
|
||||||
|
"EXPLANATION": "The double pendulum is one of physics' most famous and fascinating examples of a dynamic system that generates 'deterministic chaos'. It simply consists of a standard pendulum with a second pendulum attached to its lower end. Although the underlying laws of classical mechanics are strictly mathematically defined, the long-term behavior of the double pendulum is absolutely unpredictable. In physics, it is considered the classic showcase object for the so-called butterfly effect.",
|
||||||
|
"DISCLAIMER": "This WebGPU simulation calculates the motion and acceleration equations of the pendulum 60 times per second in real-time. The following characteristics apply:",
|
||||||
|
"DISCLAIMER_1": "Extreme Sensitivity: The tiniest changes in the initial conditions (e.g., a thousandth of a degree deviation in the starting angle or mass) lead to a completely different, chaotic trajectory after just a short time.",
|
||||||
|
"DISCLAIMER_2": "Deterministic Chaos: The movement may look completely wild and random, but it isn't. If you restart the simulation with the exact same values, the pendulum will follow 100% the same path.",
|
||||||
|
"DISCLAIMER_3": "Numerical Integration: Since computers do not calculate time continuously but in tiny steps (dt), minute mathematical rounding errors occur in every frame. These add up over time and further influence the chaos.",
|
||||||
|
"DISCLAIMER_4": "Energy Conservation & Friction: In a perfect physical system without resistance, the pendulum would swing forever. For a natural look, the algorithm uses an artificial damping factor that simulates air friction and eventually brings the system to a halt.",
|
||||||
|
"DISCLAIMER_BOTTOM": "NOTE: If too many impulses are fed into the system, the simulation becomes unstable. The pendulum will then just hang down and the simulation will have to be restarted."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CLOTH": {
|
||||||
|
"TITLE": "Cloth simulation",
|
||||||
|
"WIND_ON": "Wind On",
|
||||||
|
"WIND_OFF": "Wind Off",
|
||||||
|
"OUTLINE_ON": "Show Mesh",
|
||||||
|
"OUTLINE_OFF": "Hide Mesh",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Real-time Cloth Simulation on the GPU",
|
||||||
|
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Cloth Simulation",
|
||||||
|
"XPBD_EXPLANATION_TITLE": "XPBD (Extended Position-Based Dynamics)",
|
||||||
|
"GPU_PARALLELIZATION_EXPLANATION_TITLE": "GPU Parallelization",
|
||||||
|
"DATA_STRUCTURES_EXPLANATION_TITLE": "Data Structures",
|
||||||
|
"CLOTH_SIMULATION_EXPLANATION": "Cloth simulations usually model textiles as a grid of mass points (vertices) held together by invisible connections. The goal is to represent physical influences like gravity, wind, and collisions in real time without the material tearing or stretching unnaturally like rubber.",
|
||||||
|
"XPBD_EXPLANATION": "XPBD (Extended Position-Based Dynamics) is a modern algorithm that manipulates point positions directly to satisfy distance conditions (constraints) instead of calculating accelerations. The 'Extended' means that true physical stiffness is simulated independently of the framerate. Advantage: Absolutely stable, does not explode, and topological changes (like cutting cloth) are trivial. Disadvantage: It is an iterative approximation method and slightly less physically accurate than complex matrix solvers.",
|
||||||
|
"GPU_PARALLELIZATION_EXPLANATION": "To calculate tens of thousands of points in parallel on the graphics card, one must prevent 'race conditions' – i.e., two processing cores shifting the same node at the exact same time. The solution is called 'Independent Sets' (or Graph Coloring): The connections are divided into isolated groups (e.g., 4 phases for a 2D grid) in which not a single point overlaps. This allows the GPU to process each group blindly and at maximum speed.",
|
||||||
|
"DATA_STRUCTURES_EXPLANATION": "For maximum GPU performance, data must be memory-aligned (16-byte alignment). Instead of using many individual variables, information is cleverly packed into blocks of four (vec4). A vertex stores, for example, [X, Y, Z, Inverse_Mass]. If a point has an inverse mass of 0.0, the algorithm ignores it, and it floats motionlessly in the air – an elegant trick for pinning cloth without extra if/then statements.",
|
||||||
|
"DISCLAIMER": "XPBD vs. Mass-Spring Systems: In physical simulations, there are fundamental architectural differences when solving equations:",
|
||||||
|
"DISCLAIMER_1": "Classical Mass-Spring Systems: Here, forces (Hooke's Law) are calculated, leading to accelerations and ultimately new positions. There are two ways to mathematically project these into the future (integration):",
|
||||||
|
"DISCLAIMER_2": "Explicit Solvers (e.g., Forward Euler): These rigidly calculate the next step solely from the current state. They are easy to program but extremely unstable for stiff cloths. Forces can escalate and the simulation 'explodes' unless tiny, very performance-heavy time steps are chosen.",
|
||||||
|
"DISCLAIMER_3": "Implicit Solvers (e.g., Backward Euler): These calculate the next step based on the future state. This is mathematically highly stable but requires solving massive global matrix equation systems in every frame. This is harder to parallelize on the GPU and breaks down if the structure changes (e.g., when the cloth is cut).",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ALGORITHM": {
|
"ALGORITHM": {
|
||||||
@@ -372,9 +505,29 @@
|
|||||||
"DESCRIPTION": "Visualizing various sorting algorithms."
|
"DESCRIPTION": "Visualizing various sorting algorithms."
|
||||||
},
|
},
|
||||||
"GOL": {
|
"GOL": {
|
||||||
"TITLE:": "Conway's Game of Life",
|
"TITLE": "Conway's Game of Life",
|
||||||
"DESCRIPTION": "The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970."
|
"DESCRIPTION": "The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970."
|
||||||
},
|
},
|
||||||
|
"LABYRINTH": {
|
||||||
|
"TITLE": "Maze Generation",
|
||||||
|
"DESCRIPTION": "Visualizing various maze generation algorithms."
|
||||||
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fractals",
|
||||||
|
"DESCRIPTION": "Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)."
|
||||||
|
},
|
||||||
|
"FRACTAL3D": {
|
||||||
|
"TITLE": "Fractals 3D",
|
||||||
|
"DESCRIPTION": "3D Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)."
|
||||||
|
},
|
||||||
|
"PENDULUM": {
|
||||||
|
"TITLE": "Double pendulum",
|
||||||
|
"DESCRIPTION": "Visualisation of a chaotic double pendulum simulation with WebGPU."
|
||||||
|
},
|
||||||
|
"CLOTH": {
|
||||||
|
"TITLE": "Cloth simulation",
|
||||||
|
"DESCRIPTION": "Simulation of cloth with WebGPU."
|
||||||
|
},
|
||||||
"NOTE": "Note",
|
"NOTE": "Note",
|
||||||
"GRID_HEIGHT": "Height",
|
"GRID_HEIGHT": "Height",
|
||||||
"GRID_WIDTH": "Width"
|
"GRID_WIDTH": "Width"
|
||||||
|
|||||||
BIN
src/assets/projects/playground/1.png
Normal file
BIN
src/assets/projects/playground/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
630
src/styles.scss
630
src/styles.scss
@@ -1,15 +1,13 @@
|
|||||||
@use '@angular/material' as mat;
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
// ---- Themes ----
|
// ---- Themes ----
|
||||||
$light-theme: mat.define-theme((
|
$light-theme: mat.define-theme((color: (theme-type: light, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
|
||||||
color: ( theme-type: light, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
|
typography: (brand-family: 'Inter, Roboto, Arial, sans-serif', bold-weight: 600),
|
||||||
typography: ( brand-family: 'Inter, Roboto, Arial, sans-serif', bold-weight: 600 ),
|
density: (scale: 0),
|
||||||
density: ( scale: 0 ),
|
));
|
||||||
));
|
|
||||||
|
|
||||||
$dark-theme: mat.define-theme((
|
$dark-theme: mat.define-theme((color: (theme-type: dark, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
|
||||||
color: ( theme-type: dark, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
|
));
|
||||||
));
|
|
||||||
|
|
||||||
// ---- Core + Components ----
|
// ---- Core + Components ----
|
||||||
@include mat.core-theme($light-theme);
|
@include mat.core-theme($light-theme);
|
||||||
@@ -23,11 +21,12 @@ $dark-theme: mat.define-theme((
|
|||||||
|
|
||||||
/* ---- Custom variables ---- */
|
/* ---- Custom variables ---- */
|
||||||
:root {
|
:root {
|
||||||
--app-topbar-bg: #{mat.get-theme-color($light-theme, surface)};
|
--app-maxWidth: 1200px;
|
||||||
--app-bg: #{mat.get-theme-color($light-theme, surface-container-low)};
|
--app-bg: #{mat.get-theme-color($light-theme, surface-container-low)};
|
||||||
--app-fg: #{mat.get-theme-color($light-theme, on-surface)};
|
--app-fg: #{mat.get-theme-color($light-theme, on-surface)};
|
||||||
--app-logo-bg: #313131;
|
--app-logo-bg: #313131;
|
||||||
--app-card-background: #fafafa;
|
--app-card-background: #fafafa;
|
||||||
|
--app-topbar-bg: var(--app-card-background);
|
||||||
|
|
||||||
--card-radius: 18px;
|
--card-radius: 18px;
|
||||||
--card-bg: var(--app-card-background);
|
--card-bg: var(--app-card-background);
|
||||||
@@ -37,12 +36,13 @@ $dark-theme: mat.define-theme((
|
|||||||
--link-color: #38a7ff;
|
--link-color: #38a7ff;
|
||||||
--link-color-hover: #66bfff;
|
--link-color-hover: #66bfff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--app-topbar-bg: #{mat.get-theme-color($dark-theme, surface-container-highest)};
|
--app-bg: #{mat.get-theme-color($dark-theme,surface-variant)};
|
||||||
--app-bg: #{mat.get-theme-color($dark-theme,surface-variant)};
|
|
||||||
--app-fg: #{mat.get-theme-color($dark-theme, on-surface)};
|
--app-fg: #{mat.get-theme-color($dark-theme, on-surface)};
|
||||||
--app-card-background: #313131;
|
--app-card-background: #313131;
|
||||||
--app-logo-bg: #313131;
|
--app-logo-bg: #313131;
|
||||||
|
--app-topbar-bg: var(--app-card-background);
|
||||||
|
|
||||||
--card-bg: var(--app-card-background);
|
--card-bg: var(--app-card-background);
|
||||||
|
|
||||||
@@ -51,7 +51,11 @@ $dark-theme: mat.define-theme((
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- global background and tests ---- */
|
/* ---- global background and tests ---- */
|
||||||
html, body { height: 100%; }
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Inter, Roboto, Arial, sans-serif;
|
font-family: Inter, Roboto, Arial, sans-serif;
|
||||||
@@ -62,10 +66,14 @@ body {
|
|||||||
|
|
||||||
.material-symbols-outlined {
|
.material-symbols-outlined {
|
||||||
font-variation-settings:
|
font-variation-settings:
|
||||||
"FILL" 0, /* 0 oder 1 */
|
"FILL" 0,
|
||||||
"wght" 400, /* 100..700 */
|
/* 0 oder 1 */
|
||||||
"GRAD" 0, /* -50..200 */
|
"wght" 400,
|
||||||
"opsz" 24; /* 20..48 */
|
/* 100..700 */
|
||||||
|
"GRAD" 0,
|
||||||
|
/* -50..200 */
|
||||||
|
"opsz" 24;
|
||||||
|
/* 20..48 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* smooth transition between theme change */
|
/* smooth transition between theme change */
|
||||||
@@ -111,9 +119,9 @@ a {
|
|||||||
box-shadow 200ms ease,
|
box-shadow 200ms ease,
|
||||||
transform 200ms ease;
|
transform 200ms ease;
|
||||||
|
|
||||||
&.container {
|
&.algo-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1920px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +156,8 @@ a {
|
|||||||
.mat-accordion .mat-expansion-panel {
|
.mat-accordion .mat-expansion-panel {
|
||||||
border-radius: var(--card-radius) !important;
|
border-radius: var(--card-radius) !important;
|
||||||
background: var(--card-bg) !important;
|
background: var(--card-bg) !important;
|
||||||
overflow: hidden; /* ok */
|
overflow: hidden;
|
||||||
|
/* ok */
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,8 +229,12 @@ a {
|
|||||||
|
|
||||||
// algos
|
// algos
|
||||||
|
|
||||||
.container {
|
.algo-container {
|
||||||
padding: 2rem;
|
max-width: var(--app-maxWidth);
|
||||||
|
gap: clamp(1rem, 3vw, 1.5rem);
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.algo-info {
|
.algo-info {
|
||||||
@@ -262,13 +275,13 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-size {
|
.input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.grid-field {
|
.input-field {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,6 +289,7 @@ a {
|
|||||||
canvas {
|
canvas {
|
||||||
border: 1px solid lightgray;
|
border: 1px solid lightgray;
|
||||||
display: block;
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,13 +308,49 @@ canvas {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
&.start { background-color: green; }
|
&.start {
|
||||||
&.end { background-color: red; }
|
background-color: green;
|
||||||
&.wall { background-color: black; }
|
}
|
||||||
&.visited { background-color: skyblue; }
|
|
||||||
&.path { background-color: gold; }
|
&.end {
|
||||||
&.empty { background-color: lightgray; }
|
background-color: red;
|
||||||
&.alive { background-color: black; }
|
}
|
||||||
|
|
||||||
|
&.wall {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.visited {
|
||||||
|
background-color: skyblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.path {
|
||||||
|
background-color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alive {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.L1 {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.L2 {
|
||||||
|
background-color: magenta;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.M1 {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.M2 {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,33 +360,529 @@ canvas {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sorting Visualization */
|
/* Sorting Visualization & Canvas */
|
||||||
.sorting-visualization-area {
|
.sorting-visualization-area,
|
||||||
|
.visualization-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
height: 300px; /* Max height for bars */
|
height: clamp(200px, 40vh, 400px);
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid var(--app-fg);
|
||||||
margin-bottom: 20px;
|
margin-bottom: clamp(10px, 3vw, 20px);
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
background-color: #f0f0f0;
|
background-color: var(--card-bg);
|
||||||
|
|
||||||
.sorting-bar {
|
.sorting-bar,
|
||||||
|
.bar {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background-color: #424242; /* Default unsorted color */
|
background-color: #424242;
|
||||||
transition: height 0.05s ease-in-out, background-color 0.05s ease-in-out;
|
transition: height 0.05s ease-in-out, background-color 0.05s ease-in-out;
|
||||||
width: 10px; /* Default width, flex-grow will adjust */
|
width: 10px;
|
||||||
min-width: 1px; /* Ensure bars are always visible */
|
min-width: 1px;
|
||||||
|
|
||||||
&.unsorted {
|
&.unsorted {
|
||||||
background-color: #424242;
|
background-color: #424242;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.comparing {
|
&.comparing {
|
||||||
background-color: #ffeb3b; /* Yellow for comparing */
|
background-color: #ffeb3b;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sorted {
|
&.sorted {
|
||||||
background-color: #4caf50; /* Green for sorted */
|
background-color: #4caf50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Modern Layouts & Typography (Grid, Flex, Clamp) ---- */
|
||||||
|
|
||||||
|
.layout-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--app-maxWidth);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: clamp(1rem, 4vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
app-root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--app-maxWidth);
|
||||||
|
margin: 1rem auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-surface {
|
||||||
|
flex-grow: 1;
|
||||||
|
color: var(--app-fg);
|
||||||
|
transition: background-color 220ms ease, color 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, .08);
|
||||||
|
padding: clamp(1rem, 2vw, 1.5rem);
|
||||||
|
text-align: center;
|
||||||
|
opacity: .8;
|
||||||
|
background: var(--app-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Menu Overrides ---- */
|
||||||
|
.mat-mdc-menu-item .mdc-list-item__primary-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-menu-item .kbd {
|
||||||
|
margin-left: auto;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0 .35rem;
|
||||||
|
opacity: .65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-menu-item .mat-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-menu-item .flag-icon {
|
||||||
|
width: 20px !important;
|
||||||
|
height: 14px !important;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: .5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-menu-panel {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, .14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mat-mdc-menu-panel {
|
||||||
|
border-color: rgba(255, 255, 255, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- About Page Sections ---- */
|
||||||
|
.about,
|
||||||
|
.imprint {
|
||||||
|
display: grid;
|
||||||
|
gap: clamp(1rem, 3vw, 1.5rem);
|
||||||
|
max-width: var(--app-maxWidth);
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
background: var(--card-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: clamp(1rem, 4vw, 2rem);
|
||||||
|
padding: clamp(1rem, 3vw, 1.5rem);
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
flex: 1 1 min(100%, 425px);
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 425px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 6px 24px rgba(0, 0, 0, .25);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
flex: 999 1 min(100%, 400px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .intro h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: clamp(1.5rem, 5vw, 2.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .intro .lead {
|
||||||
|
opacity: .9;
|
||||||
|
margin: 0.5rem 0 1rem;
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.15rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .intro .meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .intro .meta .row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .intro .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills,
|
||||||
|
.experience,
|
||||||
|
.projects,
|
||||||
|
.education {
|
||||||
|
padding: clamp(5px, 2vw, 15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills h2,
|
||||||
|
.experience h2,
|
||||||
|
.projects h2,
|
||||||
|
.education h2 {
|
||||||
|
margin-top: .25rem;
|
||||||
|
margin-left: .25rem;
|
||||||
|
font-size: clamp(1.2rem, 4vw, 1.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills .chip-groups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
|
||||||
|
gap: clamp(0.5rem, 2vw, 1rem);
|
||||||
|
margin-left: .25rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills .chip-groups h3 {
|
||||||
|
margin: .2rem 0 .4rem;
|
||||||
|
font-size: .95rem;
|
||||||
|
opacity: .85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-list {
|
||||||
|
margin-left: .25rem;
|
||||||
|
display: grid;
|
||||||
|
gap: clamp(0.75rem, 2vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-item .xp-head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-item .xp-head .time {
|
||||||
|
opacity: .75;
|
||||||
|
font-size: clamp(0.85rem, 2vw, 0.95rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-item .xp-sub {
|
||||||
|
opacity: .9;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-item ul {
|
||||||
|
margin: .25rem 0 .5rem 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-head-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: calc(clamp(32px, 8vw, 48px) + .75rem) 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
column-gap: clamp(0.5rem, 2vw, .75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrap {
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
grid-column: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-logo {
|
||||||
|
width: clamp(32px, 8vw, 48px);
|
||||||
|
height: clamp(32px, 8vw, 48px);
|
||||||
|
object-fit: contain;
|
||||||
|
opacity: .9;
|
||||||
|
border-radius: 10%;
|
||||||
|
background-color: var(--app-logo-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-row {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: clamp(0.25rem, 1vw, 0.5rem) 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-row strong {
|
||||||
|
font-size: clamp(0.95rem, 2.5vw, 1.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-row .time {
|
||||||
|
opacity: .75;
|
||||||
|
font-size: clamp(0.85rem, 2vw, 0.95rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-row {
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 2;
|
||||||
|
margin-top: .1rem;
|
||||||
|
opacity: .85;
|
||||||
|
font-size: clamp(0.85rem, 2vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlights {
|
||||||
|
margin-top: .4rem;
|
||||||
|
margin-left: clamp(0.25rem, 1vw, .75rem);
|
||||||
|
padding-left: clamp(0.8rem, 2vw, 1.2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlights li,
|
||||||
|
.highlights-noMargin li {
|
||||||
|
margin: .2rem 0;
|
||||||
|
font-size: clamp(0.9rem, 2vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Imprint ---- */
|
||||||
|
.imprint-card {
|
||||||
|
padding: clamp(1rem, 3vw, 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imprint-title {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: clamp(1rem, 3vw, 1.2rem);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imprint-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imprint-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Projects Page & Dialog ---- */
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: clamp(1rem, 3vw, 1.5rem);
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 450px), 1fr));
|
||||||
|
max-width: var(--app-maxWidth);
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algo-card {
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card.featured {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card mat-card-header {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card mat-card-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card mat-chip-set {
|
||||||
|
padding-top: clamp(0.5rem, 2vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card mat-card-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: clamp(150px, 20vw, 200px);
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-icon {
|
||||||
|
font-size: clamp(3rem, 8vw, 4rem);
|
||||||
|
width: clamp(3rem, 8vw, 4rem);
|
||||||
|
height: clamp(3rem, 8vw, 4rem);
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
img[mat-card-image] {
|
||||||
|
width: 100%;
|
||||||
|
height: clamp(150px, 25vw, 250px);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-swiper {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-swiper::part(button-prev),
|
||||||
|
.my-swiper::part(button-next) {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 0, 0, .5);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-swiper::part(button-prev):hover,
|
||||||
|
.my-swiper::part(button-next):hover {
|
||||||
|
background: rgba(0, 0, 0, .75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-swiper::part(pagination) {
|
||||||
|
bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swiper-slide {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: clamp(300px, 60vh, 512px) !important;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-source {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #aaa;
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: right;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-section {
|
||||||
|
display: flex;
|
||||||
|
gap: clamp(0.5rem, 2vw, 1rem);
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Shared Elements ---- */
|
||||||
|
.canvas-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 1000px;
|
||||||
|
touch-action: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: clamp(10px, 2vw, 20px);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: clamp(0.5rem, 2vw, 1rem);
|
||||||
|
margin-top: clamp(1rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-cards mat-card {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-cards mat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorting-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1920px;
|
||||||
|
padding: clamp(10px, 3vw, 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorting-card .controls-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: clamp(5px, 2vw, 10px);
|
||||||
|
margin-bottom: clamp(10px, 3vw, 20px);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorting-card .controls-panel mat-form-field {
|
||||||
|
width: clamp(150px, 20vw, 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorting-card .info-panel {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user