Compare commits

..

50 Commits

Author SHA1 Message Date
61a2057291 src/assets/i18n/en.json aktualisiert
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m30s
Build, Test & Push Frontend / docker (push) Successful in 1m10s
2026-02-26 08:10:05 +01:00
737601636f src/assets/i18n/de.json aktualisiert
Some checks failed
Build, Test & Push Frontend / quality-check (push) Has been cancelled
Build, Test & Push Frontend / docker (push) Has been cancelled
2026-02-26 08:09:08 +01:00
0037502b00 src/app/pages/projects/projects.component.ts aktualisiert
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m34s
Build, Test & Push Frontend / docker (push) Successful in 1m7s
2026-02-26 07:12:19 +01:00
5485e57bdb Updated wiki links
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m35s
Build, Test & Push Frontend / docker (push) Successful in 1m9s
2026-02-25 09:10:41 +01:00
f4104d02e8 Bump deps and refactor layouts/styles
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m40s
Build, Test & Push Frontend / docker (push) Successful in 1m8s
Update package-lock with multiple dependency bumps (Angular CLI/DevKit/schematics to 21.1.5, ajv to 8.18.0, minimatch/brace-expansion updates, tar, qs, swiper, hono, etc.). Wrap the generic grid canvas in a container and clean up HTML structure/indentation in algorithms and pathfinding templates (add card-grid wrapper and reformat content). Adjust global styles: refine .algo-container and app-root margins/gaps, and refactor hero/photo/intro styles for responsive layout. Changes are primarily dependency updates and UI/layout refinements.
2026-02-25 08:50:28 +01:00
28bde29c8b Merge pull request 'feature/clothsimulation' (#28) from feature/clothsimulation into main
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m31s
Build, Test & Push Frontend / docker (push) Successful in 1m42s
Reviewed-on: #28
2026-02-24 09:31:35 +01:00
ab3bca4395 Cloth: add info, outline, diagonals, shader
All checks were successful
Build, Test & Push Frontend / quality-check (pull_request) Successful in 2m1s
Build, Test & Push Frontend / docker (pull_request) Has been skipped
Add an informational panel and mesh-outline toggle to the cloth demo, plus richer physics and shading. The cloth component now provides AlgorithmInformation to an <app-information> view and a toggleMesh() that flips the mesh wireframe. Constraint generation was extended with four diagonal phases (constraintsP4..P7) and the solver loop was generalized to iterate solver pipelines, improving parallel XPBD constraint handling. The WGSL vertex/fragment shaders were updated to pass world positions, compute normals, add simple lighting and a grid-based base color. Also update information template/model to support optional translated entry names and expand i18n (DE/EN) with cloth texts and a Docker key.
2026-02-24 09:28:16 +01:00
12411e58bf Refactored the cloth class for better reading 2026-02-24 08:51:53 +01:00
14d7a78ac4 Added wind to cloth simulation 2026-02-24 08:46:57 +01:00
ed0e370e9d Fixed some styling issues 2026-02-24 08:27:59 +01:00
f656206691 Refactor cloth component and shaders
Improve readability, typing and structure for the cloth simulation component and WGSL shaders. Changes include: formatted imports, added file/header JSDoc and inline comments, made renderConfig and lifecycle methods public with explicit types, renamed component selector to 'app-cloth', converted several functions to typed helpers (e.g. createAndPopulateBuffer, addConstraint), consolidated buffer creation, and cleaned up compute shader binding mappings. Shader file receives file header and minor comment clarifications and a bounds check comment; overall changes are stylistic and organizational to increase maintainability and clarity without altering core algorithm behavior.
2026-02-23 11:30:05 +01:00
728dbc047f Use 4-phase graph-coloring for constraints
Split cloth constraints into 4 graph-colored phases (horizontal even/odd, vertical even/odd) instead of one big constraints array. Create dynamic JS arrays (constraintsP0..P3) with an addConstraint helper, allocate four GPU constraint buffers and four corresponding solve compute shaders (csSolve0..csSolve3) via a createSolver helper, and dispatch them per substep to avoid write-write races. Update integrate/velocity shader bindings setup and dispatch logic; keep positions/prevPositions/velocities buffers as before. In WGSL, mark constraints as read-only and use arrayLength(&constraints) to bound-check the constraint index instead of relying on a CPU-side count. Also tweak sim parameter (compliance lowered) and minor refactors/cleanups for clarity and consistency.
2026-02-23 11:19:28 +01:00
746022c48d Update cloth.component.ts 2026-02-23 11:07:09 +01:00
954211b3cf Add cloth simulation page with WGSL shaders
Introduce a new cloth simulation feature: adds ClothComponent (TS/HTML/SCSS) and WGSL compute/vertex/fragment shaders implementing an XPBD-based cloth sim. Wire up routing and RouterConstants, add the cloth entry to the algorithms list, and add English/German i18n strings. Also include small refactors/renames for algorithm-category and algorithms.service imports and update BabylonCanvas to tolerate optional shader configuration and avoid null access during setup.
2026-02-23 11:02:54 +01:00
885e609082 Update particles-background.component.ts
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m38s
Build, Test & Push Frontend / docker (push) Successful in 1m41s
2026-02-23 10:08:36 +01:00
b61eb4eb73 Update particles-background.component.ts
Some checks failed
Build, Test & Push Frontend / quality-check (push) Failing after 31s
Build, Test & Push Frontend / docker (push) Has been skipped
2026-02-23 10:07:26 +01:00
96d4659652 Merge pull request 'Added background animation to habe a little bit more interesting page' (#27) from feature/backgroundAnimation into main
Some checks failed
Build, Test & Push Frontend / quality-check (push) Failing after 44s
Build, Test & Push Frontend / docker (push) Has been skipped
Reviewed-on: #27
2026-02-23 10:01:28 +01:00
ad43459173 Added background animation to habe a little bit more interesting page
Some checks failed
Build, Test & Push Frontend / docker (pull_request) Has been cancelled
Build, Test & Push Frontend / quality-check (pull_request) Has been cancelled
2026-02-23 10:01:01 +01:00
32ecfcb621 Merge pull request 'feature/scss-cleanup' (#26) from feature/scss-cleanup into main
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m31s
Build, Test & Push Frontend / docker (push) Successful in 1m53s
Reviewed-on: #26
2026-02-23 09:46:12 +01:00
aceb0ea24e optimized pendulum visual
All checks were successful
Build, Test & Push Frontend / quality-check (pull_request) Successful in 1m58s
Build, Test & Push Frontend / docker (pull_request) Has been skipped
2026-02-23 09:43:34 +01:00
c160fb4bc8 overworked the project detail pages 2026-02-23 09:37:26 +01:00
45c11e42cd Added an image for the playground project 2026-02-23 09:11:16 +01:00
e66206c518 Fixed hero page 2026-02-23 09:07:33 +01:00
d0c4ad770b Cleaned and focused about page 2026-02-23 08:56:34 +01:00
5f8b1de20f Changed link from github to codeberg 2026-02-23 08:50:24 +01:00
30965afcbd Changed layout, taht everything is max 1200px width and centered 2026-02-23 08:41:49 +01:00
6330d45b4e Fixed topbar 2026-02-22 12:48:20 +01:00
c6edc922fe Refactor topbar HTML, projects TS and styles
Minor markup and formatting cleanup plus layout adjustments.

- topbar.component.html: removed mat-toolbar color attribute, compacted/normalized element attributes and spacing, fixed small markup spacing issues.
- projects.component.ts: code formatting and whitespace normalization (imports, property spacing, object literals, small function signature/timeout formatting); no logic changes.
- styles.scss: reorganized topbar rules (added app-topbar wrapper, display and color), switched .hero from grid to flex with responsive flex-basis, adjusted photo and intro flex behavior, increased project grid column min width and centered grid with max-width, removed featured card full-width grid span.

These changes improve consistency, readability and adjust layout/responsiveness of the topbar/hero/project grid.
2026-02-22 12:27:20 +01:00
0e78e6b471 Update styles.scss 2026-02-22 12:02:17 +01:00
8f21b0e6b0 Update styles.scss 2026-02-22 11:59:36 +01:00
cb2ffa2d80 Consolidate and modernize SCSS into global styles
Move component-level styles into src/styles.scss and remove duplicated rules from several component SCSS files (app, topbar, about, algorithms, sorting, imprint, project dialog, projects, babylon-canvas). The global stylesheet now centralizes layout and typography (clamp-based sizing), sorting visualization & canvas rules, topbar/menu overrides, project/swiper styles, and shared utilities. This reduces duplication, improves responsiveness, and simplifies stylesheet management.
2026-02-22 11:48:21 +01:00
5ebd1d19ea Merge pull request 'feature/webGPU' (#25) from feature/webGPU into main
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m33s
Build, Test & Push Frontend / docker (push) Successful in 1m40s
Reviewed-on: #25
2026-02-21 12:32:03 +01:00
eed7e8c0fa Update fractal.component.ts
All checks were successful
Build, Test & Push Frontend / quality-check (pull_request) Successful in 1m30s
Build, Test & Push Frontend / docker (pull_request) Has been skipped
fixed linting issue
2026-02-21 12:27:07 +01:00
24d6d9cdbe Finalized Algorithm
Some checks failed
Build, Test & Push Frontend / quality-check (pull_request) Failing after 56s
Build, Test & Push Frontend / docker (pull_request) Has been skipped
Added final descriptions and polished the system
2026-02-21 12:25:07 +01:00
34148aade2 Added UI and painted pendulum in different colors 2026-02-21 11:47:57 +01:00
5721b2e48e Added resize callback to restart simulation new if canvas is resized 2026-02-21 10:25:20 +01:00
2bfa8ba9a1 Added trail effect 2026-02-21 10:21:21 +01:00
13f99ac7ae Refactored #2
- Refactored shader code and typescript code
- Made it more clear
- Added some comments
2026-02-21 10:03:01 +01:00
66df3a7f88 Added some comments and removed unused UVs 2026-02-21 09:53:14 +01:00
598013a7d0 Rendeirng problems fixed
Problem was broken uv coordinates in the fragment shader
2026-02-21 09:46:55 +01:00
f499b78fd5 Adding uniform buffers
But still resolution problem
2026-02-20 17:25:42 +01:00
0d2e7c97ec See pendulum
I can see the pendulum, but something is not correct with the resolution
2026-02-20 17:14:58 +01:00
13b59d0b36 Smaller refactoring
- Put shader in own file
- renamed package
2026-02-20 16:50:24 +01:00
55ece27e1c Add pendulum demo and WGSL support
Introduce a new Pendulum demo (component, template, stylesheet) and wire it into routing and the algorithms list. Extend Babylon canvas API to emit a SceneReadyEvent (scene + engine) and accept a shaderLanguage option in RenderConfig so materials/shaders can target WGSL; update Fractal to consume the new SceneReadyEvent signature. Also add i18n entries for the pendulum demo.
2026-02-18 11:58:25 +01:00
68e21489ea Update babylon-canvas.component.ts 2026-02-17 10:41:51 +01:00
796fdf4a79 Fixed small visual problems with canvas 2026-02-17 09:39:37 +01:00
270716551d Merge pull request 'Fixed resizing problem for 2d canvas' (#24) from bugfix/resize2dCanvas into main
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m29s
Build, Test & Push Frontend / docker (push) Successful in 1m7s
Reviewed-on: #24
2026-02-17 09:29:26 +01:00
a494c8156d Fixed resizing problem for 2d canvas
All checks were successful
Build, Test & Push Frontend / quality-check (pull_request) Successful in 1m30s
Build, Test & Push Frontend / docker (pull_request) Has been skipped
2026-02-17 09:27:21 +01:00
5691cb408d Merge pull request 'Changed slider handling for 2d' (#23) from debug/fractalsliderProblem into main
All checks were successful
Build, Test & Push Frontend / quality-check (push) Successful in 1m0s
Build, Test & Push Frontend / docker (push) Successful in 53s
Reviewed-on: #23
2026-02-13 14:15:45 +01:00
c2ad2ae992 Changed slider handling for 2d
All checks were successful
Build, Test & Push Frontend / quality-check (pull_request) Successful in 1m5s
Build, Test & Push Frontend / docker (pull_request) Has been skipped
2026-02-13 14:15:22 +01:00
58 changed files with 3201 additions and 1186 deletions

289
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "playground-frontend",
"version": "1.0.0",
"dependencies": {
"@angular-slider/ngx-slider": "^21.0.0",
"@angular/animations": "~21.1.0",
"@angular/cdk": "~21.1.0",
"@angular/common": "~21.1.0",
@@ -309,13 +310,13 @@
}
},
"node_modules/@angular-devkit/schematics": {
"version": "21.1.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.3.tgz",
"integrity": "sha512-Ps7bRl5uOcM7WpNJHbSls/jz5/wAI0ldkTlKyiBFA7RtNeQIABAV+hvlw5DJuEb1Lo5hnK0hXj90AyZdOxzY+w==",
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.5.tgz",
"integrity": "sha512-CGmoorQL5+mVCJEHwHWOrhSd1hFxB3h66i9wUDizJAEQUM3mSml5SiglHArpWY/G4GmFwi6XVe+Jm3U8J/mcFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "21.1.3",
"@angular-devkit/core": "21.1.5",
"jsonc-parser": "3.3.1",
"magic-string": "0.30.21",
"ora": "9.0.0",
@@ -328,13 +329,13 @@
}
},
"node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": {
"version": "21.1.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz",
"integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==",
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.5.tgz",
"integrity": "sha512-KUKbllHvHefkAbTBjWNpRPyrpBqecW+6HBBAR+XNbKBuFTHkG+gxtuwMXNsvO5KECKwQphvQt5h3g05Xtaf0LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "8.17.1",
"ajv": "8.18.0",
"ajv-formats": "3.0.1",
"jsonc-parser": "3.3.1",
"picomatch": "4.0.3",
@@ -355,6 +356,23 @@
}
}
},
"node_modules/@angular-devkit/schematics/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@angular-eslint/builder": {
"version": "21.2.0",
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz",
@@ -464,6 +482,22 @@
"typescript": "*"
}
},
"node_modules/@angular-slider/ngx-slider": {
"version": "21.0.0",
"resolved": "https://registry.npmjs.org/@angular-slider/ngx-slider/-/ngx-slider-21.0.0.tgz",
"integrity": "sha512-66mbz9+022VDcquV8RrbD3jF6KXUX28r5YEzBAiv2D2H2wJGZiq6GdxKuCPnIgPs6VL5eCbD7nCO7m02U8kBcw==",
"license": "MIT",
"dependencies": {
"detect-passive-events": "^2.0.3",
"rxjs": "^7.8.2",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0"
}
},
"node_modules/@angular/animations": {
"version": "21.1.2",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.1.2.tgz",
@@ -596,19 +630,19 @@
}
},
"node_modules/@angular/cli": {
"version": "21.1.3",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.3.tgz",
"integrity": "sha512-UPtDcpKyrKZRPfym9gTovcibPzl2O/Woy7B8sm45sAnjDH+jDUCcCvuIak7GpH47shQkC2J4yvnHZbD4c6XxcQ==",
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.5.tgz",
"integrity": "sha512-ljqvAzSk8FKMaYW/aZhR+SXjudbQViYYkMlJvJUClGpokjDM9KfJWPX+QZfr2J+piW5yaaHmFaIMddO9QxkUDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/architect": "0.2101.3",
"@angular-devkit/core": "21.1.3",
"@angular-devkit/schematics": "21.1.3",
"@angular-devkit/architect": "0.2101.5",
"@angular-devkit/core": "21.1.5",
"@angular-devkit/schematics": "21.1.5",
"@inquirer/prompts": "7.10.1",
"@listr2/prompt-adapter-inquirer": "3.0.5",
"@modelcontextprotocol/sdk": "1.26.0",
"@schematics/angular": "21.1.3",
"@schematics/angular": "21.1.5",
"@yarnpkg/lockfile": "1.1.0",
"algoliasearch": "5.46.2",
"ini": "6.0.0",
@@ -632,13 +666,13 @@
}
},
"node_modules/@angular/cli/node_modules/@angular-devkit/architect": {
"version": "0.2101.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.3.tgz",
"integrity": "sha512-vKz8aPA62W+e9+pF6ct4CRDG/MjlIH7sWFGYkxPPRst2g46ZQsRkrzfMZAWv/wnt6OZ1OwyRuO3RW83EMhag8g==",
"version": "0.2101.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.5.tgz",
"integrity": "sha512-eTo6wWzUW5AyBBLTbaUTpBHhGbZhzteErtNGklWkhjicCr/soNH+2mVtvg8bqA8sNreYffK1VXKFsq5NyMh5qg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "21.1.3",
"@angular-devkit/core": "21.1.5",
"rxjs": "7.8.2"
},
"bin": {
@@ -651,13 +685,13 @@
}
},
"node_modules/@angular/cli/node_modules/@angular-devkit/core": {
"version": "21.1.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz",
"integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==",
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.5.tgz",
"integrity": "sha512-KUKbllHvHefkAbTBjWNpRPyrpBqecW+6HBBAR+XNbKBuFTHkG+gxtuwMXNsvO5KECKwQphvQt5h3g05Xtaf0LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "8.17.1",
"ajv": "8.18.0",
"ajv-formats": "3.0.1",
"jsonc-parser": "3.3.1",
"picomatch": "4.0.3",
@@ -678,6 +712,23 @@
}
}
},
"node_modules/@angular/cli/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@angular/common": {
"version": "21.1.2",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.1.2.tgz",
@@ -1688,9 +1739,9 @@
}
},
"node_modules/@eslint/config-array/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -1751,9 +1802,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1796,9 +1847,9 @@
"license": "MIT"
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2316,29 +2367,6 @@
}
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -5174,14 +5202,14 @@
]
},
"node_modules/@schematics/angular": {
"version": "21.1.3",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.3.tgz",
"integrity": "sha512-obJvWBhzRdsYL2msM4+8bQD21vFl3VxaVsuiq6iIfYsxhU5i2Iar2wM9NaRaIIqAYhZ8ehQQ/moB9BEbWvDCTw==",
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.5.tgz",
"integrity": "sha512-AndJ17ePYUoqJqiIF9VaXbGAFfOqDcHuAxhwozsQlWDzwgQSOUC/WWeG9hKVCgMD6tE02Sxr2ova9DiBKsLQNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "21.1.3",
"@angular-devkit/schematics": "21.1.3",
"@angular-devkit/core": "21.1.5",
"@angular-devkit/schematics": "21.1.5",
"jsonc-parser": "3.3.1"
},
"engines": {
@@ -5191,13 +5219,13 @@
}
},
"node_modules/@schematics/angular/node_modules/@angular-devkit/core": {
"version": "21.1.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz",
"integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==",
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.5.tgz",
"integrity": "sha512-KUKbllHvHefkAbTBjWNpRPyrpBqecW+6HBBAR+XNbKBuFTHkG+gxtuwMXNsvO5KECKwQphvQt5h3g05Xtaf0LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "8.17.1",
"ajv": "8.18.0",
"ajv-formats": "3.0.1",
"jsonc-parser": "3.3.1",
"picomatch": "4.0.3",
@@ -5218,6 +5246,23 @@
}
}
},
"node_modules/@schematics/angular/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@sentry-internal/tracing": {
"version": "7.120.4",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.4.tgz",
@@ -5421,16 +5466,16 @@
}
},
"node_modules/@tufjs/models/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz",
"integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -6632,13 +6677,26 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/brace-expansion/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/browserslist": {
@@ -7364,6 +7422,12 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/detect-it/-/detect-it-4.0.1.tgz",
"integrity": "sha512-dg5YBTJYvogK1+dA2mBUDKzOWfYZtHVba89SyZUhc4+e3i2tzgjANFg5lDRCd3UOtRcw00vUTMK8LELcMdicug==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -7375,6 +7439,15 @@
"node": ">=8"
}
},
"node_modules/detect-passive-events": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-passive-events/-/detect-passive-events-2.0.3.tgz",
"integrity": "sha512-QN/1X65Axis6a9D8qg8Py9cwY/fkWAmAH/edTbmLMcv4m5dboLJ7LcAi8CfaCON2tjk904KwKX/HTdsHC6yeRg==",
"license": "MIT",
"dependencies": {
"detect-it": "^4.0.1"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1467305",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz",
@@ -7845,9 +7918,9 @@
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7920,9 +7993,9 @@
"license": "MIT"
},
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -8577,16 +8650,16 @@
"license": "BSD-2-Clause"
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz",
"integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -8662,9 +8735,9 @@
}
},
"node_modules/hono": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.8.tgz",
"integrity": "sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz",
"integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8833,16 +8906,16 @@
}
},
"node_modules/ignore-walk/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz",
"integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -10409,13 +10482,13 @@
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz",
"integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -11641,9 +11714,9 @@
}
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -11846,9 +11919,9 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -12551,9 +12624,9 @@
}
},
"node_modules/swiper": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.0.tgz",
"integrity": "sha512-BD4CpAOOyEvZ2f0CDx362ea+vmOwukVcmbsQx+0BhRIaBUz8wvcCd//E7RFmvBZCrfyqXCHUVqmgUwts6ywlxw==",
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.2.tgz",
"integrity": "sha512-4gILrI3vXZqoZh71I1PALqukCFgk+gpOwe1tOvz5uE9kHtl2gTDzmYflYCwWvR4LOvCrJi6UEEU+gnuW5BtkgQ==",
"funding": [
{
"type": "patreon",
@@ -12570,9 +12643,9 @@
}
},
"node_modules/tar": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {

View File

@@ -11,6 +11,7 @@
},
"private": true,
"dependencies": {
"@angular-slider/ngx-slider": "^21.0.0",
"@angular/animations": "~21.1.0",
"@angular/cdk": "~21.1.0",
"@angular/common": "~21.1.0",

View File

@@ -13,6 +13,8 @@ export const routes: Routes = [
{ 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.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT},
{ path: RouterConstants.PENDULUM.PATH, component: RouterConstants.PENDULUM.COMPONENT},
{ path: RouterConstants.CLOTH.PATH, component: RouterConstants.CLOTH.COMPONENT}
];

View File

@@ -15,6 +15,10 @@ export class AssetsConstants {
static readonly DIPLOMA = '/assets/projects/diploma/Dahm2010-Diplomarbeit.pdf';
//project images
static readonly PLAYGROUND_IMAGES = [
'/assets/projects/playground/1.png'
];
static readonly EL_MUCHO_IMAGES = [
'/assets/projects/el-mucho/1.jpg',
'/assets/projects/el-mucho/2.jpg',

View File

@@ -8,6 +8,8 @@ import {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.comp
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 {
@@ -65,6 +67,18 @@ export class RouterConstants {
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 = {
PATH: 'imprint',
LINK: '/imprint',

View File

@@ -1,6 +1,6 @@
export class UrlConstants {
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 ASTAR_WIKI = 'https://de.wikipedia.org/wiki/A*-Algorithmus'
static readonly BUBBLE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Bubblesort'
@@ -17,4 +17,10 @@
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'
}

View File

@@ -1,6 +1,6 @@
<app-particles-background></app-particles-background>
<app-topbar />
<main class="container app-surface">
<main class="app-container app-surface">
<router-outlet />
</main>

View File

@@ -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;
}

View File

@@ -2,12 +2,13 @@ import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {TopbarComponent} from '../topbar/topbar.component';
import {TranslatePipe} from '@ngx-translate/core';
import {ParticleBackgroundComponent} from '../../shared/components/particles-background/particles-background.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, TopbarComponent, TranslatePipe],
imports: [RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})

View File

@@ -1,11 +1,7 @@
<mat-toolbar class="topbar" color="primary" (keydown)="onKeydown($event)">
<a class="brand" routerLink="/">
<img class="logo-dot"
src="{{AssetsConstants.LOGO}}"
alt="" aria-hidden="true"
draggable="false"
oncontextmenu="return false;"
>
<img class="logo-dot" src="{{AssetsConstants.LOGO}}" alt="" aria-hidden="true" draggable="false"
oncontextmenu="return false;">
<span class="brand-text">{{ 'APP.TITLE' | translate }}</span>
</a>
@@ -17,11 +13,7 @@
</nav>
<!-- Mobile nav menu button -->
<button
mat-icon-button
class="nav-menu-btn"
[matMenuTriggerFor]="navMenu"
aria-label="Open navigation">
<button mat-icon-button class="nav-menu-btn" [matMenuTriggerFor]="navMenu" aria-label="Open navigation">
<mat-icon>menu</mat-icon>
</button>
@@ -46,7 +38,8 @@
<span class="spacer"></span>
<!-- 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>
</button>
@@ -58,7 +51,7 @@
<span>{{ 'LANG.DE' | translate }}</span>
@if (lang.lang() === 'de')
{
<mat-icon >check</mat-icon>
<mat-icon>check</mat-icon>
}
</button>
<button mat-menu-item (click)="setLang('en')">
@@ -66,7 +59,7 @@
<span>{{ 'LANG.EN' | translate }}</span>
@if (lang.lang() === 'en')
{
<mat-icon>check</mat-icon>
<mat-icon>check</mat-icon>
}
</button>
</div>

View File

@@ -1,83 +1,76 @@
/* ---- Topbar Host & Base ---- */
:host {
position: sticky;
top: 0;
z-index: 100;
display: block;
}
.topbar {
position: sticky; top: 0; z-index: 100;
backdrop-filter: saturate(1.1) blur(8px);
background:
color-mix(in oklab, var(--app-topbar-bg) 80%, transparent);
border-bottom: 1px solid rgba(0,0,0,.08);
/* Erzeugt den Milchglas-Effekt */
backdrop-filter: saturate(1.1) blur(8px);
-webkit-backdrop-filter: saturate(1.1) blur(8px);
/* Safari Support */
/* 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 {
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; }
.spacer { flex: 1; }
.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;
}
.brand-text {
font-weight: 600;
letter-spacing: .2px;
font-size: clamp(1rem, 3vw, 1.2rem);
}
}
::ng-deep .mat-mdc-menu-item .mdc-list-item__primary-text {
display: flex;
align-items: center;
gap: .5rem;
/* ---- Navigation ---- */
.nav {
position: absolute;
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 {
display: none;
}
@media (max-width: 760px) {
.topbar .nav {
display: none;
}
.nav-menu-btn {
display: inline-flex;
}
}
.spacer {
flex: 1;
}
/* ---- Mobile Responsiveness ---- */
@media (max-width: 760px) {
.nav {
display: none;
}
.nav-menu-btn {
display: inline-flex;
}
.brand {
flex: unset;
}
}

View File

@@ -1,42 +1,39 @@
<section class="about">
<mat-card class="hero">
<div class="photo">
<img
[ngSrc]="AssetsConstants.ME"
width="421" height="512"
alt="{{ 'ABOUT.ALT.PROFILE' | translate }}"
draggable="false"
oncontextmenu="return false;"
priority />
</div>
<div class="hero-flex-container">
<div class="photo">
<img [ngSrc]="AssetsConstants.ME" width="421" height="512" alt="{{ 'ABOUT.ALT.PROFILE' | translate }}"
draggable="false" oncontextmenu="return false;" priority />
</div>
<div class="intro">
<h1>{{ 'ABOUT.HELLO' | translate }}</h1>
<p class="lead">
{{ 'ABOUT.LEAD' | translate }}
</p>
<div class="intro">
<h1>{{ 'ABOUT.HELLO' | translate }}</h1>
<p class="lead">
{{ 'ABOUT.LEAD' | translate }}
</p>
<div class="meta">
<div class="row">
<mat-icon aria-hidden="true">work</mat-icon>
<span>{{ 'ABOUT.ROLE' | translate }}</span>
</div>
<div class="row">
<mat-icon aria-hidden="true">location_on</mat-icon>
<span>{{ 'ABOUT.LOCATION' | translate }}</span>
</div>
<div class="row">
<div class="meta">
<div class="row">
<mat-icon aria-hidden="true">work</mat-icon>
<span>{{ 'ABOUT.ROLE' | translate }}</span>
</div>
<div class="row">
<mat-icon aria-hidden="true">location_on</mat-icon>
<span>{{ 'ABOUT.LOCATION' | translate }}</span>
</div>
<div class="row">
<mat-icon aria-hidden="true">email</mat-icon>
<a href="" (click)="SharedFunctions.openMail($event)">
{{ 'ABOUT.CONTACT_ME' | translate }}
</a>
</div>
<div class="row">
<mat-icon svgIcon="github"></mat-icon>
<a href="{{UrlConstants.GIT_HUB}}" target="_blank" rel="noopener">GitHub</a>
<span>·</span>
<mat-icon svgIcon="linkedin"></mat-icon>
<a href="{{UrlConstants.LINKED_IN}}" target="_blank" rel="noopener">LinkedIn</a>
</div>
<div class="row">
<mat-icon>data_object</mat-icon>
<a href="{{UrlConstants.CODEBERG}}" target="_blank" rel="noopener">Codeberg</a>
<span>·</span>
<mat-icon svgIcon="linkedin"></mat-icon>
<a href="{{UrlConstants.LINKED_IN}}" target="_blank" rel="noopener">LinkedIn</a>
</div>
</div>
</div>
</div>
@@ -46,19 +43,28 @@
<h2>{{ 'ABOUT.SECTION.SKILLS' | translate }}</h2>
<div class="chip-groups">
<div>
<h3>{{ 'ABOUT.SECTION.PRIMARY' | translate }}</h3>
<mat-chip-set aria-label="Primary skills">
@for (s of primarySkills; track s) {
<mat-chip >{{ s | translate }}</mat-chip>
<h3>{{ 'ABOUT.SECTION.BACKEND_ARCH' | translate }}</h3>
<mat-chip-set aria-label="Backend and Architecture">
@for (s of skillsArchitecture; track s) {
<mat-chip>{{ s | translate }}</mat-chip>
}
</mat-chip-set>
</div>
<div>
<h3>{{ 'ABOUT.SECTION.TOOLSET' | translate }}</h3>
<mat-chip-set aria-label="Toolset">
@for (t of toolset; track t) {
<mat-chip>{{ t | translate }}</mat-chip>
<h3>{{ 'ABOUT.SECTION.INFRA_CLOUD' | translate }}</h3>
<mat-chip-set aria-label="Infrastructure and Cloud">
@for (s of skillsCore; track s) {
<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>
</div>
@@ -69,39 +75,34 @@
<h2 style="margin-left: 0.5rem;">{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
<div class="xp-list">
@for (entry of xpKeys; track entry.key) {
<div class="xp-item">
<div class="xp-head-grid">
<div class="logo-wrap">
<img
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 class="xp-item">
<div class="xp-head-grid">
<div class="logo-wrap">
<img src="{{entry.logo}}" alt="" class="company-logo" aria-hidden="true" />
</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 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 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)
{
<mat-divider></mat-divider>
}
@if(entry.key !== xpKeys.at(xpKeys.length-1)?.key)
{
<mat-divider></mat-divider>
}
}
</div>
</mat-card>
@@ -118,21 +119,16 @@
{{ (entry.key + '.DESCRIPTION') | translate }}
</div>
@if (entry.externalLink) {
<div class="link-row">
<a class="link-with-icon"
href="{{entry.externalLink}}"
target="_blank"
rel="noopener noreferrer">
<mat-icon>open_in_new</mat-icon>
{{ (entry.key + '.LINK_EXTERNAL') | translate }}
</a>
</div>
<div class="link-row">
<a class="link-with-icon" href="{{entry.externalLink}}" target="_blank" rel="noopener noreferrer">
<mat-icon>open_in_new</mat-icon>
{{ (entry.key + '.LINK_EXTERNAL') | translate }}
</a>
</div>
}
<div class="link-row">
<a class="link-with-icon"
[routerLink]="['/projects']"
[queryParams]="{ project: entry.identifier }"
rel="noopener noreferrer">
<a class="link-with-icon" [routerLink]="['/projects']" [queryParams]="{ project: entry.identifier }"
rel="noopener noreferrer">
<mat-icon>link</mat-icon>
{{ (entry.key + '.LINK_INTERNAL') | translate }}
</a>
@@ -148,9 +144,9 @@
@if(entry.key !== projectKeys.at(projectKeys.length-1)?.key)
{
<mat-divider></mat-divider>
<mat-divider></mat-divider>
}
}
}
</div>
</mat-card>
@@ -160,18 +156,18 @@
<div class="xp-list">
<div class="xp-item">
@for (entry of educationKeys; track entry.key) {
<div class="head-row">
<strong>{{ (entry.key + '.WHERE') | translate }}</strong>
<span class="time">{{ (entry.key + '.WHEN') | translate }}</span>
</div>
<div class="company-row">
{{ (entry.key + '.WHAT') | translate }}
</div>
<div class="head-row">
<strong>{{ (entry.key + '.WHERE') | translate }}</strong>
<span class="time">{{ (entry.key + '.WHEN') | translate }}</span>
</div>
<div class="company-row">
{{ (entry.key + '.WHAT') | translate }}
</div>
@if(entry.key !== educationKeys.at(educationKeys.length-1)?.key)
{
<mat-divider style="margin-top: .5rem; margin-bottom: .5rem"></mat-divider>
}
@if(entry.key !== educationKeys.at(educationKeys.length-1)?.key)
{
<mat-divider style="margin-top: .5rem; margin-bottom: .5rem"></mat-divider>
}
}
</div>
</div>

View File

@@ -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;
}
}

View File

@@ -126,26 +126,30 @@ export class AboutComponent {
}
]
primarySkills = [
skillsCore = [
'ABOUT.SKILLS.JAVA',
'ABOUT.SKILLS.SPRING',
'ABOUT.SKILLS.ANGULAR',
'ABOUT.SKILLS.DOCKER',
'ABOUT.SKILLS.UNITY',
'ABOUT.SKILLS.PYTHON',
'ABOUT.SKILLS.TYPESCRIPT',
'ABOUT.SKILLS.CSHARP',
'ABOUT.SKILLS.TYPESCRIPT'
'ABOUT.SKILLS.PYTHON'
];
toolset = [
'ABOUT.TOOLS.GIT',
'ABOUT.TOOLS.GITHUB',
'ABOUT.TOOLS.GITLAB',
'ABOUT.TOOLS.JENKINS',
skillsArchitecture = [
'ABOUT.SKILLS.ARCH_MICROSERVICES',
'ABOUT.SKILLS.ARCH_CLOUD',
'ABOUT.TOOLS.DOCKER',
'ABOUT.TOOLS.K8S',
'ABOUT.TOOLS.POSTGRES',
'ABOUT.TOOLS.MONGO',
'ABOUT.TOOLS.GRAFANA',
'ABOUT.TOOLS.JENKINS',
'ABOUT.TOOLS.POSTGRES'
];
skillsEngineering = [
'ABOUT.SKILLS.ENG_ALGO',
'ABOUT.SKILLS.ENG_SIM',
'ABOUT.SKILLS.ENG_GPU',
'ABOUT.SKILLS.UNITY',
'ABOUT.SKILLS.ENG_PERF'
];
protected readonly UrlConstants = UrlConstants;

View File

@@ -1,8 +1,9 @@
<div class="container">
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
<div class="category-cards">
<div class="card-grid">
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
</div>
<div class="card-grid">
@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-title>{{ category.title | translate }}</mat-card-title>
</mat-card-header>
@@ -12,4 +13,3 @@
</mat-card>
}
</div>
</div>

View File

@@ -1,21 +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;
&:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
}
}

View File

@@ -1,6 +1,6 @@
import { Component, OnInit, inject } from '@angular/core';
import { AlgorithmsService } from './service/algorithms.service';
import { AlgorithmCategory } from './models/algorithm-category';
import { AlgorithmsService } from './algorithms.service';
import { AlgorithmCategory } from './algorithm-category';
import { Observable } from 'rxjs';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { AlgorithmCategory } from '../models/algorithm-category';
import { AlgorithmCategory } from './algorithm-category';
import { Observable, of } from 'rxjs';
import {RouterConstants} from '../../../constants/RouterConstants';
import {RouterConstants} from '../../constants/RouterConstants';
@Injectable({
providedIn: 'root'
@@ -44,6 +44,18 @@ export class AlgorithmsService {
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
}
];

View 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>

View 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);
});
}
}

