Merge pull request 'Add fractal visualization feature' (#17) from feature/fractals into main
Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
@@ -11,6 +11,7 @@ export const routes: Routes = [
|
|||||||
{ path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT},
|
{ path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT},
|
||||||
{ path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT},
|
{ path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT},
|
||||||
{ path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT},
|
{ path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT},
|
||||||
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT}
|
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT},
|
||||||
|
{ path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {PathfindingComponent} from '../pages/algorithms/pathfinding/pathfinding.
|
|||||||
import {SortingComponent} from '../pages/algorithms/sorting/sorting.component';
|
import {SortingComponent} from '../pages/algorithms/sorting/sorting.component';
|
||||||
import {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.component';
|
import {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.component';
|
||||||
import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/labyrinth.component';
|
import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/labyrinth.component';
|
||||||
|
import {FractalComponent} from '../pages/algorithms/fractal/fractal.component';
|
||||||
|
|
||||||
export class RouterConstants {
|
export class RouterConstants {
|
||||||
|
|
||||||
@@ -51,6 +52,12 @@ export class RouterConstants {
|
|||||||
COMPONENT: LabyrinthComponent
|
COMPONENT: LabyrinthComponent
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static readonly FRACTAL = {
|
||||||
|
PATH: 'algorithms/fractal',
|
||||||
|
LINK: '/algorithms/fractal',
|
||||||
|
COMPONENT: FractalComponent
|
||||||
|
};
|
||||||
|
|
||||||
static readonly IMPRINT = {
|
static readonly IMPRINT = {
|
||||||
PATH: 'imprint',
|
PATH: 'imprint',
|
||||||
LINK: '/imprint',
|
LINK: '/imprint',
|
||||||
|
|||||||
@@ -10,4 +10,8 @@
|
|||||||
static readonly CONWAYS_WIKI = 'https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens'
|
static readonly CONWAYS_WIKI = 'https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens'
|
||||||
static readonly PRIMS_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Prim'
|
static readonly PRIMS_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Prim'
|
||||||
static readonly KRUSKAL_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Kruskal'
|
static readonly KRUSKAL_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Kruskal'
|
||||||
|
static readonly MANDELBROT_WIKI = 'https://de.wikipedia.org/wiki/Mandelbrot-Menge'
|
||||||
|
static readonly JULIA_WIKI = 'https://de.wikipedia.org/wiki/Julia-Menge'
|
||||||
|
static readonly NEWTON_FRACTAL_WIKI = 'https://de.wikipedia.org/wiki/Newtonfraktal'
|
||||||
|
static readonly BURNING_SHIP_WIKI = 'https://de.wikipedia.org/wiki/Burning_ship_(Fraktal)'
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/app/pages/algorithms/fractal/fractal.component.html
Normal file
55
src/app/pages/algorithms/fractal/fractal.component.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<mat-card class="container">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{ 'FRACTAL.TITLE' | translate }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<app-information [algorithmInformation]="algoInformation"/>
|
||||||
|
<div class="controls-panel">
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>{{ 'FRACTAL.ALGORITHM' | translate }}</mat-label>
|
||||||
|
<mat-select [(ngModel)]="selectedAlgorithm"
|
||||||
|
(selectionChange)="onAlgorithmChange()">
|
||||||
|
@for (algo of algoInformation.entries; track algo.name) {
|
||||||
|
<mat-option [value]="algo.name">{{ algo.name }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>{{ 'FRACTAL.COLOR_SCHEME' | translate }}</mat-label>
|
||||||
|
<mat-select [(ngModel)]="selectedColorScheme"
|
||||||
|
(selectionChange)="onColorChanged()"
|
||||||
|
[disabled]="selectedAlgorithm === 'Newton'">
|
||||||
|
@for (name of FRACTAL_COLOR_SCHEMES; track name) {
|
||||||
|
<mat-option [value]="name">{{ name }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'FRACTAL.MAX_ITERATION' | translate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="number"
|
||||||
|
[disabled]="selectedAlgorithm === 'Newton'"
|
||||||
|
[min]="MIN_ITERATION"
|
||||||
|
[max]="MAX_ITERATION"
|
||||||
|
[(ngModel)]="currentIteration"
|
||||||
|
(blur)="onIterationChanged()"
|
||||||
|
(keyup.enter)="onIterationChanged()"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="canvas-container">
|
||||||
|
<canvas #fractalCanvas
|
||||||
|
width="1000"
|
||||||
|
height="1000"
|
||||||
|
(mousedown)="onMouseDown($event)"
|
||||||
|
(mousemove)="onMouseMove($event)"
|
||||||
|
(mouseup)="onMouseUp()"
|
||||||
|
(mouseleave)="onMouseUp()"
|
||||||
|
(wheel)="onWheel($event)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
189
src/app/pages/algorithms/fractal/fractal.component.ts
Normal file
189
src/app/pages/algorithms/fractal/fractal.component.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
|
||||||
|
import {Information} from '../information/information';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {TranslatePipe} from '@ngx-translate/core';
|
||||||
|
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||||
|
import {MatOption} from '@angular/material/core';
|
||||||
|
import {MatSelect} from '@angular/material/select';
|
||||||
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {FractalService} from './service/fractal.service';
|
||||||
|
import {DEFAULT_ITERATION, FRACTAL_COLOR_SCHEMES, FractalConfig, MAX_ITERATION, MIN_ITERATION} from './fractal.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-fractal',
|
||||||
|
imports: [
|
||||||
|
Information,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
TranslatePipe,
|
||||||
|
MatFormField,
|
||||||
|
MatLabel,
|
||||||
|
MatOption,
|
||||||
|
MatSelect,
|
||||||
|
FormsModule,
|
||||||
|
MatInput
|
||||||
|
],
|
||||||
|
templateUrl: './fractal.component.html',
|
||||||
|
styleUrl: './fractal.component.scss',
|
||||||
|
})
|
||||||
|
export class FractalComponent implements AfterViewInit {
|
||||||
|
algoInformation: AlgorithmInformation = {
|
||||||
|
title: 'FRACTAL.EXPLANATION.TITLE',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
name: 'Mandelbrot',
|
||||||
|
description: 'FRACTAL.EXPLANATION.MANDELBROT_EXPLANATION',
|
||||||
|
link: UrlConstants.MANDELBROT_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Julia',
|
||||||
|
description: 'FRACTAL.EXPLANATION.JULIA_EXPLANATION',
|
||||||
|
link: UrlConstants.JULIA_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Burning Ship',
|
||||||
|
description: 'FRACTAL.EXPLANATION.BURNING_SHIP_EXPLANATION',
|
||||||
|
link: UrlConstants.BURNING_SHIP_WIKI
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Newton',
|
||||||
|
description: 'FRACTAL.EXPLANATION.NEWTON_EXPLANATION',
|
||||||
|
link: UrlConstants.NEWTON_FRACTAL_WIKI
|
||||||
|
}
|
||||||
|
],
|
||||||
|
disclaimer: 'FRACTAL.EXPLANATION.DISCLAIMER',
|
||||||
|
disclaimerBottom: '',
|
||||||
|
disclaimerListEntry: [
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_1',
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_2',
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_3',
|
||||||
|
'FRACTAL.EXPLANATION.DISCLAIMER_4'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
private readonly fractalService = inject(FractalService);
|
||||||
|
|
||||||
|
config: FractalConfig = {
|
||||||
|
algorithm: 'Mandelbrot',
|
||||||
|
width: 1000,
|
||||||
|
height: 1000,
|
||||||
|
maxIterations: DEFAULT_ITERATION,
|
||||||
|
zoom: 1,
|
||||||
|
offsetX: -0.5,
|
||||||
|
offsetY: 0,
|
||||||
|
colorScheme: FRACTAL_COLOR_SCHEMES[0]
|
||||||
|
};
|
||||||
|
private isDragging = false;
|
||||||
|
private dragStartX = 0;
|
||||||
|
private dragStartY = 0;
|
||||||
|
|
||||||
|
@ViewChild('fractalCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
selectedAlgorithm: string = this.config.algorithm;
|
||||||
|
currentIteration: number = this.config.maxIterations;
|
||||||
|
selectedColorScheme: string = 'Blue-Gold';
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetView(): void{
|
||||||
|
this.isDragging = false;
|
||||||
|
this.dragStartX = 0;
|
||||||
|
this.dragStartY = 0;
|
||||||
|
this.config.offsetX = -0.5;
|
||||||
|
this.config.offsetY = 0;
|
||||||
|
this.config.zoom = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAlgorithmChange(): void {
|
||||||
|
this.config.algorithm = this.selectedAlgorithm as any;
|
||||||
|
this.resetView();
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
onColorChanged(): void {
|
||||||
|
this.config.colorScheme = this.selectedColorScheme as any;
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
onIterationChanged(): void {
|
||||||
|
this.config.maxIterations = Math.max(Math.min(this.currentIteration, MAX_ITERATION), MIN_ITERATION );
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private draw(): void {
|
||||||
|
const canvas = this.canvasRef.nativeElement;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
this.fractalService.draw(ctx, this.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//movement
|
||||||
|
onMouseDown(event: MouseEvent): void {
|
||||||
|
this.isDragging = true;
|
||||||
|
this.dragStartX = event.clientX;
|
||||||
|
this.dragStartY = event.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(): void {
|
||||||
|
this.isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event: MouseEvent): void {
|
||||||
|
if (!this.isDragging) return;
|
||||||
|
|
||||||
|
const deltaX = event.clientX - this.dragStartX;
|
||||||
|
const deltaY = event.clientY - this.dragStartY;
|
||||||
|
|
||||||
|
const reScale = 4 / (this.config.width * this.config.zoom);
|
||||||
|
const imScale = 4 / (this.config.height * this.config.zoom);
|
||||||
|
|
||||||
|
this.config.offsetX -= deltaX * reScale;
|
||||||
|
this.config.offsetY -= deltaY * imScale;
|
||||||
|
|
||||||
|
this.dragStartX = event.clientX;
|
||||||
|
this.dragStartY = event.clientY;
|
||||||
|
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel(event: WheelEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const zoomFactor = 1.1;
|
||||||
|
const zoomIn = event.deltaY < 0;
|
||||||
|
|
||||||
|
const rect = this.canvasRef.nativeElement.getBoundingClientRect();
|
||||||
|
const mouseX = event.clientX - rect.left;
|
||||||
|
const mouseY = event.clientY - rect.top;
|
||||||
|
|
||||||
|
const reScale = 4 / (this.config.width * this.config.zoom);
|
||||||
|
const imScale = 4 / (this.config.height * this.config.zoom);
|
||||||
|
|
||||||
|
const mouseRe = (mouseX - this.config.width / 2) * reScale + this.config.offsetX;
|
||||||
|
const mouseIm = (mouseY - this.config.height / 2) * imScale + this.config.offsetY;
|
||||||
|
|
||||||
|
if (zoomIn) {
|
||||||
|
this.config.zoom *= zoomFactor;
|
||||||
|
} else {
|
||||||
|
this.config.zoom /= zoomFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newReScale = 4 / (this.config.width * this.config.zoom);
|
||||||
|
const newImScale = 4 / (this.config.height * this.config.zoom);
|
||||||
|
|
||||||
|
this.config.offsetX = mouseRe - (mouseX - this.config.width / 2) * newReScale;
|
||||||
|
this.config.offsetY = mouseIm - (mouseY - this.config.height / 2) * newImScale;
|
||||||
|
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly MIN_ITERATION = MIN_ITERATION;
|
||||||
|
protected readonly MAX_ITERATION = MAX_ITERATION;
|
||||||
|
protected readonly FRACTAL_COLOR_SCHEMES = FRACTAL_COLOR_SCHEMES;
|
||||||
|
}
|
||||||
33
src/app/pages/algorithms/fractal/fractal.model.ts
Normal file
33
src/app/pages/algorithms/fractal/fractal.model.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export interface FractalConfig {
|
||||||
|
algorithm: 'Mandelbrot' | 'Julia' | 'Burning Ship' | 'Newton';
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
maxIterations: number;
|
||||||
|
zoom: number;
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
cReal?: number;
|
||||||
|
cImag?: number;
|
||||||
|
colorScheme: FractalColorScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FRACTAL_COLOR_SCHEMES = [
|
||||||
|
'Blue-Gold',
|
||||||
|
'Fire',
|
||||||
|
'Rainbow',
|
||||||
|
'Greyscale',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type FractalColorScheme = typeof FRACTAL_COLOR_SCHEMES[number];
|
||||||
|
export class ComplexNumber {
|
||||||
|
constructor(public re: number, public im: number) {}
|
||||||
|
|
||||||
|
add(other: ComplexNumber): ComplexNumber {
|
||||||
|
return new ComplexNumber(this.re + other.re, this.im + other.im);
|
||||||
|
}
|
||||||
|
// Für Newton brauchen wir später auch Multiplikation und Division
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ITERATION = 100;
|
||||||
|
export const MIN_ITERATION = 20;
|
||||||
|
export const MAX_ITERATION = 1000;
|
||||||
256
src/app/pages/algorithms/fractal/service/fractal.service.ts
Normal file
256
src/app/pages/algorithms/fractal/service/fractal.service.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {FractalColorScheme, FractalConfig} from '../fractal.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FractalService {
|
||||||
|
|
||||||
|
private currentPalette: Uint8ClampedArray = new Uint8ClampedArray(0);
|
||||||
|
private lastScheme: FractalColorScheme | null = null;
|
||||||
|
private lastMaxIter: number = 0;
|
||||||
|
|
||||||
|
draw(ctx: CanvasRenderingContext2D, config: FractalConfig): void {
|
||||||
|
const width = config.width;
|
||||||
|
const height = config.height;
|
||||||
|
|
||||||
|
this.updateColorPalette(config);
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(width, height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
|
||||||
|
const re = (x - width / 2) * (4 / width / config.zoom) + config.offsetX;
|
||||||
|
const im = (y - height / 2) * (4 / height / config.zoom) + config.offsetY;
|
||||||
|
|
||||||
|
let iterations = 0;
|
||||||
|
const pixelIndex = (y * width + x) * 4;
|
||||||
|
switch (config.algorithm) {
|
||||||
|
case 'Mandelbrot':
|
||||||
|
iterations = this.calculateMandelbrot(re, im, config.maxIterations);
|
||||||
|
this.colorizePixel(data, pixelIndex, iterations);
|
||||||
|
break;
|
||||||
|
case 'Julia':
|
||||||
|
{ const cRe = config.cReal ?? -0.7;
|
||||||
|
const cIm = config.cImag ?? 0.27015;
|
||||||
|
iterations = this.calculateJulia(re, im, cRe, cIm, config.maxIterations);
|
||||||
|
this.colorizePixel(data, pixelIndex, iterations);
|
||||||
|
break; }
|
||||||
|
case 'Burning Ship':
|
||||||
|
iterations = this.calculateBurningShip(re, im, config.maxIterations);
|
||||||
|
this.colorizePixel(data, pixelIndex, iterations);
|
||||||
|
break;
|
||||||
|
case 'Newton':
|
||||||
|
this.handleNewtonFractal(re, im, pixelIndex, config, data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleNewtonFractal(re: number, im: number, pixelIndex:number, config: FractalConfig, data: Uint8ClampedArray<ArrayBuffer>) {
|
||||||
|
|
||||||
|
const result = this.calculateNewton(re, im, config.maxIterations);
|
||||||
|
if (result === config.maxIterations) {
|
||||||
|
data[pixelIndex] = 0;
|
||||||
|
data[pixelIndex + 1] = 0;
|
||||||
|
data[pixelIndex + 2] = 0;
|
||||||
|
} else if (result >= 2000) {
|
||||||
|
const light = 255 - (result - 2000) * 10;
|
||||||
|
data[pixelIndex] = 0;
|
||||||
|
data[pixelIndex + 1] = 0;
|
||||||
|
data[pixelIndex + 2] = Math.max(0, light);
|
||||||
|
} else if (result >= 1000) {
|
||||||
|
const light = 255 - (result - 1000) * 10;
|
||||||
|
data[pixelIndex] = 0;
|
||||||
|
data[pixelIndex + 1] = Math.max(0, light);
|
||||||
|
data[pixelIndex + 2] = 0;
|
||||||
|
} else {
|
||||||
|
const light = 255 - result * 10;
|
||||||
|
data[pixelIndex] = Math.max(0, light);
|
||||||
|
data[pixelIndex + 1] = 0;
|
||||||
|
data[pixelIndex + 2] = 0;
|
||||||
|
}
|
||||||
|
data[pixelIndex + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateColorPalette(config: FractalConfig) {
|
||||||
|
if (this.lastScheme !== config.colorScheme || this.lastMaxIter !== config.maxIterations) {
|
||||||
|
this.currentPalette = this.generatePalette(config.colorScheme, config.maxIterations);
|
||||||
|
this.lastScheme = config.colorScheme;
|
||||||
|
this.lastMaxIter = config.maxIterations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateMandelbrot(cRe: number, cIm: number, maxIter: number): number {
|
||||||
|
let zRe = 0;
|
||||||
|
let zIm = 0;
|
||||||
|
let n = 0;
|
||||||
|
|
||||||
|
while (zRe * zRe + zIm * zIm <= 4 && n < maxIter) {
|
||||||
|
const zReNew = zRe * zRe - zIm * zIm + cRe;
|
||||||
|
zIm = 2 * zRe * zIm + cIm;
|
||||||
|
zRe = zReNew;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateJulia(zRe: number, zIm: number, cRe: number, cIm: number, maxIter: number): number {
|
||||||
|
let n = 0;
|
||||||
|
while (zRe * zRe + zIm * zIm <= 4 && n < maxIter) {
|
||||||
|
const zReNew = zRe * zRe - zIm * zIm + cRe;
|
||||||
|
zIm = 2 * zRe * zIm + cIm;
|
||||||
|
zRe = zReNew;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateBurningShip(cRe: number, cIm: number, maxIter: number): number {
|
||||||
|
let zRe = 0;
|
||||||
|
let zIm = 0;
|
||||||
|
let n = 0;
|
||||||
|
|
||||||
|
while (zRe * zRe + zIm * zIm <= 4 && n < maxIter) {
|
||||||
|
const zReAbs = Math.abs(zRe);
|
||||||
|
const zImAbs = Math.abs(zIm);
|
||||||
|
|
||||||
|
const zReNew = zReAbs * zReAbs - zImAbs * zImAbs + cRe;
|
||||||
|
zIm = 2 * zReAbs * zImAbs + cIm;
|
||||||
|
zRe = zReNew;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private colorizePixel(data: Uint8ClampedArray, pixelIndex: number, iterations: number): void {
|
||||||
|
const paletteIndex = iterations * 4;
|
||||||
|
data[pixelIndex] = this.currentPalette[paletteIndex]; // R
|
||||||
|
data[pixelIndex + 1] = this.currentPalette[paletteIndex + 1]; // G
|
||||||
|
data[pixelIndex + 2] = this.currentPalette[paletteIndex + 2]; // B
|
||||||
|
data[pixelIndex + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
// z^3 - 1
|
||||||
|
// 1. 1 + 0i (right)
|
||||||
|
// 2. -0.5 + 0.866i (upper left)
|
||||||
|
// 3. -0.5 - 0.866i (lower left)
|
||||||
|
private calculateNewton(x0: number, y0: number, maxIter: number): number {
|
||||||
|
let x = x0;
|
||||||
|
let y = y0;
|
||||||
|
const tolerance = 0.000001;
|
||||||
|
|
||||||
|
for (let i = 0; i < maxIter; i++) {
|
||||||
|
const x2 = x * x;
|
||||||
|
const y2 = y * y;
|
||||||
|
|
||||||
|
if (x2 + y2 < 0.0000001) return maxIter;
|
||||||
|
|
||||||
|
const oldX = x;
|
||||||
|
const oldY = y;
|
||||||
|
|
||||||
|
const z3Re = x*x*x - 3*x*y*y;
|
||||||
|
const z3Im = 3*x*x*y - y*y*y;
|
||||||
|
|
||||||
|
const fRe = z3Re - 1;
|
||||||
|
const fIm = z3Im;
|
||||||
|
|
||||||
|
const fPrimeRe = 3 * (x2 - y2);
|
||||||
|
const fPrimeIm = 3 * (2 * x * y);
|
||||||
|
|
||||||
|
const divDenom = fPrimeRe * fPrimeRe + fPrimeIm * fPrimeIm;
|
||||||
|
if (divDenom === 0) return maxIter;
|
||||||
|
|
||||||
|
const divRe = (fRe * fPrimeRe + fIm * fPrimeIm) / divDenom;
|
||||||
|
const divIm = (fIm * fPrimeRe - fRe * fPrimeIm) / divDenom;
|
||||||
|
|
||||||
|
x = oldX - divRe;
|
||||||
|
y = oldY - divIm;
|
||||||
|
|
||||||
|
if ((x - 1)*(x - 1) + y*y < tolerance) return i;
|
||||||
|
|
||||||
|
if ((x + 0.5)*(x + 0.5) + (y - 0.866)*(y - 0.866) < tolerance) return i + 1000;
|
||||||
|
|
||||||
|
if ((x + 0.5)*(x + 0.5) + (y + 0.866)*(y + 0.866) < tolerance) return i + 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxIter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Paletten-Generator ---
|
||||||
|
private generatePalette(scheme: FractalColorScheme, maxIter: number): Uint8ClampedArray {
|
||||||
|
const palette = new Uint8ClampedArray((maxIter + 1) * 4);
|
||||||
|
|
||||||
|
for (let i = 0; i <= maxIter; i++) {
|
||||||
|
let r = 0, g = 0, b = 0;
|
||||||
|
|
||||||
|
if (i === maxIter) {
|
||||||
|
r = 0; g = 0; b = 0;
|
||||||
|
} else {
|
||||||
|
const t = i / maxIter;
|
||||||
|
|
||||||
|
switch (scheme) {
|
||||||
|
case 'Greyscale':
|
||||||
|
r = g = b = t * 255;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Blue-Gold':
|
||||||
|
r = 9 * (1 - t) * t * t * t * 255;
|
||||||
|
g = 15 * (1 - t) * (1 - t) * t * t * 255;
|
||||||
|
b = 8.5 * (1 - t) * (1 - t) * (1 - t) * t * 255;
|
||||||
|
r = Math.min(255, r * 2);
|
||||||
|
g = Math.min(255, g * 2);
|
||||||
|
b = Math.min(255, b * 4) + 40;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Fire':
|
||||||
|
if (i === maxIter) {
|
||||||
|
r = 0; g = 0; b = 0;
|
||||||
|
} else {
|
||||||
|
const t = Math.sqrt(i / maxIter);
|
||||||
|
|
||||||
|
r = t * 255 * 2;
|
||||||
|
g = (t - 0.3) * 255 * 3;
|
||||||
|
b = (t - 0.6) * 255 * 6;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Rainbow':
|
||||||
|
{ const hue = (i * 5) % 360;
|
||||||
|
const rgb = this.hsvToRgb(hue, 1, 1);
|
||||||
|
r = rgb[0]; g = rgb[1]; b = rgb[2];
|
||||||
|
break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = i * 4;
|
||||||
|
palette[index] = Math.min(255, Math.max(0, r));
|
||||||
|
palette[index + 1] = Math.min(255, Math.max(0, g));
|
||||||
|
palette[index + 2] = Math.min(255, Math.max(0, b));
|
||||||
|
palette[index + 3] = 255; // Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
||||||
|
let r = 0, g = 0, b = 0;
|
||||||
|
const c = v * s;
|
||||||
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||||
|
const m = v - c;
|
||||||
|
|
||||||
|
if (h >= 0 && h < 60) { r = c; g = x; b = 0; }
|
||||||
|
else if (h >= 60 && h < 120) { r = x; g = c; b = 0; }
|
||||||
|
else if (h >= 120 && h < 180) { r = 0; g = c; b = x; }
|
||||||
|
else if (h >= 180 && h < 240) { r = 0; g = x; b = c; }
|
||||||
|
else if (h >= 240 && h < 300) { r = x; g = 0; b = c; }
|
||||||
|
else if (h >= 300 && h < 360) { r = c; g = 0; b = x; }
|
||||||
|
|
||||||
|
return [(r + m) * 255, (g + m) * 255, (b + m) * 255];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,12 @@ export class AlgorithmsService {
|
|||||||
title: 'ALGORITHM.LABYRINTH.TITLE',
|
title: 'ALGORITHM.LABYRINTH.TITLE',
|
||||||
description: 'ALGORITHM.LABYRINTH.DESCRIPTION',
|
description: 'ALGORITHM.LABYRINTH.DESCRIPTION',
|
||||||
routerLink: RouterConstants.LABYRINTH.LINK
|
routerLink: RouterConstants.LABYRINTH.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fractal',
|
||||||
|
title: 'ALGORITHM.FRACTAL.TITLE',
|
||||||
|
description: 'ALGORITHM.FRACTAL.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.FRACTAL.LINK
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -377,6 +377,24 @@
|
|||||||
"DISCLAIMER_4": "Anwendung: Solche Labyrinthe sind die perfekte Testumgebung für Pfadfindungsalgorithmen wie Dijkstra oder A*."
|
"DISCLAIMER_4": "Anwendung: Solche Labyrinthe sind die perfekte Testumgebung für Pfadfindungsalgorithmen wie Dijkstra oder A*."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fraktale",
|
||||||
|
"ALGORITHM": "Algorithmen",
|
||||||
|
"COLOR_SCHEME": "Farbschema",
|
||||||
|
"MAX_ITERATION": "Maximale Auflösung",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Mathematische Kunst",
|
||||||
|
"MANDELBROT_EXPLANATION": "basiert auf der iterativen Formel 'z_{n+1} = z_n^2 + c'. Sie prüft für jeden Punkt in der komplexen Ebene, ob die Zahlenfolge stabil bleibt oder ins Unendliche entkommt. Vorteil: Gilt als 'Apfelmännchen' und Mutter der Fraktale. Sie bietet eine unendliche Vielfalt an selbstähnlichen Strukturen, in die man ewig hineinzoomen kann.",
|
||||||
|
"JULIA_EXPLANATION": "nutzt dieselbe Formel wie Mandelbrot, fixiert jedoch den Parameter 'c' und variiert den Startwert. Je nach Wahl von 'c' entstehen filigrane, wolkenartige Gebilde oder zusammenhanglose 'Staubwolken'. Vorteil: Ermöglicht eine enorme ästhetische Varianz, da jede Koordinate der Mandelbrot-Menge ein völlig eigenes, einzigartiges Julia-Fraktal erzeugt.",
|
||||||
|
"NEWTON_EXPLANATION": "entsteht durch die Visualisierung des Newton-Verfahrens zur Nullstellen-Suche einer komplexen Funktion. Jeder Pixel wird danach eingefärbt, zu welcher Nullstelle der Algorithmus konvergiert. Vorteil: Erzeugt faszinierende, sternförmige Symmetrien und komplexe Grenzen, an denen sich die Einzugsgebiete der Nullstellen auf chaotische Weise treffen.",
|
||||||
|
"BURNING_SHIP_EXPLANATION": "ist eine Variation des Mandelbrots, bei der vor jedem Iterationsschritt der Absolutbetrag der Real- und Imaginärteile genommen wird: '(|Re(z)| + i|Im(z)|)^2 + c'. Vorteil: Erzeugt eine markante, asymmetrische Struktur, die einem brennenden Schiff mit Segeln ähnelt. Das Fraktal wirkt düsterer und 'mechanischer' als die klassischen Mengen.",
|
||||||
|
"DISCLAIMER": "Alle diese Fraktale basieren auf dem Prinzip der Iteration und dem Chaos-Effekt. Das bedeutet für deine Visualisierung:",
|
||||||
|
"DISCLAIMER_1": "Unendliche Tiefe: Egal wie weit du hineinzoomst, es erscheinen immer neue, komplexe Strukturen, die dem Ganzen oft ähneln (Selbstähnlichkeit).",
|
||||||
|
"DISCLAIMER_2": "Fluchtzeit-Algorithmus: Die Farben geben meist an, wie schnell eine Folge einen bestimmten Schwellenwert überschreitet – je schneller, desto 'heißer' oder heller die Farbe.",
|
||||||
|
"DISCLAIMER_3": "Komplexe Zahlen: Die Berechnung findet nicht in einem normalen Koordinatensystem statt, sondern in der komplexen Ebene mit realen und imaginären Anteilen.",
|
||||||
|
"DISCLAIMER_4": "Rechenintensität: Da für jeden Pixel hunderte Berechnungen durchgeführt werden, sind Fraktale ein klassischer Benchmark für die Performance von Grafikprozessoren (GPUs)."
|
||||||
|
}
|
||||||
|
},
|
||||||
"ALGORITHM": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithmen",
|
"TITLE": "Algorithmen",
|
||||||
"PATHFINDING": {
|
"PATHFINDING": {
|
||||||
@@ -395,6 +413,10 @@
|
|||||||
"TITLE": "Labyrinth-Erzeugung",
|
"TITLE": "Labyrinth-Erzeugung",
|
||||||
"DESCRIPTION": "Visualisierung verschiedener Laybrinth-Erzeugungs-Algorithmen."
|
"DESCRIPTION": "Visualisierung verschiedener Laybrinth-Erzeugungs-Algorithmen."
|
||||||
},
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fraktale",
|
||||||
|
"DESCRIPTION": "Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)."
|
||||||
|
},
|
||||||
"NOTE": "HINWEIS",
|
"NOTE": "HINWEIS",
|
||||||
"GRID_HEIGHT": "Höhe",
|
"GRID_HEIGHT": "Höhe",
|
||||||
"GRID_WIDTH": "Beite"
|
"GRID_WIDTH": "Beite"
|
||||||
|
|||||||
@@ -376,6 +376,25 @@
|
|||||||
"DISCLAIMER_4": "Application: Such labyrinths are the perfect test environment for pathfinding algorithms such as Dijkstra or A*."
|
"DISCLAIMER_4": "Application: Such labyrinths are the perfect test environment for pathfinding algorithms such as Dijkstra or A*."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fractals",
|
||||||
|
"ALGORITHM": "Algorithms",
|
||||||
|
"COLOR_SCHEME": "Color Scheme",
|
||||||
|
"MAX_ITERATION": "Max. Resolution",
|
||||||
|
"EXPLANATION": {
|
||||||
|
"TITLE": "Mathematical Art",
|
||||||
|
"MANDELBROT_EXPLANATION": "is based on the iterative formula 'z_{n+1} = z_n^2 + c'. It checks for every point in the complex plane whether the sequence remains stable or escapes to infinity. Advantage: Known as the 'Apple Man', it is the mother of all fractals, offering infinite variety and self-similar structures to zoom into forever.",
|
||||||
|
"JULIA_EXPLANATION": "uses the same formula as Mandelbrot but fixes the parameter 'c' and varies the starting value. Depending on the choice of 'c', it creates delicate, cloud-like structures or disconnected 'dust'. Advantage: Allows for immense aesthetic variance, as every coordinate in the Mandelbrot set produces its own unique Julia fractal.",
|
||||||
|
"NEWTON_EXPLANATION": "is created by visualizing Newton's method for finding roots of a complex function. Each pixel is colored based on which root the algorithm converges to. Advantage: Produces fascinating star-shaped symmetries and complex boundaries where the attraction basins of the roots meet in a chaotic dance.",
|
||||||
|
"BURNING_SHIP_EXPLANATION": "is a variation of the Mandelbrot set where the absolute values of the real and imaginary parts are taken before each iteration: '(|Re(z)| + i|Im(z)|)^2 + c'. Advantage: Generates a striking, asymmetrical structure resembling a ship on fire. It feels more 'mechanical' and darker compared to the classical sets.",
|
||||||
|
"DISCLAIMER": "All these fractals are based on the principle of iteration and the butterfly effect. This means for your visualization:",
|
||||||
|
"DISCLAIMER_1": "Infinite Depth: No matter how far you zoom in, new complex structures appear that often resemble the whole (self-similarity).",
|
||||||
|
"DISCLAIMER_2": "Escape-Time Algorithm: Colors usually represent how quickly a sequence exceeds a certain threshold—the faster it escapes, the 'hotter' or brighter the color.",
|
||||||
|
"DISCLAIMER_3": "Complex Numbers: Calculations don't happen in a standard coordinate system, but in the complex plane using real and imaginary components.",
|
||||||
|
"DISCLAIMER_4": "Computational Load: Since hundreds of calculations are performed for every single pixel, fractals are a classic benchmark for GPU and processor performance."
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
"ALGORITHM": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithms",
|
"TITLE": "Algorithms",
|
||||||
"PATHFINDING": {
|
"PATHFINDING": {
|
||||||
@@ -394,6 +413,10 @@
|
|||||||
"TITLE": "Maze Generation",
|
"TITLE": "Maze Generation",
|
||||||
"DESCRIPTION": "Visualizing various maze generation algorithms."
|
"DESCRIPTION": "Visualizing various maze generation algorithms."
|
||||||
},
|
},
|
||||||
|
"FRACTAL": {
|
||||||
|
"TITLE": "Fractals",
|
||||||
|
"DESCRIPTION": "Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)."
|
||||||
|
},
|
||||||
"NOTE": "Note",
|
"NOTE": "Note",
|
||||||
"GRID_HEIGHT": "Height",
|
"GRID_HEIGHT": "Height",
|
||||||
"GRID_WIDTH": "Width"
|
"GRID_WIDTH": "Width"
|
||||||
|
|||||||
Reference in New Issue
Block a user