feature/4ColorProblem #32
@@ -15,5 +15,6 @@ export const routes: Routes = [
|
||||
{ path: RouterConstants.FRACTAL3d.PATH, loadComponent: () => import('./pages/algorithms/fractal3d/fractal3d.component').then(m => m.Fractal3dComponent) },
|
||||
{ path: RouterConstants.PENDULUM.PATH, loadComponent: () => import('./pages/algorithms/pendulum/pendulum.component').then(m => m.default) },
|
||||
{ path: RouterConstants.CLOTH.PATH, loadComponent: () => import('./pages/algorithms/cloth/cloth.component').then(m => m.ClothComponent) },
|
||||
{ path: RouterConstants.FOUR_COLOR.PATH, loadComponent: () => import('./pages/algorithms/four-color/four-color.component').then(m => m.FourColorComponent) },
|
||||
];
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
LINK: '/algorithms/cloth',
|
||||
};
|
||||
|
||||
static readonly FOUR_COLOR = {
|
||||
PATH: 'algorithms/four_color',
|
||||
LINK: '/algorithms/four_color',
|
||||
};
|
||||
|
||||
static readonly IMPRINT = {
|
||||
PATH: 'imprint',
|
||||
LINK: '/imprint',
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
static readonly XPBD_WIKI = 'https://www.emergentmind.com/topics/extended-position-based-dynamics-xpbd'
|
||||
static readonly GPU_COMPUTING_WIKI = 'https://en.wikipedia.org/wiki/General-purpose_computing_on_graphics_processing_units'
|
||||
static readonly DATA_STRUCTURE_WIKI = 'https://de.wikipedia.org/wiki/Datenstruktur'
|
||||
static readonly FOUR_COLOR_THEOREM = 'https://de.wikipedia.org/wiki/Vier-Farben-Satz'
|
||||
|
||||
}
|
||||
|
||||
@@ -63,6 +63,13 @@ export class AlgorithmsService {
|
||||
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
||||
routerLink: RouterConstants.CLOTH.LINK,
|
||||
icon: 'texture'
|
||||
},
|
||||
{
|
||||
id: 'fourColor',
|
||||
title: 'ALGORITHM.FOUR_COLOR.TITLE',
|
||||
description: 'ALGORITHM.FOUR_COLOR.DESCRIPTION',
|
||||
routerLink: RouterConstants.FOUR_COLOR.LINK,
|
||||
icon: 'palette'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<mat-card class="algo-container">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'FOUR_COLOR.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
|
||||
<div class="controls-container">
|
||||
<div class="controls-panel">
|
||||
<button mat-flat-button color="primary" (click)="generateNewMap()">{{ 'FOUR_COLOR.GENERATE' | translate }}</button>
|
||||
<button mat-flat-button color="accent" (click)="autoSolve()">{{ 'FOUR_COLOR.SOLVE' | translate }}</button>
|
||||
<button mat-stroked-button (click)="resetColors()">{{ 'FOUR_COLOR.CLEAR' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-panel">
|
||||
<div class="input-container">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridRows"
|
||||
(ngModelChange)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridCols"
|
||||
(ngModelChange)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><span class="legend-color color1"></span> {{ 'FOUR_COLOR.COLOR_1' | translate }}</span>
|
||||
<span><span class="legend-color color2"></span> {{ 'FOUR_COLOR.COLOR_2' | translate }}</span>
|
||||
<span><span class="legend-color color3"></span> {{ 'FOUR_COLOR.COLOR_3' | translate }}</span>
|
||||
<span><span class="legend-color color4"></span> {{ 'FOUR_COLOR.COLOR_4' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="status-panel" [ngClass]="solutionStatus.toLowerCase()">
|
||||
<span class="status-label">{{ 'FOUR_COLOR.STATUS.LABEL' | translate }}:</span>
|
||||
<span class="status-message">{{ 'FOUR_COLOR.STATUS.' + solutionStatus | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-container">
|
||||
<canvas #fourColorCanvas
|
||||
(mousedown)="onMouseDown($event)"
|
||||
(mousemove)="onMouseMove($event)"
|
||||
(mouseup)="onMouseUp()"
|
||||
(mouseleave)="onMouseUp()"
|
||||
(touchstart)="onTouchStart($event)"
|
||||
(touchmove)="onTouchMove($event)"
|
||||
(touchend)="onMouseUp()"
|
||||
></canvas>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
.status-panel {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--app-fg);
|
||||
border-left: 5px solid #9e9e9e;
|
||||
font-weight: 500;
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-panel.incomplete {
|
||||
border-left-color: #9e9e9e;
|
||||
background-color: var(--app-bg);
|
||||
}
|
||||
|
||||
.status-panel.solved {
|
||||
border-left-color: #4CAF50;
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-panel.conflicts {
|
||||
border-left-color: #ff9800;
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.status-panel.invalid {
|
||||
border-left-color: #f44336;
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.color1 { background-color: #FF5252; }
|
||||
.color2 { background-color: #448AFF; }
|
||||
.color3 { background-color: #4CAF50; }
|
||||
.color4 { background-color: #FFEB3B; }
|
||||
|
||||
canvas {
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
372
src/app/pages/algorithms/four-color/four-color.component.ts
Normal file
372
src/app/pages/algorithms/four-color/four-color.component.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, FourColorNode, Region} from './four-color.models';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {Information} from '../information/information';
|
||||
import {GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||
import {SharedFunctions} from '../../../shared/SharedFunctions';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-four-color',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
TranslateModule,
|
||||
Information
|
||||
],
|
||||
templateUrl: './four-color.component.html',
|
||||
styleUrl: './four-color.component.scss'
|
||||
})
|
||||
export class FourColorComponent implements AfterViewInit {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'FOUR_COLOR.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: 'FOUR_COLOR.TITLE',
|
||||
translateName: true,
|
||||
description: 'FOUR_COLOR.EXPLANATION.EXPLANATION',
|
||||
link: UrlConstants.FOUR_COLOR_THEOREM
|
||||
}
|
||||
],
|
||||
disclaimer: 'FOUR_COLOR.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: 'FOUR_COLOR.EXPLANATION.DISCLAIMER_BOTTOM',
|
||||
disclaimerListEntry: [
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_1',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_2',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_3',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_4'
|
||||
]
|
||||
};
|
||||
|
||||
gridRows = DEFAULT_GRID_ROWS;
|
||||
gridCols = DEFAULT_GRID_COLS;
|
||||
grid: FourColorNode[][] = [];
|
||||
regions: Region[] = [];
|
||||
executionTime = 0;
|
||||
solutionStatus: 'INCOMPLETE' | 'SOLVED' | 'CONFLICTS' | 'INVALID' = 'INCOMPLETE';
|
||||
|
||||
@ViewChild('fourColorCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
private nodeSize = 0;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ctx = this.canvasRef.nativeElement.getContext('2d')!;
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
applyGridSize(): void {
|
||||
if (this.gridRows < MIN_GRID_SIZE) this.gridRows = MIN_GRID_SIZE;
|
||||
if (this.gridRows > MAX_GRID_SIZE) this.gridRows = MAX_GRID_SIZE;
|
||||
if (this.gridCols < MIN_GRID_SIZE) this.gridCols = MIN_GRID_SIZE;
|
||||
if (this.gridCols > MAX_GRID_SIZE) this.gridCols = MAX_GRID_SIZE;
|
||||
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
initializeGrid(): void {
|
||||
this.grid = [];
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
const row: FourColorNode[] = [];
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
row.push({
|
||||
row: r,
|
||||
col: c,
|
||||
regionId: -1,
|
||||
color: 0,
|
||||
});
|
||||
}
|
||||
this.grid.push(row);
|
||||
}
|
||||
|
||||
this.generateRegions();
|
||||
this.resizeCanvas();
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
private resizeCanvas(): void {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const maxDim = Math.max(this.gridRows, this.gridCols);
|
||||
this.nodeSize = Math.floor(MAX_GRID_PX / maxDim);
|
||||
|
||||
canvas.width = this.gridCols * this.nodeSize;
|
||||
canvas.height = this.gridRows * this.nodeSize;
|
||||
}
|
||||
|
||||
generateRegions(): void {
|
||||
const numRegions = Math.floor((this.gridRows * this.gridCols) / 30);
|
||||
this.regions = [];
|
||||
const seeds = this.determineSeeds(numRegions);
|
||||
this.regionGrowth(seeds);
|
||||
this.determineAdjacency();
|
||||
}
|
||||
|
||||
private determineAdjacency() {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
const currentRegionId = this.grid[row][col].regionId;
|
||||
const neighbors = this.getNeighbors(row, col);
|
||||
for (const neighbor of neighbors) {
|
||||
const neighborRegionId = this.grid[neighbor.row][neighbor.col].regionId;
|
||||
if (neighborRegionId !== -1 && neighborRegionId !== currentRegionId) {
|
||||
this.regions[currentRegionId].neighbors.add(neighborRegionId);
|
||||
this.regions[neighborRegionId].neighbors.add(currentRegionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private regionGrowth(seeds: GridPos[]) {
|
||||
const queue: GridPos[] = [...seeds];
|
||||
while (queue.length > 0) {
|
||||
const {row, col} = queue.shift()!;
|
||||
const regionId = this.grid[row][col].regionId;
|
||||
|
||||
const neighbors = this.getNeighbors(row, col);
|
||||
for (const neighbor of neighbors) {
|
||||
if (this.grid[neighbor.row][neighbor.col].regionId === -1) {
|
||||
this.grid[neighbor.row][neighbor.col].regionId = regionId;
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private determineSeeds(numRegions: number) {
|
||||
const seeds: GridPos[] = [];
|
||||
for (let i = 0; i < numRegions; i++) {
|
||||
let r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
let c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
while (this.grid[r][c].regionId !== -1) {
|
||||
r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
}
|
||||
this.grid[r][c].regionId = i;
|
||||
seeds.push({row: r, col: c});
|
||||
this.regions.push({id: i, color: 0, neighbors: new Set<number>()});
|
||||
}
|
||||
return seeds;
|
||||
}
|
||||
|
||||
private getNeighbors(row: number, col: number): GridPos[] {
|
||||
const res: GridPos[] = [];
|
||||
if (row > 0) res.push({row: row - 1, col});
|
||||
if (row < this.gridRows - 1) res.push({row: row + 1, col});
|
||||
if (col > 0) res.push({row, col: col - 1});
|
||||
if (col < this.gridCols - 1) res.push({row, col: col + 1});
|
||||
return res;
|
||||
}
|
||||
|
||||
drawGrid(): void {
|
||||
if (!this.ctx) return;
|
||||
|
||||
this.ctx.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height);
|
||||
|
||||
// 1. Draw Cell Backgrounds
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
const node = this.grid[r][c];
|
||||
this.ctx.fillStyle = this.getNodeColor(node);
|
||||
this.ctx.fillRect(c * this.nodeSize, r * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw Region Borders
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
const currentRegion = this.grid[r][c].regionId;
|
||||
|
||||
// Right border
|
||||
if (c < this.gridCols - 1 && this.grid[r][c+1].regionId !== currentRegion) {
|
||||
this.ctx.moveTo((c + 1) * this.nodeSize, r * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
if (r < this.gridRows - 1 && this.grid[r+1][c].regionId !== currentRegion) {
|
||||
this.ctx.moveTo(c * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.ctx.stroke();
|
||||
|
||||
// 3. Draw Outer Border
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.strokeRect(0, 0, this.gridCols * this.nodeSize, this.gridRows * this.nodeSize);
|
||||
}
|
||||
|
||||
private getNodeColor(node: FourColorNode): string {
|
||||
switch (node.color) {
|
||||
case 1: return '#FF5252'; // Red
|
||||
case 2: return '#448AFF'; // Blue
|
||||
case 3: return '#4CAF50'; // Green
|
||||
case 4: return '#FFEB3B'; // Yellow
|
||||
default: return 'white';
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent): void {
|
||||
const pos = this.getGridPos(event);
|
||||
if (pos) this.handleInteraction(pos);
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent): void {
|
||||
if (event.buttons !== 1){
|
||||
return;
|
||||
}
|
||||
this.getGridPos(event);
|
||||
}
|
||||
|
||||
onMouseUp(): void {}
|
||||
|
||||
onTouchStart(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
const touch = event.touches[0];
|
||||
const pos = this.getGridPos(touch);
|
||||
if (pos) this.handleInteraction(pos);
|
||||
}
|
||||
|
||||
onTouchMove(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private getGridPos(event: MouseEvent | Touch): GridPos | null {
|
||||
const rect = this.canvasRef.nativeElement.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
const col = Math.floor(x / (rect.width / this.gridCols));
|
||||
const row = Math.floor(y / (rect.height / this.gridRows));
|
||||
|
||||
if (row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols) {
|
||||
return {row, col};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private handleInteraction(pos: GridPos): void {
|
||||
const node = this.grid[pos.row][pos.col];
|
||||
if (node.regionId === -1){
|
||||
return;
|
||||
}
|
||||
|
||||
const region = this.regions[node.regionId];
|
||||
region.color = (region.color % 4) + 1;
|
||||
this.updateRegionColors(region);
|
||||
this.checkSolution();
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
private updateRegionColors(region: Region): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
if (this.grid[row][col].regionId === region.id) {
|
||||
this.grid[row][col].color = region.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetColors(): void {
|
||||
for (const region of this.regions) {
|
||||
region.color = 0;
|
||||
this.updateRegionColors(region);
|
||||
}
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
autoSolve(): void {
|
||||
const startTime = performance.now();
|
||||
this.resetColors();
|
||||
|
||||
const success = this.backtrackSolve(0);
|
||||
const endTime = performance.now();
|
||||
this.executionTime = endTime - startTime;
|
||||
|
||||
if (success) {
|
||||
this.checkSolution();
|
||||
this.drawGrid();
|
||||
} else {
|
||||
const message = this.translate.instant('FOUR_COLOR.ALERT.NO_SOLUTION');
|
||||
this.snackBar.open(message, 'ALERT');
|
||||
}
|
||||
}
|
||||
|
||||
private backtrackSolve(regionIndex: number): boolean {
|
||||
if (regionIndex === this.regions.length) return true;
|
||||
|
||||
const region = this.regions[regionIndex];
|
||||
const availableColors = [1, 2, 3, 4];
|
||||
|
||||
for (const color of availableColors) {
|
||||
if (this.isColorValid(region, color)) {
|
||||
region.color = color;
|
||||
this.updateRegionColors(region);
|
||||
|
||||
if (this.backtrackSolve(regionIndex + 1)) return true;
|
||||
|
||||
region.color = 0;
|
||||
this.updateRegionColors(region);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isColorValid(region: Region, color: number): boolean {
|
||||
for (const neighborId of region.neighbors) {
|
||||
if (this.regions[neighborId].color === color) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
checkSolution(): void {
|
||||
let allColored = true;
|
||||
let hasConflicts = false;
|
||||
|
||||
for (const region of this.regions) {
|
||||
if (region.color === 0) {
|
||||
allColored = false;
|
||||
}
|
||||
if (region.color > 0 && !this.isColorValid(region, region.color)) {
|
||||
hasConflicts = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConflicts) {
|
||||
this.solutionStatus = allColored ? 'INVALID' : 'CONFLICTS';
|
||||
} else {
|
||||
this.solutionStatus = allColored ? 'SOLVED' : 'INCOMPLETE';
|
||||
}
|
||||
}
|
||||
|
||||
generateNewMap(): void {
|
||||
this.initializeGrid();
|
||||
}
|
||||
}
|
||||
18
src/app/pages/algorithms/four-color/four-color.models.ts
Normal file
18
src/app/pages/algorithms/four-color/four-color.models.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface FourColorNode {
|
||||
row: number;
|
||||
col: number;
|
||||
regionId: number;
|
||||
color: number; // 0: none, 1-4: colors
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: number;
|
||||
color: number;
|
||||
neighbors: Set<number>;
|
||||
}
|
||||
|
||||
export const DEFAULT_GRID_ROWS = 30;
|
||||
export const DEFAULT_GRID_COLS = 30;
|
||||
export const MIN_GRID_SIZE = 20;
|
||||
export const MAX_GRID_SIZE = 50;
|
||||
export const MAX_GRID_PX = 600;
|
||||
@@ -203,7 +203,7 @@
|
||||
},
|
||||
"TRIBBLE": {
|
||||
"TITLE": "Homeserver 'Tribble'",
|
||||
"DESCRIPTION": "In diesem Projekt geht es um die Einrichtung und Wartung meines eigenen Homeservers. Er betreibt mehrere Docker-Container wie Gitea, Jellyfin und mehr. Es ist eine großartige Lernerfahrung im Bereich Self-Hosting und Systemadministration.",
|
||||
"DESCRIPTION": "In diesem Projekt geht es um die Einrichtung und Wartung meines eigenen Homeservers. Er betreibt mehrere Docker-Container wie Gitea, Jellyfin and more. Es ist eine großartige Lernerfahrung im Bereich Self-Hosting und Systemadministration.",
|
||||
"LINK_INTERNAL": "Projektdetails",
|
||||
"HIGHLIGHTS": {
|
||||
"P1": "Self-Hosting verschiedener Dienste mit Docker.",
|
||||
@@ -266,7 +266,7 @@
|
||||
"BULLET_1": "Entwicklung mit Angular 19+ und Material Design.",
|
||||
"BULLET_2": "Implementierung performanter Visualisierungen (WebGPU, Shader, Canvas).",
|
||||
"BULLET_3": "Automatisierte CI/CD-Pipelines und Containerisierung mit Docker.",
|
||||
"BULLET_4": "Internationalisierung (i18n) für globale Reichweite.",
|
||||
"BULLET_4": "Internationalization (i18n) für globale Reichweite.",
|
||||
"CHALLENGE_1": "Optimierung der Render-Performance bei komplexen 3D-Fraktalen in Echtzeit.",
|
||||
"CHALLENGE_2": "Architektur einer skalierbaren und wartbaren Frontend-Struktur für diverse Sub-Projekte.",
|
||||
"LEARNING_1": "Effektives UI/UX-Design für komplexe datengesteuerte Visualisierungen.",
|
||||
@@ -303,7 +303,7 @@
|
||||
"TITLE": "Rapid Prototyping & Game Jams",
|
||||
"SHORT_DESCRIPTION": "Sammlung innovativer Spielkonzepte, entstanden in unter 48 Stunden.",
|
||||
"INTRODUCTION": "Teilnahme an nationalen Wettbewerben (z.B. Beansjam). Hier geht es darum, unter extremem Zeitdruck funktionale und spaßige Prototypen zu erschaffen.",
|
||||
"BULLET_1": "Fokus auf 'Core Game Loop' und schnelles Feedback.",
|
||||
"BULLET_1": "Fokus on 'Core Game Loop' und schnelles Feedback.",
|
||||
"BULLET_2": "Kollaborative Entwicklung in kleinen, agilen Teams.",
|
||||
"BULLET_3": "Effektives Zeitmanagement und Scope-Kontrolle.",
|
||||
"BULLET_4": "Veröffentlichung und Iteration basierend auf Community-Votings.",
|
||||
@@ -430,7 +430,7 @@
|
||||
"MANDELBROT_EXPLANATION": "basiert auf der iterativen Formel 'z_{n+1} = z_n^2 + c'. Sie prüft für jeden Punkt in der komplexen Ebene, ob die Zahlenfolge stabil bleibt oder ins Unendliche entkommt. Vorteil: Gilt als 'Apfelmännchen' und Mutter der Fraktale. Sie bietet eine unendliche Vielfalt an selbstähnlichen Strukturen, in die man ewig hineinzoomen kann.",
|
||||
"JULIA_EXPLANATION": "nutzt dieselbe Formel wie Mandelbrot, fixiert jedoch den Parameter 'c' und variiert den Startwert. Je nach Wahl von 'c' entstehen filigrane, wolkenartige Gebilde oder zusammenhanglose 'Staubwolken'. Vorteil: Ermöglicht eine enorme ästiehetische Varianz, da jede Koordinate der Mandelbrot-Menge ein völlig eigenes, einzigartiges Julia-Fraktal erzeugt.",
|
||||
"NEWTON_EXPLANATION": "entsteht durch die Visualisierung des Newton-Verfahrens zur Nullstellen-Suche einer komplexen Funktion. Jeder Pixel wird danach eingefärbt, zu welcher Nullstelle der Algorithmus konvergiert. Vorteil: Erzeugt faszinierende, sternförmige Symmetrien und komplexe Grenzen, an denen sich die Einzugsgebiete der Nullstellen auf chaotische Weise treffen.",
|
||||
"BURNING_SHIP_EXPLANATION": "ist eine Variation des Mandelbrots, bei der vor jedem Iterationsschritt der Absolutbetrag der Real- und Imaginärteile genommen wird: '(|Re(z)| + i|Im(z)|)^2 + c'. Vorteil: Erzeugt eine markante, asymmetrische Struktur, die einem brennenden Schiff mit Segeln ähnelt. Das Fraktal wirkt düsterer und 'mechanischer' als die klassischen Mengen.",
|
||||
"BURNING_SHIP_EXPLANATION": "ist eine variation des Mandelbrots, bei der vor jedem Iterationsschritt der Absolutbetrag der Real- und Imaginärteile genommen wird: '(|Re(z)| + i|Im(z)|)^2 + c'. Vorteil: Erzeugt eine markante, asymmetrische Struktur, die einem brennenden Schiff mit Segeln ähnelt. Das Fraktal wirkt düsterer und 'mechanischer' als die klassischen Mengen.",
|
||||
"DISCLAIMER": "Alle diese Fraktale basieren auf dem Prinzip der Iteration und dem Chaos-Effekt. Das bedeutet für deine Visualisierung:",
|
||||
"DISCLAIMER_1": "Unendliche Tiefe: Egal wie weit du hineinzoomst, es erscheinen immer neue, komplexe Strukturen, die dem Ganzen oft ähneln (Selbstähnlichkeit).",
|
||||
"DISCLAIMER_2": "Fluchtzeit-Algorithmus: Die Farben geben meist an, wie schnell eine Folge einen bestimmten Schwellenwert überschreitet – je schneller, desto 'heißer' oder heller die Farbe.",
|
||||
@@ -501,7 +501,7 @@
|
||||
"DATA_STRUCTURES_EXPLANATION": "Für maximale GPU-Performance müssen Daten speicherfreundlich ausgerichtet werden (16-Byte-Alignment). Anstatt viele einzelne Variablen zu nutzen, packt man Informationen clever in 4er-Blöcke (vec4). Ein Vertex speichert so z. B. [X, Y, Z, Inverse_Masse]. Hat ein Punkt die inverse Masse 0.0, wird er vom Algorithmus ignoriert und schwebt unbeweglich in der Luft – ein eleganter Trick für Aufhängungen ohne extra Wenn-Dann-Abfragen.",
|
||||
"DISCLAIMER": "XPBD vs. Masse-Feder-Systeme: In der physikalischen Simulation gibt es grundlegende Architektur-Unterschiede beim Lösen der Gleichungen:",
|
||||
"DISCLAIMER_1": "Klassische Masse-Feder-Systeme: Hier werden Kräfte (Hookesches Gesetz) berechnet, die zu Beschleunigungen und schließlich zu neuen Positionen führen. Es gibt zwei Wege, diese mathematisch in die Zukunft zu rechnen (Integration):",
|
||||
"DISCLAIMER_2": "Explizite Löser (z.B. Forward Euler): Sie berechnen den nächsten Schritt stur aus dem aktuellen Zustand. Sie sind leicht zu programmieren, aber bei steifen Stoffen extrem instabil. Die Kräfte schaukeln sich auf und die Simulation 'explodiert', sofern man keine winzigen, sehr leistungsfressenden Zeitschritte wählt.",
|
||||
"DISCLAIMER_2": "Explizite Löser (z.B. Forward Euler): Sie berechnen den nächsten Schritt stur aus dem aktuellen Zustand. Sie sind leicht zu programmieren, aber bei steifen Stoffen extrem instabil. Die Kräfte schaukeln sich auf und die simulation 'explodiert', sofern man keine winzigen, sehr leistungsfressenden Zeitschritte wählt.",
|
||||
"DISCLAIMER_3": "Implizite Löser (z.B. Backward Euler): Sie berechnen den nächsten Schritt basierend auf dem zukünftigen Zustand. Das ist mathematisch enorm stabil, erfordert aber das Lösen riesiger globaler Matrix-Gleichungssysteme in jedem Frame. Dies ist auf der GPU schwerer zu parallelisieren und bricht zusammen, wenn sich die Struktur ändert (z. B. durch Zerschneiden des Stoffs).",
|
||||
"DISCLAIMER_4": "Der XPBD-Kompromiss: XPBD umgeht dieses komplexe Matrix-Problem völlig, indem es als lokaler Löser arbeitet. Es kombiniert die unbedingte Stabilität eines impliziten Lösers mit der enormen Geschwindigkeit, Parallelisierbarkeit und dynamischen Anpassungsfähigkeit eines expliziten Systems."
|
||||
}
|
||||
@@ -527,6 +527,10 @@
|
||||
"TITLE": "Labyrinth-Erzeugung",
|
||||
"DESCRIPTION": "Visualisierung verschiedener Laybrinth-Erzeugungs-Algorithmen."
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Vier-Farben-Satz",
|
||||
"DESCRIPTION": "Der Vier-Farben-Satz besagt, dass jede Landkarte in der Ebene mit maximal vier Farben so eingefärbt werden kann, dass keine zwei aneinandergrenzenden Gebiete dieselbe Farbe besitzen."
|
||||
},
|
||||
"FRACTAL": {
|
||||
"TITLE": "Fraktale",
|
||||
"DESCRIPTION": "Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)."
|
||||
@@ -546,5 +550,35 @@
|
||||
"NOTE": "HINWEIS",
|
||||
"GRID_HEIGHT": "Höhe",
|
||||
"GRID_WIDTH": "Beite"
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Vier-Farben-Satz",
|
||||
"GENERATE": "Neue Karte generieren",
|
||||
"SOLVE": "Automatisch lösen",
|
||||
"CLEAR": "Farben zurücksetzen",
|
||||
"COLOR_1": "Farbe 1",
|
||||
"COLOR_2": "Farbe 2",
|
||||
"COLOR_3": "Farbe 3",
|
||||
"COLOR_4": "Farbe 4",
|
||||
"EXECUTION_TIME": "Ausführungszeit",
|
||||
"STATUS": {
|
||||
"LABEL": "Status",
|
||||
"INCOMPLETE": "Die Karte ist noch nicht vollständig eingefärbt.",
|
||||
"SOLVED": "Glückwunsch! Du hast die Karte korrekt gelöst!",
|
||||
"CONFLICTS": "Achtung: Benachbarte Regionen haben die gleiche Farbe!",
|
||||
"INVALID": "Karte ist vollständig, enthält aber Fehler."
|
||||
},
|
||||
"EXPLANATION": { "TITLE": "Der Vier-Farben-Satz",
|
||||
"EXPLANATION": "Der Vier-Farben-Satz besagt, dass jede Landkarte in der Ebene mit maximal vier Farben so eingefärbt werden kann, dass keine zwei aneinandergrenzenden Gebiete dieselbe Farbe besitzen.",
|
||||
"DISCLAIMER": "Dieser Algorithmus verwendet Backtracking, um eine gültige Färbung für die generierten Regionen zu finden.",
|
||||
"DISCLAIMER_1": "Kartengenerierung: Regionen werden mittels eines zufälligen Seed-Wachstumsalgorithmus (Voronoi-ähnlich) auf einem Gitter erzeugt.",
|
||||
"DISCLAIMER_2": "Adjazenz: Zwei Regionen gelten als benachbart, wenn sie mindestens eine gemeinsame Kante im Gitter teilen.",
|
||||
"DISCLAIMER_3": "Backtracking: Der Löser probiert die Farben 1-4 für jede Region aus und macht Schritte rückgängig (Backtracking), wenn ein Konflikt auftritt.",
|
||||
"DISCLAIMER_4": "Interaktiv: Sie können auch auf Regionen klicken, um manuell durch die Farben zu wechseln.",
|
||||
"DISCLAIMER_BOTTOM": "Zum einfärben in die Zelle klicken. Durch erneutes klicken ändert sich die Farbe."
|
||||
},
|
||||
"ALERT": {
|
||||
"NO_SOLUTION": "Keine Lösung gefunden (das sollte bei einer planaren Karte nicht passieren!)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +526,10 @@
|
||||
"TITLE": "Maze Generation",
|
||||
"DESCRIPTION": "Visualizing various maze generation algorithms."
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Four Color Theorem",
|
||||
"DESCRIPTION": "The four color theorem states that any map in a plane can be colored using at most four colors in such a way that regions sharing a common boundary (other than a single point) do not share the same color."
|
||||
},
|
||||
"FRACTAL": {
|
||||
"TITLE": "Fractals",
|
||||
"DESCRIPTION": "Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)."
|
||||
@@ -545,5 +549,35 @@
|
||||
"NOTE": "Note",
|
||||
"GRID_HEIGHT": "Height",
|
||||
"GRID_WIDTH": "Width"
|
||||
},
|
||||
"FOUR_COLOR": {
|
||||
"TITLE": "Four Color Theorem",
|
||||
"GENERATE": "Generate Map",
|
||||
"SOLVE": "Auto Solve",
|
||||
"CLEAR": "Clear Colors",
|
||||
"COLOR_1": "Color 1",
|
||||
"COLOR_2": "Color 2",
|
||||
"COLOR_3": "Color 3",
|
||||
"COLOR_4": "Color 4",
|
||||
"EXECUTION_TIME": "Execution Time",
|
||||
"STATUS": {
|
||||
"LABEL": "Status",
|
||||
"INCOMPLETE": "Map is not fully colored yet.",
|
||||
"SOLVED": "Congratulations! You solved the map correctly!",
|
||||
"CONFLICTS": "Warning: Adjacent regions have the same color!",
|
||||
"INVALID": "Map is fully colored, but contains conflicts."
|
||||
},
|
||||
"EXPLANATION": { "TITLE": "Four Color Theorem",
|
||||
"EXPLANATION": "The four color theorem states that any map in a plane can be colored using at most four colors in such a way that regions sharing a common boundary (other than a single point) do not share the same color.",
|
||||
"DISCLAIMER": "This algorithm uses backtracking to find a valid coloring for the generated regions.",
|
||||
"DISCLAIMER_1": "Map Generation: Regions are generated using a random seed growth algorithm (Voronoi-like) on a grid.",
|
||||
"DISCLAIMER_2": "Adjacency: Two regions are considered neighbors if they share at least one edge in the grid.",
|
||||
"DISCLAIMER_3": "Backtracking: The solver tries colors 1-4 for each region, backtracking when a conflict is found.",
|
||||
"DISCLAIMER_4": "Interactive: You can also click on regions to cycle through colors manually.",
|
||||
"DISCLAIMER_BOTTOM": "Click to color the region. Click again to change the color."
|
||||
},
|
||||
"ALERT": {
|
||||
"NO_SOLUTION": "No solution found (this should not happen for a planar map!)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,22 @@ canvas {
|
||||
&.M2 {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
&.color1 {
|
||||
background-color: #FF5252;
|
||||
}
|
||||
|
||||
&.color2 {
|
||||
background-color: #448AFF;
|
||||
}
|
||||
|
||||
&.color3 {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
&.color4 {
|
||||
background-color: #FFEB3B;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user