View 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;
}

View 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;
}
}
`;

View File

@@ -1,4 +1,4 @@
<mat-card class="container">
<mat-card class="algo-container">
<mat-card-header>
<mat-card-title>{{ 'GOL.TITLE' | translate }}</mat-card-title>
</mat-card-header>
@@ -35,8 +35,8 @@
}
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p>
</div>
<div class="grid-size">
<mat-form-field appearance="outline" class="grid-field">
<div class="input-container">
<mat-form-field appearance="outline" class="input-field">
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
<input
matInput
@@ -47,7 +47,7 @@
(ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
/>
</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>
<input
matInput
@@ -58,7 +58,7 @@
(ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
/>
</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>
<input
matInput

View File

@@ -1,4 +1,4 @@
<mat-card class="container">
<mat-card class="algo-container">
<mat-card-header>
<mat-card-title>{{ 'FRACTAL.TITLE' | translate }}</mat-card-title>
</mat-card-header>
@@ -29,7 +29,13 @@
<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"

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
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';
@@ -8,11 +8,12 @@ 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} from '../../../shared/rendering/canvas/babylon-canvas.component';
import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader';
import {PointerEventTypes, PointerInfo, Scene, ShaderMaterial, Vector2} from '@babylonjs/core';
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',
@@ -30,7 +31,8 @@ import {MatIcon} from '@angular/material/icon';
FormsModule,
BabylonCanvas,
MatButton,
MatIcon
MatIcon,
NgxSliderModule
],
templateUrl: './fractal.component.html',
styleUrl: './fractal.component.scss',
@@ -79,12 +81,24 @@ export class FractalComponent implements OnInit {
};
// --- 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;
@@ -108,14 +122,13 @@ export class FractalComponent implements OnInit {
}
onAlgorithmChange(algoName: string): void {
this.onReset()
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 {
@@ -128,14 +141,22 @@ export class FractalComponent implements OnInit {
}
onReset(): void {
this.zoom = 0.2;
this.offsetX = 0.0;
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(scene: Scene): void {
scene.onPointerObservable.add((pointerInfo) => {
onSceneReady(event: SceneEventData): void {
event.scene.onPointerObservable.add((pointerInfo) => {
switch (pointerInfo.type) {
case PointerEventTypes.POINTERDOWN:
@@ -161,14 +182,13 @@ export class FractalComponent implements OnInit {
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;
this.isDragging = false;
this.dragStartPoint = null;
}
private onPointerMove(info: PointerInfo): void {
@@ -216,6 +236,8 @@ export class FractalComponent implements OnInit {
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);
@@ -223,6 +245,13 @@ export class FractalComponent implements OnInit {
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;

View File

@@ -1,4 +1,4 @@
<mat-card class="container">
<mat-card class="algo-container">
<mat-card-header>
<mat-card-title>{{ 'FRACTAL3D.TITLE' | translate }}</mat-card-title>
</mat-card-header>

View File

@@ -7,7 +7,7 @@ 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/rendering/canvas/babylon-canvas.component';
import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/components/render-canvas/babylon-canvas.component';
@Component({
selector: 'app-fractal3d',

View File

@@ -5,7 +5,14 @@
@for (algo of algorithmInformation.entries; track algo)
{
<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>
</p>
}

View File

@@ -10,5 +10,5 @@ export interface AlgorithmEntry {
name: string;
description: string;
link: string;
translateName?: boolean;
}

View File

@@ -1,4 +1,4 @@
<mat-card class="container">
<mat-card class="algo-container">
<mat-card-header>
<mat-card-title>{{ 'LABYRINTH.TITLE' | translate }}</mat-card-title>
</mat-card-header>

View File

@@ -1,80 +1,80 @@
<mat-card class="container">
<mat-card-header>
<mat-card-title>{{ 'PATHFINDING.TITLE' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-information [algorithmInformation]="algoInformation"/>
<mat-card class="algo-container">
<mat-card-header>
<mat-card-title>{{ 'PATHFINDING.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)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
</div>
<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: '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: false, scenario: 'normal'})">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
</div>
<div class="controls-container">
<div class="controls-panel">
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
</div>
<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: '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: false, scenario: 'normal'})">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
</div>
<div class="controls-panel">
<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.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.None">{{ 'PATHFINDING.CLEAR_NODE' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div class="controls-panel">
<div class="grid-size">
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
<input
matInput
type="number"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
[(ngModel)]="gridRows"
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
/> </mat-form-field>
<div class="controls-panel">
<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.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.None">{{ 'PATHFINDING.CLEAR_NODE' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div class="controls-panel">
<div class="input-container">
<mat-form-field appearance="outline" class="input-field">
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
<input
matInput
type="number"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
[(ngModel)]="gridRows"
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
/> </mat-form-field>
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
<input
matInput
type="number"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
[(ngModel)]="gridCols"
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
/> </mat-form-field>
<mat-form-field appearance="outline" class="input-field">
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
<input
matInput
type="number"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
[(ngModel)]="gridCols"
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
/> </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 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]="createPathfindingNode"
[getNodeColorFn]="getPathfindingNodeColor"
[applySelectionFn]="applyPathfindingSelection"
[backgroundColor]="'lightgray'"
(gridChange)="grid = $event"
></app-generic-grid>
</mat-card-content>
</mat-card>
<app-generic-grid
[gridRows]="gridRows"
[gridCols]="gridCols"
[minGridSize]="MIN_GRID_SIZE"
[maxGridSize]="MAX_GRID_SIZE"
[maxGridPx]="MAX_GRID_PX"
[createNodeFn]="createPathfindingNode"
[getNodeColorFn]="getPathfindingNodeColor"
[applySelectionFn]="applyPathfindingSelection"
[backgroundColor]="'lightgray'"
(gridChange)="grid = $event"
></app-generic-grid>
</mat-card-content>
</mat-card>

View 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>

View 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;
}
}

View 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

View 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;

View 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;
}
`;

