Changed 2d fractals to webgl for more performance
Some checks failed
Build, Test & Push Frontend / docker (pull_request) Has been cancelled
Build, Test & Push Frontend / quality-check (pull_request) Has been cancelled

This commit is contained in:
2026-02-12 10:14:22 +01:00
parent cc6997e732
commit c409cd08b1
12 changed files with 341 additions and 443 deletions

View File

@@ -4,52 +4,36 @@
</mat-card-header>
<mat-card-content>
<app-information [algorithmInformation]="algoInformation"/>
<div class="controls-container">
<div class="controls-panel">
<mat-form-field appearance="fill">
<mat-label>{{ 'FRACTAL.ALGORITHM' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedAlgorithm"
(selectionChange)="onAlgorithmChange()">
@for (algo of algoInformation.entries; track algo.name) {
<mat-option [value]="algo.name">{{ algo.name }}</mat-option>
}
<mat-select [value]="'Mandelbrot'" (selectionChange)="onAlgorithmChange($event.value)">
<mat-option value="Mandelbrot">Mandelbrot</mat-option>
<mat-option value="Julia">Julia</mat-option>
<mat-option value="Burning Ship">Burning Ship</mat-option>
<mat-option value="Newton">Newton</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>{{ 'FRACTAL.COLOR_SCHEME' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedColorScheme"
(selectionChange)="onColorChanged()"
[disabled]="selectedAlgorithm === 'Newton'">
@for (name of FRACTAL_COLOR_SCHEMES; track name) {
<mat-option [value]="name">{{ name }}</mat-option>
}
<mat-select [value]="'Blue-Gold'" (selectionChange)="onColorChanged($event.value)">
<mat-option value="Blue-Gold">Blue-Gold</mat-option>
<mat-option value="Greyscale">Greyscale</mat-option>
<mat-option value="Fire">Fire</mat-option>
<mat-option value="Rainbow">Rainbow</mat-option>
</mat-select>
</mat-form-field>
</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()"
<button mat-raised-button color="primary" (click)="onReset()">
<mat-icon>undo</mat-icon> {{ 'FRACTAL.RESET' | translate }}
</button>
</div>
</div>
<app-babylon-canvas
[config]="renderConfig"
[renderCallback]="onRender"
(sceneReady)="onSceneReady($event)"
/>
</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>

View File

@@ -1,15 +1,18 @@
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
import { Component} 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 {MatFormField, MatLabel} from '@angular/material/input';
import {MatOption} from '@angular/material/core';
import {MatSelect} from '@angular/material/select';
import {AlgorithmInformation} from '../information/information.models';
import {UrlConstants} from '../../../constants/UrlConstants';
import {FormsModule} from '@angular/forms';
import {FractalService} from './service/fractal.service';
import {DEFAULT_ITERATION, FRACTAL_COLOR_SCHEMES, FractalConfig, MAX_ITERATION, MIN_ITERATION} from './fractal.model';
import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/rendering/canvas/babylon-canvas.component';
import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader';
import {PointerEventTypes, PointerInfo, Scene, ShaderMaterial, Vector2} from '@babylonjs/core';
import {MatButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon';
@Component({
selector: 'app-fractal',
@@ -25,12 +28,14 @@ import {DEFAULT_ITERATION, FRACTAL_COLOR_SCHEMES, FractalConfig, MAX_ITERATION,
MatOption,
MatSelect,
FormsModule,
MatInput
BabylonCanvas,
MatButton,
MatIcon
],
templateUrl: './fractal.component.html',
styleUrl: './fractal.component.scss',
})
export class FractalComponent implements AfterViewInit {
export class FractalComponent {
algoInformation: AlgorithmInformation = {
title: 'FRACTAL.EXPLANATION.TITLE',
entries: [
@@ -56,7 +61,7 @@ export class FractalComponent implements AfterViewInit {
}
],
disclaimer: 'FRACTAL.EXPLANATION.DISCLAIMER',
disclaimerBottom: '',
disclaimerBottom: 'FRACTAL.EXPLANATION.DISCLAIMER_BOTTOM',
disclaimerListEntry: [
'FRACTAL.EXPLANATION.DISCLAIMER_1',
'FRACTAL.EXPLANATION.DISCLAIMER_2',
@@ -64,126 +69,163 @@ export class FractalComponent implements AfterViewInit {
'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]
renderConfig: RenderConfig = {
mode: '2D',
initialViewSize: 100,
vertexShader: FRACTAL2D_VERTEX,
fragmentShader: FRACTAL2D_FRAGMENT,
uniformNames: ["worldViewProjection", "time", "targetPosition","center", "zoom", "maxIterations", "algorithm", "colorScheme", "juliaC"]
};
// --- State ---
private isDragging = false;
private dragStartX = 0;
private dragStartY = 0;
private dragStartPoint: { x: number, y: number } | null = null;
@ViewChild('fractalCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
selectedAlgorithm = 0;
selectedColorScheme = 0;
selectedAlgorithm: string = this.config.algorithm;
currentIteration: number = this.config.maxIterations;
selectedColorScheme: string = 'Blue-Gold';
zoom = 0.2;
offsetX = 0.0;
offsetY = 0.0;
maxIterations = 100;
ngAfterViewInit(): void {
this.draw();
}
juliaReal = -0.7;
juliaImag = 0.27015;
resetView(): void{
this.isDragging = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.config.offsetX = -0.5;
this.config.offsetY = 0;
this.config.zoom = 1;
}
// --- Render Callback ---
onRender: RenderCallback = (material: ShaderMaterial) => {
material.setVector2("center", new Vector2(this.offsetX, this.offsetY));
material.setFloat("zoom", this.zoom);
material.setInt("maxIterations", this.maxIterations);
material.setInt("algorithm", this.selectedAlgorithm);
material.setInt("colorScheme", this.selectedColorScheme);
material.setVector2("juliaC", new Vector2(this.juliaReal, this.juliaImag));
};
onAlgorithmChange(): void {
this.config.algorithm = this.selectedAlgorithm as any;
this.resetView();
this.draw();
}
onAlgorithmChange(algoName: string): void {
this.onReset()
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);
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;
}
}
//movement
onMouseDown(event: MouseEvent): void {
onColorChanged(schemeName: string): void {
switch(schemeName) {
case 'Blue-Gold': this.selectedColorScheme = 0; break;
case 'Greyscale': this.selectedColorScheme = 1; break;
case 'Fire': this.selectedColorScheme = 2; break;
case 'Rainbow': this.selectedColorScheme = 3; break;
}
}
onReset(): void {
this.zoom = 0.2;
this.offsetX = 0.0;
this.offsetY = 0.0;
}
onSceneReady(scene: Scene): void {
scene.onPointerObservable.add((pointerInfo) => {
switch (pointerInfo.type) {
case PointerEventTypes.POINTERDOWN:
this.onPointerDown(pointerInfo);
break;
case PointerEventTypes.POINTERUP:
this.onPointerUp();
break;
case PointerEventTypes.POINTERMOVE:
this.onPointerMove(pointerInfo);
break;
case PointerEventTypes.POINTERWHEEL:
this.onPointerWheel(pointerInfo);
break;
}
});
}
private onPointerDown(info: PointerInfo): void {
if (info.event.button !== 0) {
return;
}
this.isDragging = true;
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
this.dragStartPoint = { x: info.event.clientX, y: info.event.clientY };
}
onMouseUp(): void {
private onPointerUp(): void {
this.isDragging = false;
this.dragStartPoint = null;
}
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();
private onPointerMove(info: PointerInfo): void {
if (!this.isDragging || !this.dragStartPoint) {
return;
}
onWheel(event: WheelEvent): void {
const event = info.event as PointerEvent;
const deltaX = event.clientX - this.dragStartPoint.x;
const deltaY = event.clientY - this.dragStartPoint.y;
const element = event.target as HTMLElement;
const height = element.clientHeight;
const scaleFactor = 1 / (height * this.zoom);
this.offsetX += deltaX * scaleFactor;
this.offsetY += deltaY * scaleFactor;
this.dragStartPoint = { x: event.clientX, y: event.clientY };
}
private onPointerWheel(info: PointerInfo): void {
const event = info.event as WheelEvent;
event.preventDefault();
const element = event.target as HTMLElement;
const rect = element.getBoundingClientRect();
const mouseXPixels = -(event.clientX - rect.left - rect.width / 2);
const mouseYPixels = -(event.clientY - rect.top - rect.height / 2);
const mouseXView = mouseXPixels / rect.height;
const mouseYView = mouseYPixels / rect.height;
const mouseXWorld = mouseXView / this.zoom + this.offsetX;
const mouseYWorld = mouseYView / this.zoom + this.offsetY;
const zoomFactor = 1.1;
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;
if (event.deltaY < 0) {
this.zoom *= zoomFactor;
} else {
this.config.zoom /= zoomFactor;
this.zoom /= zoomFactor;
}
const newReScale = 4 / (this.config.width * this.config.zoom);
const newImScale = 4 / (this.config.height * this.config.zoom);
const optimalIterations = this.getIterationsForZoom(this.zoom);
this.maxIterations = Math.min(optimalIterations, 3000);
this.config.offsetX = mouseRe - (mouseX - this.config.width / 2) * newReScale;
this.config.offsetY = mouseIm - (mouseY - this.config.height / 2) * newImScale;
this.draw();
this.offsetX = mouseXWorld - mouseXView / this.zoom;
this.offsetY = mouseYWorld - mouseYView / this.zoom;
}
protected readonly MIN_ITERATION = MIN_ITERATION;
protected readonly MAX_ITERATION = MAX_ITERATION;
protected readonly FRACTAL_COLOR_SCHEMES = FRACTAL_COLOR_SCHEMES;
private getIterationsForZoom(zoom: number): number {
const baseIterations = 100;
const factor = 200;
if (zoom <= 1) {
return baseIterations;
}
return Math.floor(baseIterations + Math.log10(zoom) * factor);
}
}

View File

@@ -1,33 +0,0 @@
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;

View File

@@ -0,0 +1,155 @@
export const FRACTAL2D_VERTEX = `
precision highp float;
attribute vec3 position;
attribute vec2 uv;
uniform mat4 worldViewProjection;
varying vec2 vUV;
void main() {
gl_Position = worldViewProjection * vec4(position, 1.0);
vUV = uv;
}
`;
export const FRACTAL2D_FRAGMENT = `
precision highp float;
varying vec2 vUV;
uniform vec2 resolution;
uniform vec2 center; // OffsetX, OffsetY
uniform float zoom;
uniform int maxIterations;
uniform int algorithm; // 0:Mandel, 1:Julia, 2:Ship, 3:Newton
uniform int colorScheme; // 0:BlueGold, 1:Greyscale, 2:Fire, 3:Rainbow
uniform vec2 juliaC;
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
// --- Color Schemes ---
vec3 getColor(float t, int iter, int maxIter, int root) {
if (iter >= maxIter) return vec3(0.0);
// special newton coloring
if (algorithm == 3) {
float val = 1.0 - (float(iter) / 20.0); // Weicheres Shading für Newton
val = clamp(val, 0.0, 1.0);
if (root == 1) return vec3(val, 0.0, 0.0); // Rot
if (root == 2) return vec3(0.0, val, 0.0); // Grün
if (root == 3) return vec3(0.0, 0.0, val); // Blau
return vec3(0.0);
}
// default color
if (colorScheme == 0) { // Blue-Gold
return vec3(
9.0 * (1.0-t)*t*t*t,
15.0 * (1.0-t)*(1.0-t)*t*t,
8.5 * (1.0-t)*(1.0-t)*(1.0-t)*t
) * 3.0;
}
if (colorScheme == 1) { // Greyscale
return vec3(t);
}
if (colorScheme == 2) { // Fire
float f = sqrt(t);
return vec3(f * 2.0, (f - 0.3) * 3.0, (f - 0.6) * 6.0);
}
if (colorScheme == 3) { // Rainbow
return hsv2rgb(vec3(t * 5.0, 1.0, 1.0));
}
return vec3(t);
}
void main(void) {
float aspect = resolution.x / resolution.y;
vec2 uv = (vUV - 0.5) * vec2(aspect, 1.0);
vec2 c = uv / zoom + center;
vec2 z = c;
// For Julia is c fix, z changes. For Mandel is z=0, c changes.
if (algorithm == 1) {
z = c;
c = juliaC;
} else if (algorithm != 3) {
z = vec2(0.0);
}
int iter = 0;
int root = 0;
// --- Algorithms ---
if (algorithm == 3) { // Newton: z^3 - 1
z = c;
for(int i=0; i<100; i++) {
if (i >= maxIterations) break;
// z^3 - 1
// z_new = z - (z^3 - 1) / (3*z^2)
// simplified: z_new = (2*z^3 + 1) / (3*z^2)
float zx2 = z.x * z.x;
float zy2 = z.y * z.y;
float denom = 3.0 * (zx2 + zy2) * (zx2 + zy2); // |3z^2|^2 simplified
// z -= (z^3-1)/(3z^2)
vec2 z2 = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y);
vec2 z3 = vec2(z2.x*z.x - z2.y*z.y, z2.x*z.y + z2.y*z.x);
vec2 num = z3 - vec2(1.0, 0.0);
vec2 den = 3.0 * z2;
// Division Complex: (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c^2+d^2)
float d = den.x*den.x + den.y*den.y;
if(d < 0.000001) { iter=maxIterations; break; }
vec2 div = vec2(
(num.x*den.x + num.y*den.y)/d,
(num.y*den.x - num.x*den.y)/d
);
z -= div;
iter++;
// Roots check
// 1. (1, 0)
if (distance(z, vec2(1.0, 0.0)) < 0.001) { root = 1; break; }
// 2. (-0.5, 0.866)
if (distance(z, vec2(-0.5, 0.866)) < 0.001) { root = 2; break; }
// 3. (-0.5, -0.866)
if (distance(z, vec2(-0.5, -0.866)) < 0.001) { root = 3; break; }
}
}
else { // Mandelbrot (0), Julia (1), Burning Ship (2)
for(int i=0; i<2000; i++) {
if (i >= maxIterations) break;
float x2 = z.x * z.x;
float y2 = z.y * z.y;
if (x2 + y2 > 4.0) {
iter = i;
break;
}
iter = i;
if (algorithm == 2) { // Burning Ship
z.y = abs(z.y);
z.x = abs(z.x);
}
// z = z^2 + c
float nextX = x2 - y2 + c.x;
z.y = 2.0 * z.x * z.y + c.y;
z.x = nextX;
}
}
float t = float(iter) / float(maxIterations);
vec3 color = getColor(t, iter, maxIterations, root);
gl_FragColor = vec4(color, 1.0);
}
`;

View File

@@ -1,256 +0,0 @@
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];
}
}

View File

@@ -13,7 +13,7 @@
</div>
<app-babylon-canvas
[config]="fractalConfig"
[renderCallback]="onRender">
</app-babylon-canvas>
[renderCallback]="onRender"
/>
</mat-card-content>
</mat-card>

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,6 +1,6 @@
import {Component} from '@angular/core';
import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core';
import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal.shader';
import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader';
import {Information} from '../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core';
@@ -52,9 +52,10 @@ export class Fractal3dComponent {
fractalConfig: RenderConfig = {
mode: '3D',
initialViewSize: 4,
vertexShader: MANDELBULB_VERTEX,
fragmentShader: MANDELBULB_FRAGMENT,
uniformNames: ["power", "fractalType"]
uniformNames: ["time", "power", "fractalType"]
};
private readonly fractalPower = 8;

View File

@@ -1,8 +1,9 @@
import {AfterViewInit, Component, ElementRef, inject, Input, NgZone, OnDestroy, ViewChild} from '@angular/core';
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';
export interface RenderConfig {
mode: '2D' | '3D';
initialViewSize: number;
vertexShader: string;
fragmentShader: string;
uniformNames: string[];
@@ -24,6 +25,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
@Input({ required: true }) config!: RenderConfig;
@Input() renderCallback?: RenderCallback;
@Output() sceneReady = new EventEmitter<Scene>();
private engine!: Engine;
private scene!: Scene;
private shaderMaterial!: ShaderMaterial;
@@ -57,6 +60,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
canvas.addEventListener('wheel', (evt: WheelEvent) => evt.preventDefault(), { passive: false });
this.createShaderMaterial();
this.createFullScreenRect();
this.sceneReady.emit(this.scene);
this.addRenderLoop(canvas);
this.addResizeHandler();
}
@@ -75,13 +79,12 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
cam.mode = Camera.ORTHOGRAPHIC_CAMERA;
const aspect = canvas.width / canvas.height;
const viewSize = 10;
const viewSize = this.config?.initialViewSize ?? 10;
cam.orthoLeft = -viewSize * aspect / 2;
cam.orthoRight = viewSize * aspect / 2;
cam.orthoTop = viewSize / 2;
cam.orthoBottom = -viewSize / 2;
cam.attachControl(canvas, true, false);
this.camera = cam;
}
@@ -92,12 +95,13 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
cam.maxZ = 100;
cam.lowerRadiusLimit = 1.5;
cam.upperRadiusLimit = 20;
cam.radius = this.config?.initialViewSize ?? 1;
cam.attachControl(canvas, true);
this.camera = cam;
}
private createFullScreenRect() {
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
const plane = MeshBuilder.CreatePlane("plane", {size: 110}, this.scene);
if (this.config.mode === '3D') {
plane.parent = this.camera;
@@ -120,7 +124,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
},
{
attributes: ["position", "uv"],
uniforms: ["time", "resolution", "cameraPosition", "targetPosition", ...this.config.uniformNames]
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames]
}
);
this.shaderMaterial.disableDepthWrite = true;

View File

@@ -380,6 +380,7 @@
"FRACTAL": {
"TITLE": "Fraktale",
"ALGORITHM": "Algorithmen",
"RESET": "Reset",
"COLOR_SCHEME": "Farbschema",
"MAX_ITERATION": "Maximale Auflösung",
"EXPLANATION": {
@@ -392,7 +393,8 @@
"DISCLAIMER_1": "Unendliche Tiefe: Egal wie weit du hineinzoomst, es erscheinen immer neue, komplexe Strukturen, die dem Ganzen oft ähneln (Selbstähnlichkeit).",
"DISCLAIMER_2": "Fluchtzeit-Algorithmus: Die Farben geben meist an, wie schnell eine Folge einen bestimmten Schwellenwert überschreitet je schneller, desto 'heißer' oder heller die Farbe.",
"DISCLAIMER_3": "Komplexe Zahlen: Die Berechnung findet nicht in einem normalen Koordinatensystem statt, sondern in der komplexen Ebene mit realen und imaginären Anteilen.",
"DISCLAIMER_4": "Rechenintensität: Da für jeden Pixel hunderte Berechnungen durchgeführt werden, sind Fraktale ein klassischer Benchmark für die Performance von Grafikprozessoren (GPUs)."
"DISCLAIMER_4": "Rechenintensität: Da für jeden Pixel hunderte Berechnungen durchgeführt werden, sind Fraktale ein klassischer Benchmark für die Performance von Grafikprozessoren (GPUs).",
"DISCLAIMER_BOTTOM": "Grafikkarten rechnen standardmäßig mit 32-Bit Fließkommazahlen (float). Diese haben nur etwa 7 Stellen Genauigkeit. Bei sehr hohem Zoom (> 100.000) ist der Unterschied zwischen zwei Pixeln so winzig, dass die Grafikkarte ihn nicht mehr darstellen kann. Sie berechnet für 10 Pixel nebeneinander exakt denselben Wert -> Du siehst Blöcke oder Treppenstufen."
}
},
"FRACTAL3D": {

View File

@@ -379,6 +379,7 @@
"FRACTAL": {
"TITLE": "Fractals",
"ALGORITHM": "Algorithms",
"RESET": "Reset",
"COLOR_SCHEME": "Color Scheme",
"MAX_ITERATION": "Max. Resolution",
"EXPLANATION": {
@@ -391,9 +392,9 @@
"DISCLAIMER_1": "Infinite Depth: No matter how far you zoom in, new complex structures appear that often resemble the whole (self-similarity).",
"DISCLAIMER_2": "Escape-Time Algorithm: Colors usually represent how quickly a sequence exceeds a certain threshold—the faster it escapes, the 'hotter' or brighter the color.",
"DISCLAIMER_3": "Complex Numbers: Calculations don't happen in a standard coordinate system, but in the complex plane using real and imaginary components.",
"DISCLAIMER_4": "Computational Load: Since hundreds of calculations are performed for every single pixel, fractals are a classic benchmark for GPU and processor performance."
"DISCLAIMER_4": "Computational Load: Since hundreds of calculations are performed for every single pixel, fractals are a classic benchmark for GPU and processor performance.",
"DISCLAIMER_BOTTOM": "Graphics cards calculate with 32-bit floating point numbers (float) by default. These only have about 7 digits of accuracy. At very high zoom levels (> 100,000), the difference between two pixels is so tiny that the graphics card can no longer display it. It calculates exactly the same value for 10 pixels next to each other -> you see blocks or stair steps."
}
},
"FRACTAL3D": {
"TITLE": "3D Fractals",