View File

@@ -1,4 +1,4 @@
<mat-card class="container sorting-card">
<mat-card class="algo-container sorting-card">
<mat-card-header>
<mat-card-title>{{ 'SORTING.TITLE' | translate }}</mat-card-title>
</mat-card-header>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,63 +1,90 @@
<h2 mat-dialog-title>{{ project.title | translate }}</h2>
<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>
@for(bullet of project.bulletPoints; track bullet) {
<li>{{ bullet | translate }}</li>
}
</ul>
<div class="features-list">
<ul>
@for(bullet of project.bulletPoints; track bullet) {
<li>{{ bullet | translate }}</li>
}
</ul>
</div>
@if (project.images.length > 0)
{
<swiper-container
class="my-swiper"
[attr.slides-per-view]="1.2"
[attr.space-between]="12"
[attr.navigation]="true"
[attr.pagination]="true"
[attr.keyboard]="true"
style="width: 100%;"
>
@for (img of project.images; track img) {
<div class="insight-grid">
<div class="insight-card technical">
<div class="insight-header">
<mat-icon>settings_suggest</mat-icon>
<h3>{{ 'PROJECTS.SECTION.TECHNICAL' | translate }}</h3>
</div>
<ul>
@for(challenge of project.challenges; track challenge) {
<li>{{ challenge | translate }}</li>
}
</ul>
</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>
<img
class="slide-img"
[src]="img.url"
[alt]="project.title | translate"
/>
<img class="slide-img" [src]="img.url" [alt]="project.title | translate" />
@if (img.source) {
<div class="slide-source">
{{ img.source }}
</div>
<div class="slide-source">
{{ img.source }}
</div>
}
</swiper-slide>
}
</swiper-container>
}
<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>
}
</swiper-container>
</div>
}
@if(project.assets)
{
<a mat-button href="{{project.assets}}" rel="noopener noreferrer">
<mat-icon>download</mat-icon>
{{ 'PROJECTS.DOWNLOAD' | translate}}
</a>
}
<div class="footer-details">
<div class="tech-stack">
<mat-chip-set aria-label="Technologies">
@for(tech of project.technologies; track tech) {
<mat-chip>{{tech}}</mat-chip>
}
</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>
</mat-dialog-content>

View File

@@ -1,81 +1,117 @@
.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 {
border-radius: 12px;
}
.my-swiper::part(pagination) {
bottom: 12px;
}
swiper-slide {
border-radius: 12px;
overflow: hidden;
.project-dialog-layout {
display: flex;
flex-direction: column;
background-color: #222;
gap: 1.5rem;
}
.slide-img {
width: 100%;
height: auto;
max-height: 512px !important;
object-fit: contain;
display: block;
flex-shrink: 0;
.introduction {
font-size: 1.1rem;
line-height: 1.6;
opacity: 0.9;
margin-bottom: 1rem;
}
.slide-source {
font-size: 0.75rem;
color: #aaa;
background: #2a2a2a;
padding: 0.5rem;
text-align: right;
border-top: 1px solid #444;
}
ul {
padding-left: 20px;
.features-list {
margin-bottom: 1.5rem;
ul {
padding-left: 1.2rem;
li {
margin-bottom: 0.5rem;
}
}
}
li {
margin-bottom: 0.5rem;
.insight-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
mat-chip-set {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
.insight-card {
padding: 1.25rem;
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 {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
gap: 0.5rem;
a {
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
}
mat-dialog-actions {
justify-content: flex-end;
@media (max-width: 600px) {
.insight-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -1,4 +1,4 @@
<div class="project-grid">
<div class="card-grid">
@if (featuredProject(); as project) {
<mat-card class="project-card featured">
<mat-card-header>

View File

@@ -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;
}

View File

@@ -1,14 +1,14 @@
import {Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Subscription} from "rxjs";
import {MatCardModule} from "@angular/material/card";
import {MatChipsModule} from "@angular/material/chips";
import {MatIcon} from "@angular/material/icon";
import {TranslatePipe} from "@ngx-translate/core";
import {MatButtonModule} from "@angular/material/button";
import {MatDialog} from "@angular/material/dialog";
import {ProjectDialogComponent} from "./dialog/project-dialog.component";
import {AssetsConstants} from "../../constants/AssetsConstants";
import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from "rxjs";
import { MatCardModule } from "@angular/material/card";
import { MatChipsModule } from "@angular/material/chips";
import { MatIcon } from "@angular/material/icon";
import { TranslatePipe } from "@ngx-translate/core";
import { MatButtonModule } from "@angular/material/button";
import { MatDialog } from "@angular/material/dialog";
import { ProjectDialogComponent } from "./dialog/project-dialog.component";
import { AssetsConstants } from "../../constants/AssetsConstants";
export interface Projects {
identifier: string;
@@ -26,6 +26,8 @@ export interface Projects {
url: string
}[],
bulletPoints: string[],
challenges: string[],
learnings: string[],
isFeatured: boolean,
technologies: string[]
}
@@ -46,124 +48,165 @@ export interface Projects {
})
export class ProjectsComponent implements OnInit, OnDestroy {
private readonly route = inject(ActivatedRoute);
private readonly dialog = inject(MatDialog);
private readonly router = inject(Router);
private queryParamSub: Subscription | undefined;
private readonly route = inject(ActivatedRoute);
private readonly dialog = inject(MatDialog);
private readonly router = inject(Router);
private queryParamSub: Subscription | undefined;
allProjects: Projects[] = [
{
identifier: "playground",
title: 'PROJECTS.PLAYGROUND.TITLE',
shortDescription: 'PROJECTS.PLAYGROUND.SHORT_DESCRIPTION',
introduction: 'PROJECTS.PLAYGROUND.INTRODUCTION',
images: [],
icon: 'web',
assets: '',
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://andreas-dahm.eu'}],
bulletPoints: [
'PROJECTS.PLAYGROUND.BULLET_1',
'PROJECTS.PLAYGROUND.BULLET_2',
'PROJECTS.PLAYGROUND.BULLET_3',
'PROJECTS.PLAYGROUND.BULLET_4',
],
isFeatured: false,
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'GitHub Actions', 'Docker']
},
{
identifier: "elmucho",
title: 'PROJECTS.EL_MUCHO.TITLE',
shortDescription: 'PROJECTS.EL_MUCHO.SHORT_DESCRIPTION',
introduction: 'PROJECTS.EL_MUCHO.INTRODUCTION',
images: AssetsConstants.EL_MUCHO_IMAGES.map(url => ({ url, source: '' })),
icon: 'sports_esports',
assets: '',
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://store.steampowered.com/app/1532640/El_Mucho/'}],
bulletPoints: [
'PROJECTS.EL_MUCHO.BULLET_1',
'PROJECTS.EL_MUCHO.BULLET_2',
'PROJECTS.EL_MUCHO.BULLET_3',
'PROJECTS.EL_MUCHO.BULLET_4',
],
isFeatured: true,
technologies: ['Unity', 'C#', 'Steamworks', 'Git']
},
{
identifier: "gamejams",
title: 'PROJECTS.GAME_JAMS.TITLE',
shortDescription: 'PROJECTS.GAME_JAMS.SHORT_DESCRIPTION',
introduction: 'PROJECTS.GAME_JAMS.INTRODUCTION',
images: AssetsConstants.GAME_JAMS_IMAGES.map(url => ({ url, source: '' })),
icon: 'videogame_asset',
assets: '',
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://itch.io/c/6628860/lobos-collection'}],
bulletPoints: [
'PROJECTS.GAME_JAMS.BULLET_1',
'PROJECTS.GAME_JAMS.BULLET_2',
'PROJECTS.GAME_JAMS.BULLET_3',
'PROJECTS.GAME_JAMS.BULLET_4',
],
isFeatured: false,
technologies: ['Unity', 'C#', 'Git']
},
{
identifier: "diploma",
title: 'PROJECTS.DIPLOMA.TITLE',
shortDescription: 'PROJECTS.DIPLOMA.SHORT_DESCRIPTION',
introduction: 'PROJECTS.DIPLOMA.INTRODUCTION',
images: AssetsConstants.DIPLOMA_IMAGES.map(url => ({ url, source: '' })),
icon: 'history_edu',
assets: AssetsConstants.DIPLOMA,
links: [{name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://www.th-bingen.de'}],
bulletPoints: [
'PROJECTS.DIPLOMA.BULLET_1',
'PROJECTS.DIPLOMA.BULLET_2',
'PROJECTS.DIPLOMA.BULLET_3',
'PROJECTS.DIPLOMA.BULLET_4',
],
isFeatured: false,
technologies: ['C++', 'OpenGL', 'Qt', '3D-Scanner']
},
{
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://commons.wikimedia.org/wiki/File:Traefik.logo.png'}
],
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',
],
isFeatured: false,
technologies: ['Ubuntu Server', 'Docker', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale']
}
]
allProjects: Projects[] = [
{
identifier: "playground",
title: 'PROJECTS.PLAYGROUND.TITLE',
shortDescription: 'PROJECTS.PLAYGROUND.SHORT_DESCRIPTION',
introduction: 'PROJECTS.PLAYGROUND.INTRODUCTION',
images: AssetsConstants.PLAYGROUND_IMAGES.map(url => ({ url, source: '' })),
icon: 'web',
assets: '',
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://andreas-dahm.eu' }],
bulletPoints: [
'PROJECTS.PLAYGROUND.BULLET_1',
'PROJECTS.PLAYGROUND.BULLET_2',
'PROJECTS.PLAYGROUND.BULLET_3',
'PROJECTS.PLAYGROUND.BULLET_4',
],
challenges: [
'PROJECTS.PLAYGROUND.CHALLENGE_1',
'PROJECTS.PLAYGROUND.CHALLENGE_2',
],
learnings: [
'PROJECTS.PLAYGROUND.LEARNING_1',
'PROJECTS.PLAYGROUND.LEARNING_2',
],
isFeatured: false,
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'GitHub Actions', 'Docker']
},
{
identifier: "elmucho",
title: 'PROJECTS.EL_MUCHO.TITLE',
shortDescription: 'PROJECTS.EL_MUCHO.SHORT_DESCRIPTION',
introduction: 'PROJECTS.EL_MUCHO.INTRODUCTION',
images: AssetsConstants.EL_MUCHO_IMAGES.map(url => ({ url, source: '' })),
icon: 'sports_esports',
assets: '',
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://store.steampowered.com/app/1532640/El_Mucho/' }],
bulletPoints: [
'PROJECTS.EL_MUCHO.BULLET_1',
'PROJECTS.EL_MUCHO.BULLET_2',
'PROJECTS.EL_MUCHO.BULLET_3',
'PROJECTS.EL_MUCHO.BULLET_4',
],
challenges: [
'PROJECTS.EL_MUCHO.CHALLENGE_1',
'PROJECTS.EL_MUCHO.CHALLENGE_2',
'PROJECTS.EL_MUCHO.CHALLENGE_3',
],
learnings: [
'PROJECTS.EL_MUCHO.LEARNING_1',
'PROJECTS.EL_MUCHO.LEARNING_2',
],
isFeatured: true,
technologies: ['Unity', 'C#', 'Steamworks', 'Git']
},
{
identifier: "gamejams",
title: 'PROJECTS.GAME_JAMS.TITLE',
shortDescription: 'PROJECTS.GAME_JAMS.SHORT_DESCRIPTION',
introduction: 'PROJECTS.GAME_JAMS.INTRODUCTION',
images: AssetsConstants.GAME_JAMS_IMAGES.map(url => ({ url, source: '' })),
icon: 'videogame_asset',
assets: '',
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://itch.io/c/6628860/lobos-collection' }],
bulletPoints: [
'PROJECTS.GAME_JAMS.BULLET_1',
'PROJECTS.GAME_JAMS.BULLET_2',
'PROJECTS.GAME_JAMS.BULLET_3',
'PROJECTS.GAME_JAMS.BULLET_4',
],
challenges: [
'PROJECTS.GAME_JAMS.CHALLENGE_1',
'PROJECTS.GAME_JAMS.CHALLENGE_2',
],
learnings: [
'PROJECTS.GAME_JAMS.LEARNING_1',
'PROJECTS.GAME_JAMS.LEARNING_2',
],
isFeatured: false,
technologies: ['Unity', 'C#', 'Git']
},
{
identifier: "diploma",
title: 'PROJECTS.DIPLOMA.TITLE',
shortDescription: 'PROJECTS.DIPLOMA.SHORT_DESCRIPTION',
introduction: 'PROJECTS.DIPLOMA.INTRODUCTION',
images: AssetsConstants.DIPLOMA_IMAGES.map(url => ({ url, source: '' })),
icon: 'history_edu',
assets: AssetsConstants.DIPLOMA,
links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://www.th-bingen.de' }],
bulletPoints: [
'PROJECTS.DIPLOMA.BULLET_1',
'PROJECTS.DIPLOMA.BULLET_2',
'PROJECTS.DIPLOMA.BULLET_3',
'PROJECTS.DIPLOMA.BULLET_4',
],
challenges: [
'PROJECTS.DIPLOMA.CHALLENGE_1',
'PROJECTS.DIPLOMA.CHALLENGE_2',
],
learnings: [
'PROJECTS.DIPLOMA.LEARNING_1',
'PROJECTS.DIPLOMA.LEARNING_2',
],
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));
otherProjects = computed(() => this.allProjects.filter(p => !p.isFeatured));
ngOnInit(): void {
window.scrollTo({ top: 0, behavior: 'smooth' });
setTimeout(() =>{ this.dialogOpenFunction(); },10);
setTimeout(() => { this.dialogOpenFunction(); }, 10);
}
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 => {
const projectIdentifier = params.get('project');
if (projectIdentifier) {

View File

@@ -1 +1,3 @@
<canvas #gridCanvas></canvas>
<div class="canvas-container">
<canvas #gridCanvas></canvas>
</div>

View File

@@ -0,0 +1 @@
<canvas #canvas></canvas>

View File

@@ -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;
}

View File

@@ -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);
};
}

View File

@@ -1,16 +1,23 @@
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/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[];
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: [],
@@ -25,44 +32,55 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
@Input({ required: true }) config!: RenderConfig;
@Input() renderCallback?: RenderCallback;
@Output() sceneReady = new EventEmitter<Scene>();
@Output() sceneReady = new EventEmitter<SceneEventData>();
@Output() sceneResized = new EventEmitter<SceneEventData>();
private engine!: Engine;
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.ngZone.runOutsideAngular(() => {
this.initBabylon();
});
this.initBabylon().then(() => { console.log("Engine initialized"); });
}
/*ngOnChanges(changes: SimpleChanges): void {
//if something changes during runtime, new materials are necessary ans needs maybe build here
}*/
ngOnDestroy(): void {
window.removeEventListener('resize', this.resizeHandler);
const canvas = this.canvasRef?.nativeElement;
if (canvas) {
//remove listener if needed
canvas.removeEventListener('wheel', this.wheelHandler);
}
if (this.engine) {
this.engine.dispose();
}
}
private initBabylon(): void {
private async initBabylon(): Promise<void> {
const canvas = this.canvasRef.nativeElement;
this.engine = new Engine(canvas, true);
this.scene = new Scene(this.engine);
this.setupCamera(canvas);
canvas.addEventListener('wheel', (evt: WheelEvent) => evt.preventDefault(), { passive: false });
this.createShaderMaterial();
this.createFullScreenRect();
this.sceneReady.emit(this.scene);
this.addRenderLoop(canvas);
this.addResizeHandler();
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) {
@@ -102,8 +120,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
}
private createFullScreenRect() {
const plane = MeshBuilder.CreatePlane("plane", {size: 110}, this.scene);
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;
@@ -116,6 +136,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
}
private createShaderMaterial() {
if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) {
return;
}
this.shaderMaterial = new ShaderMaterial(
"shaderMaterial",
this.scene,
@@ -125,7 +149,9 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
},
{
attributes: ["position", "uv"],
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames]
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames],
uniformBuffers: this.config.uniformBufferNames ?? [],
shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL
}
);
this.shaderMaterial.disableDepthWrite = true;
@@ -141,19 +167,31 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
}
// default uniforms which maybe each scene has
this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
if (this.shaderMaterial) {
this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
}
this.scene.render();
});
}
private addResizeHandler() {
window.addEventListener('resize', () => {
private handleResize(): void {
if (this.engine) {
this.engine.resize();
if (this.config.mode === '2D' && this.camera instanceof ArcRotateCamera && this.camera.mode === Camera.ORTHOGRAPHIC_CAMERA) {
//maybe update the aspect ratio here
}
}
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
});
}
}

View File

@@ -1,2 +0,0 @@
.canvas-container { width: 100%; height: 1000px; }
canvas { width: 100%; height: 100%; touch-action: none; border-width: 0; border-color: transparent; border-style: hidden; }

View File

@@ -1,7 +1,7 @@
{
{
"APP": {
"TITLE": "Playground",
"COPYRIGHT": "Bilder urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!"
"COPYRIGHT": "Bilder und Sourcecode sind urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!"
},
"TOPBAR": {
"ABOUT": "Über mich",
@@ -25,21 +25,29 @@
"CONTACT_ME": "Kontaktiere mich",
"SECTION": {
"SKILLS": "Fähigkeiten & Stack",
"PRIMARY": "Schwerpunkte",
"TOOLSET": "Toolset",
"BACKEND_ARCH": "Backend & Architektur",
"INFRA_CLOUD": "Infrastruktur & Cloud",
"SIM_ALGO": "Simulation & Algorithmen",
"EXPERIENCE": "Erfahrung",
"PROJECTS": "Projekte",
"EDUCATION": "Ausbildung"
},
"SKILLS": {
"JAVA": "Java 8/Java 21+",
"JAVA": "Java 8/21+",
"SPRING": "Spring Boot 2/3",
"ANGULAR": "Angular 20+",
"ANGULAR": "Angular 19+",
"DOCKER": "Docker",
"UNITY": "Unity",
"PYTHON": "Python",
"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": {
"GIT": "Git",
@@ -49,7 +57,8 @@
"K8S": "Kubernetes / k3d",
"POSTGRES": "PostgreSQL",
"MONGO": "MongoDB",
"GRAFANA": "Grafana/Prometheus"
"GRAFANA": "Grafana/Prometheus",
"DOCKER": "Docker"
},
"XP": {
"COMPANY8": {
@@ -241,50 +250,75 @@
"READ_MORE": "Mehr erfahren",
"LINK_TO_PROJECT": "Zum Projekt",
"CLOSE": "Schließen",
"SECTION": {
"TECHNICAL": "Technische Herausforderungen",
"LEARNINGS": "Learnings & Soft Skills"
},
"PLAYGROUND": {
"TITLE": "Playground Website",
"SHORT_DESCRIPTION": "Hier geht es um diese Webseite.",
"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.",
"BULLET_1": "Verwendung moderner Technologien und CI/CD-Pipelines (Angular 20+, Spring Boot 4, GitHub).",
"BULLET_2": "Präsentation persönlicher Projekte und kontinuierliche Verbesserung algorithmischer Fähigkeiten.",
"BULLET_3": "Vertiefung von JavaScript/TypeScript-, Angular- und Spring-Boot-Kenntnissen durch praktisches Arbeiten.",
"BULLET_4": "Die Seite ist Open Source und auf GitHub verfügbar."
"TITLE": "Playground Portfolio",
"SHORT_DESCRIPTION": "Full-Stack Portfolio mit interaktiven Algorithmus-Visualisierungen.",
"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": "Entwicklung mit Angular 19+ und Material Design.",
"BULLET_2": "Implementierung performanter Visualisierungen (WebGPU, Shader, Canvas).",
"BULLET_3": "Automatisierte CI/CD-Pipelines und Containerisierung mit Docker.",
"BULLET_4": "Internationalisierung (i18n) für globale Reichweite.",
"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": {
"TITLE": "Trouble with Tribble",
"SHORT_DESCRIPTION": "Ein Projekt, das die Einrichtung und Wartung eines Homeservers beschreibt, auf dem verschiedene Docker-Container für Self-Hosting-Dienste laufen.",
"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.",
"BULLET_1": "Self-Hosting verschiedener Dienste mit Docker.",
"BULLET_2": "CI/CD-Pipeline für die persönliche Website mit Gitea.",
"BULLET_3": "Sicherer Fernzugriff mit Tailscale und Traefik.",
"BULLET_4": "Netzwerkweites Blockieren von Werbung mit AdGuard Home."
"TITLE": "Self-Hosted Infrastructure",
"SHORT_DESCRIPTION": "Home-Infrastruktur mit Docker, Traefik und sicherer VPN-Anbindung.",
"INTRODUCTION": "Dokumentation und Aufbau einer privaten Cloud-Infrastruktur. Fokus liegt auf Datensouveränität, Automatisierung und Sicherheit.",
"BULLET_1": "Zentrale Verwaltung via Docker-Compose und Portainer.",
"BULLET_2": "Automatisches SSL-Management und Reverse-Proxy mit Traefik.",
"BULLET_3": "Private Versionskontrolle (Gitea) und Medien-Streaming (Jellyfin).",
"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": {
"TITLE": "El Mucho",
"SHORT_DESCRIPTION": "Hier geht es um mein ersten Spiel 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.",
"BULLET_1": "Veröffentlichung eines Spiels auf Steam und Integration der Steam-API.",
"BULLET_2": "Konzeption, Planung und vollständige Entwicklung eines eigenen Spiels.",
"BULLET_3": "Implementierung komplexer Algorithmen wie einer eigenen A*-Pfadfindungslogik und Spiel-KI.",
"BULLET_4": "Das Spiel wurde mit Unity und C# entwickelt."
"TITLE": "El Mucho (Steam Release)",
"SHORT_DESCRIPTION": "Rundenbasiertes Taktik-RPG, veröffentlicht auf Steam.",
"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": "Komplette Engine-Entwicklung in Unity (C#).",
"BULLET_2": "Integration von Steamworks-Funktionen (Achievements, Cloud Saves).",
"BULLET_3": "Entwicklung einer eigenen taktischen KI und Pfadfindungs-Logik.",
"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": {
"TITLE": "Game Jams",
"SHORT_DESCRIPTION": "Hier geht es meine Teilnahme an mehreren Game Jams.",
"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.",
"BULLET_1": "Planung eines realistischen Projektumfangs mit einem Team, der innerhalb von 48 Stunden umsetzbar ist.",
"BULLET_2": "Lernen, fokussiert und effizient unter strengen Zeitvorgaben zu arbeiten.",
"BULLET_3": "Die Freude zu erleben, in kurzer Zeit ein spielbares Projekt zu erstellen und andere damit spielen zu sehen.",
"BULLET_4": "Alle Projekte sind auf Itch.io verfügbar und spielbar."
"TITLE": "Rapid Prototyping & Game Jams",
"SHORT_DESCRIPTION": "Sammlung innovativer Spielkonzepte, entstanden in unter 48 Stunden.",
"INTRODUCTION": "Teilnahme an nationalen Wettbewerben (z.B. Beansjam). Hier geht es darum, unter extremem Zeitdruck funktionale und spaßige Prototypen zu erschaffen.",
"BULLET_1": "Fokus auf 'Core Game Loop' und schnelles Feedback.",
"BULLET_2": "Kollaborative Entwicklung in kleinen, agilen Teams.",
"BULLET_3": "Effektives Zeitmanagement und Scope-Kontrolle.",
"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": {
"TITLE": "Diplomarbeit",
"SHORT_DESCRIPTION": "Kollisionserkennung und Behandlung von komplexen Kleidungsstücken.",
"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.",
"BULLET_1": "Echtzeit behandlung von Kollisionserkennung und Behandlung.",
"BULLET_2": "Verstehen und Einschätzen von wissenschaftlichen Arbeiten.",
"BULLET_3": "Adaption und Weiterentwicklung von vorausgegangenen Forschungsarbeiten.",
"BULLET_4": "Die Arbeit wurde mit C++ und OpenGL geschrieben und in die Vidya-Software integriert."
"TITLE": "Wissenschaftliche Diplomarbeit",
"SHORT_DESCRIPTION": "Echtzeit-Kollisionserkennung für komplexe, flexible 3D-Objekte.",
"INTRODUCTION": "Forschungsarbeit im Bereich Computergraphik. Entwicklung eines Algorithmus zur physikalisch korrekten Simulation von Stoffen und Kleidung in Echtzeit.",
"BULLET_1": "Mathematische Modellierung von Mass-Spring-Systemen.",
"BULLET_2": "Low-Level Programmierung mit Java.",
"BULLET_3": "Optimierung durch räumliche Datenstrukturen (AABB Trees, Bounding Spheres).",
"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": {
@@ -386,7 +420,7 @@
"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 ästhetische Varianz, da jede Koordinate der Mandelbrot-Menge ein völlig eigenes, einzigartiges Julia-Fraktal erzeugt.",
"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:",
@@ -415,6 +449,52 @@
"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": {
"TITLE": "Algorithmen",
"PATHFINDING": {
@@ -441,6 +521,14 @@
"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",
"GRID_HEIGHT": "Höhe",
"GRID_WIDTH": "Beite"

View File

@@ -1,7 +1,7 @@
{
{
"APP": {
"TITLE": "Playground",
"COPYRIGHT": "Images protected by copyright, no use without permission!"
"COPYRIGHT": "Images and code protected by copyright, no use without permission!"
},
"TOPBAR": {
"ABOUT": "About me",
@@ -25,21 +25,29 @@
"CONTACT_ME": "Contact me",
"SECTION": {
"SKILLS": "Skills & Stack",
"PRIMARY": "Core",
"TOOLSET": "Toolset",
"BACKEND_ARCH": "Backend & Architecture",
"INFRA_CLOUD": "Infrastructure & Cloud",
"SIM_ALGO": "Simulation & Algorithms",
"EXPERIENCE": "Experience",
"PROJECTS": "Projects",
"EDUCATION": "Education"
},
"SKILLS": {
"JAVA": "Java 8/Java 21+",
"JAVA": "Java 8/21+",
"SPRING": "Spring Boot 2/3",
"ANGULAR": "Angular 20+",
"ANGULAR": "Angular 19+",
"DOCKER": "Docker",
"UNITY": "Unity",
"PYTHON": "Python",
"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": {
"GIT": "Git",
@@ -49,7 +57,8 @@
"K8S": "Kubernetes / k3d",
"POSTGRES": "PostgreSQL",
"MONGO": "MongoDB",
"GRAFANA": "Grafana/Prometheus"
"GRAFANA": "Grafana/Prometheus",
"DOCKER": "Docker"
},
"XP": {
"COMPANY8": {
@@ -138,7 +147,7 @@
"TIME": "Jul. 2002 Jun. 2005",
"HIGHLIGHTS": {
"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."
}
}
@@ -189,7 +198,7 @@
},
"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",
"HIGHLIGHTS": {
"P1": "Self-hosting of various services using Docker.",
@@ -241,50 +250,75 @@
"READ_MORE": "Read More",
"LINK_TO_PROJECT": "To the project",
"CLOSE": "Close",
"SECTION": {
"TECHNICAL": "Technical Challenges",
"LEARNINGS": "Learnings & Soft Skills"
},
"PLAYGROUND": {
"TITLE": "Playground Website",
"SHORT_DESCRIPTION": "This is about this website.",
"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.",
"BULLET_1": "Using modern technologies and CI/CD pipelines (Angular 20+, Spring Boot 4, GitHub).",
"BULLET_2": "Showcasing personal projects and improving algorithmic skills over time.",
"BULLET_3": "Deepening knowledge in JavaScript/TypeScript, Angular, Spring Boot and related technologies through hands-on practice.",
"BULLET_4": "The site is open source and available on GitHub."
"TITLE": "Playground Portfolio",
"SHORT_DESCRIPTION": "Full-stack portfolio with interactive algorithm visualizations.",
"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": "Development with Angular 19+ and Material Design.",
"BULLET_2": "Implementation of performant visualizations (WebGPU, Shader, Canvas).",
"BULLET_3": "Automated CI/CD pipelines and containerization with Docker.",
"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": {
"TITLE": "Trouble with Tribble",
"SHORT_DESCRIPTION": "A project detailing the setup and maintenance of a home server running various Docker containers for self-hosting services.",
"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.",
"BULLET_1": "Self-hosting of various services using Docker.",
"BULLET_2": "CI/CD pipeline for the personal website using Gitea.",
"BULLET_3": "Secure remote access with Tailscale and Traefik.",
"BULLET_4": "Network-wide ad-blocking with AdGuard Home."
"TITLE": "Self-Hosted Infrastructure",
"SHORT_DESCRIPTION": "Home infrastructure with Docker, Traefik, and secure VPN connectivity.",
"INTRODUCTION": "Documentation and construction of a private cloud infrastructure. Focus is on data sovereignty, automation, and security.",
"BULLET_1": "Central management via Docker-Compose and Portainer.",
"BULLET_2": "Automatic SSL management and reverse proxy with Traefik.",
"BULLET_3": "Private version control (Gitea) and media streaming (Jellyfin).",
"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": {
"TITLE": "El Mucho",
"SHORT_DESCRIPTION": "This is about my first game 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.",
"BULLET_1": "Publishing a game on Steam and integrating the Steam API.",
"BULLET_2": "Designing, planning and developing a complete game from scratch.",
"BULLET_3": "Implementing complex algorithms, including a custom A* pathfinding system and game AI logic.",
"BULLET_4": "The game was developed with Unity and C#."
"TITLE": "El Mucho (Steam Release)",
"SHORT_DESCRIPTION": "Turn-based tactical RPG, published on Steam.",
"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": "Complete engine development in Unity (C#).",
"BULLET_2": "Integration of Steamworks features (Achievements, Cloud Saves).",
"BULLET_3": "Development of a custom tactical AI and pathfinding logic.",
"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": {
"TITLE": "Game Jams",
"SHORT_DESCRIPTION": "This is about my participation at several game jams.",
"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.",
"BULLET_1": "Planning a realistic project scope with a team that can be built within 48 hours.",
"BULLET_2": "Learning to stay focused and work effectively under strict time constraints.",
"BULLET_3": "Experiencing the joy of creating a playable game in a short timeframe and seeing others enjoy it.",
"BULLET_4": "All projects are available and playable on Itch.io."
"TITLE": "Rapid Prototyping & Game Jams",
"SHORT_DESCRIPTION": "Collection of innovative game concepts, created in under 48 hours.",
"INTRODUCTION": "Participation in national competitions (e.g. BeansJam). The focus is on creating functional and fun prototypes under extreme time pressure.",
"BULLET_1": "Focus on 'Core Game Loop' and fast feedback.",
"BULLET_2": "Collaborative development in small, agile teams.",
"BULLET_3": "Effective time management and scope control.",
"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": {
"TITLE": "Diploma thesis",
"SHORT_DESCRIPTION": "Collision detection and handling of complex garments.",
"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.",
"BULLET_1": "Real-time handling of collision detection and response.",
"BULLET_2": "Understanding and evaluating scientific papers.",
"BULLET_3": "Adaptation and further development of previous research work.",
"BULLET_4": "The thesis was written with C++ and OpenGL and integrated into the Vidya software."
"TITLE": "Scientific Diploma Thesis",
"SHORT_DESCRIPTION": "Real-time collision detection for complex, flexible 3D objects.",
"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": "Mathematical modeling of mass-spring systems.",
"BULLET_2": "Low-level programming with Java.",
"BULLET_3": "Optimization through spatial data structures (AABB Trees, Bounding Spheres).",
"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": {
@@ -393,7 +427,7 @@
"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."
"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": {
@@ -414,6 +448,52 @@
"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": {
"TITLE": "Algorithms",
"PATHFINDING": {
@@ -440,6 +520,14 @@
"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",
"GRID_HEIGHT": "Height",
"GRID_WIDTH": "Width"

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,15 +1,13 @@
@use '@angular/material' as mat;
// ---- Themes ----
$light-theme: mat.define-theme((
color: ( theme-type: light, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
typography: ( brand-family: 'Inter, Roboto, Arial, sans-serif', bold-weight: 600 ),
density: ( scale: 0 ),
));
$light-theme: mat.define-theme((color: (theme-type: light, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
typography: (brand-family: 'Inter, Roboto, Arial, sans-serif', bold-weight: 600),
density: (scale: 0),
));
$dark-theme: mat.define-theme((
color: ( theme-type: dark, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
));
$dark-theme: mat.define-theme((color: (theme-type: dark, primary: mat.$cyan-palette, tertiary: mat.$orange-palette ),
));
// ---- Core + Components ----
@include mat.core-theme($light-theme);
@@ -23,11 +21,12 @@ $dark-theme: mat.define-theme((
/* ---- Custom variables ---- */
: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-fg: #{mat.get-theme-color($light-theme, on-surface)};
--app-logo-bg: #313131;
--app-card-background: #fafafa;
--app-topbar-bg: var(--app-card-background);
--card-radius: 18px;
--card-bg: var(--app-card-background);
@@ -37,12 +36,13 @@ $dark-theme: mat.define-theme((
--link-color: #38a7ff;
--link-color-hover: #66bfff;
}
.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-card-background: #313131;
--app-logo-bg: #313131;
--app-topbar-bg: var(--app-card-background);
--card-bg: var(--app-card-background);
@@ -51,7 +51,11 @@ $dark-theme: mat.define-theme((
}
/* ---- global background and tests ---- */
html, body { height: 100%; }
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Inter, Roboto, Arial, sans-serif;
@@ -62,10 +66,14 @@ body {
.material-symbols-outlined {
font-variation-settings:
"FILL" 0, /* 0 oder 1 */
"wght" 400, /* 100..700 */
"GRAD" 0, /* -50..200 */
"opsz" 24; /* 20..48 */
"FILL" 0,
/* 0 oder 1 */
"wght" 400,
/* 100..700 */
"GRAD" 0,
/* -50..200 */
"opsz" 24;
/* 20..48 */
}
/* smooth transition between theme change */
@@ -111,9 +119,9 @@ a {
box-shadow 200ms ease,
transform 200ms ease;
&.container {
&.algo-container {
width: 100%;
max-width: 1200px;
max-width: 1920px;
padding: 20px;
}
}
@@ -148,7 +156,8 @@ a {
.mat-accordion .mat-expansion-panel {
border-radius: var(--card-radius) !important;
background: var(--card-bg) !important;
overflow: hidden; /* ok */
overflow: hidden;
/* ok */
border: none !important;
}
@@ -220,8 +229,12 @@ a {
// algos
.container {
padding: 2rem;
.algo-container {
max-width: var(--app-maxWidth);
gap: clamp(1rem, 3vw, 1.5rem);
margin-right: 1rem;
margin-left: 0.5rem;
margin-top: auto;
}
.algo-info {
@@ -262,13 +275,13 @@ a {
}
}
.grid-size {
.input-container {
display: flex;
gap: 0.75rem;
align-items: center;
flex-wrap: wrap;
.grid-field {
.input-field {
width: 150px;
}
}
@@ -276,6 +289,7 @@ a {
canvas {
border: 1px solid lightgray;
display: block;
margin: 0 auto;
max-width: 100%;
}
@@ -294,13 +308,49 @@ canvas {
vertical-align: middle;
margin-right: 5px;
&.start { background-color: green; }
&.end { background-color: red; }
&.wall { background-color: black; }
&.visited { background-color: skyblue; }
&.path { background-color: gold; }
&.empty { background-color: lightgray; }
&.alive { background-color: black; }
&.start {
background-color: green;
}
&.end {
background-color: red;
}
&.wall {
background-color: black;
}
&.visited {
background-color: skyblue;
}
&.path {
background-color: gold;
}
&.empty {
background-color: lightgray;
}
&.alive {
background-color: black;
}
&.L1 {
background-color: yellow;
}
&.L2 {
background-color: magenta;
}
&.M1 {
background-color: red;
}
&.M2 {
background-color: green;
}
}
}
@@ -310,33 +360,529 @@ canvas {
margin-bottom: 1rem;
}
/* Sorting Visualization */
.sorting-visualization-area {
/* Sorting Visualization & Canvas */
.sorting-visualization-area,
.visualization-area {
display: flex;
align-items: flex-end;
height: 300px; /* Max height for bars */
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
height: clamp(200px, 40vh, 400px);
border-bottom: 1px solid var(--app-fg);
margin-bottom: clamp(10px, 3vw, 20px);
gap: 1px;
background-color: #f0f0f0;
background-color: var(--card-bg);
.sorting-bar {
.sorting-bar,
.bar {
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;
width: 10px; /* Default width, flex-grow will adjust */
min-width: 1px; /* Ensure bars are always visible */
width: 10px;
min-width: 1px;
&.unsorted {
background-color: #424242;
}
&.comparing {
background-color: #ffeb3b; /* Yellow for comparing */
background-color: #ffeb3b;
}
&.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;
}