From ea15e66c507d1956ad1b94379f8182a28022119a Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Thu, 12 Feb 2026 08:35:26 +0100 Subject: [PATCH 1/3] optimized handling a little bit and fixed small scss problems --- src/app/pages/algorithms/algorithms.component.scss | 5 +++++ src/app/pages/algorithms/fractal3d/fractal3d.component.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/app/pages/algorithms/algorithms.component.scss b/src/app/pages/algorithms/algorithms.component.scss index ba0db08..c98bda9 100644 --- a/src/app/pages/algorithms/algorithms.component.scss +++ b/src/app/pages/algorithms/algorithms.component.scss @@ -12,5 +12,10 @@ cursor: pointer; min-width: 450px; max-width: 450px; + + &:hover { + transform: translateY(-5px); + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + } } } diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index d0312f8..3d2d84a 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -80,6 +80,10 @@ export class Fractal3dComponent implements AfterViewInit, OnDestroy { camera.upperRadiusLimit = 20; camera.attachControl(this.canvasRef.nativeElement, true); + canvas.addEventListener('wheel', (evt: WheelEvent) => { + evt.preventDefault(); + }, { passive: false }); + const plane = MeshBuilder.CreatePlane("plane", { size: 10 }, this.scene); plane.parent = camera; plane.position.z = 1; From cc6997e7327e91faad849c318373f76f095a9c3e Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Thu, 12 Feb 2026 09:13:35 +0100 Subject: [PATCH 2/3] Excluded the rendering in an own component --- .../fractal3d/fractal3d.component.html | 7 +- .../fractal3d/fractal3d.component.ts | 114 +++---------- .../canvas/babylon-canvas.component.html | 3 + .../canvas/babylon-canvas.component.scss | 2 + .../canvas/babylon-canvas.component.ts | 154 ++++++++++++++++++ 5 files changed, 187 insertions(+), 93 deletions(-) create mode 100644 src/app/shared/rendering/canvas/babylon-canvas.component.html create mode 100644 src/app/shared/rendering/canvas/babylon-canvas.component.scss create mode 100644 src/app/shared/rendering/canvas/babylon-canvas.component.ts diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.html b/src/app/pages/algorithms/fractal3d/fractal3d.component.html index 07ac16f..abb834c 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.html +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.html @@ -11,8 +11,9 @@ -
- -
+ + diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index 3d2d84a..3927dbe 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -1,5 +1,5 @@ -import {AfterViewInit, Component, ElementRef, inject, NgZone, OnDestroy, ViewChild} from '@angular/core'; -import {ArcRotateCamera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core'; +import {Component} from '@angular/core'; +import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core'; import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal.shader'; import {Information} from '../information/information'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; @@ -7,6 +7,7 @@ import {TranslatePipe} from '@ngx-translate/core'; import {AlgorithmInformation} from '../information/information.models'; import {UrlConstants} from '../../../constants/UrlConstants'; import {MatButton} from '@angular/material/button'; +import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/rendering/canvas/babylon-canvas.component'; @Component({ selector: 'app-fractal3d', @@ -17,15 +18,13 @@ import {MatButton} from '@angular/material/button'; MatCardHeader, MatCardTitle, TranslatePipe, - MatButton + MatButton, + BabylonCanvas ], templateUrl: './fractal3d.component.html', styleUrl: './fractal3d.component.scss', }) -export class Fractal3dComponent implements AfterViewInit, OnDestroy { - readonly ngZone = inject(NgZone); - - @ViewChild('renderCanvas') canvasRef!: ElementRef; +export class Fractal3dComponent { algoInformation: AlgorithmInformation = { title: 'FRACTAL3D.EXPLANATION.TITLE', @@ -51,100 +50,35 @@ export class Fractal3dComponent implements AfterViewInit, OnDestroy { disclaimerListEntry: ['FRACTAL3D.EXPLANATION.DISCLAIMER_1', 'FRACTAL3D.EXPLANATION.DISCLAIMER_2', 'FRACTAL3D.EXPLANATION.DISCLAIMER_3', 'FRACTAL3D.EXPLANATION.DISCLAIMER_4'] }; + fractalConfig: RenderConfig = { + mode: '3D', + vertexShader: MANDELBULB_VERTEX, + fragmentShader: MANDELBULB_FRAGMENT, + uniformNames: ["power", "fractalType"] + }; + private readonly fractalPower = 8; - private engine!: Engine; - private scene!: Scene; - private shaderMaterial!: ShaderMaterial; private time = 0; - private triggerCamUpdate = false; - private cameraPosition: number = 3.5; + private oldType = 0; public currentFractalType = 0; - ngAfterViewInit(): void { - this.ngZone.runOutsideAngular(() => { - this.initBabylon(); - }); - } + onRender: RenderCallback = (material: ShaderMaterial, camera: Camera) => { + this.time += 0.005; - private initBabylon(): void { - const canvas = this.canvasRef.nativeElement; - this.engine = new Engine(canvas, true); - this.scene = new Scene(this.engine); + if (this.oldType != this.currentFractalType && camera instanceof ArcRotateCamera) { + this.oldType = this.currentFractalType; + camera.radius = this.currentFractalType == 1 ? 15 : 4; + } - const camera = new ArcRotateCamera("Camera", 0, Math.PI / 2, 4, Vector3.Zero(), this.scene); - camera.wheelPrecision = 100; - camera.minZ = 0.1; - camera.maxZ = 100; - camera.lowerRadiusLimit = 1.5; - camera.upperRadiusLimit = 20; - camera.attachControl(this.canvasRef.nativeElement, true); + material.setFloat("time", this.time); + material.setFloat("power", this.fractalPower); + material.setInt("fractalType", this.currentFractalType); + }; - canvas.addEventListener('wheel', (evt: WheelEvent) => { - evt.preventDefault(); - }, { passive: false }); - - const plane = MeshBuilder.CreatePlane("plane", { size: 10 }, this.scene); - plane.parent = camera; - plane.position.z = 1; - plane.alwaysSelectAsActiveMesh = true; - - this.shaderMaterial = new ShaderMaterial( - "mandelbulbShader", - this.scene, - { - vertexSource: MANDELBULB_VERTEX, - fragmentSource: MANDELBULB_FRAGMENT - }, - { - attributes: ["position", "uv"], - uniforms: ["time", "resolution", "cameraPosition", "targetPosition", "power", "fractalType"] - } - ); - this.shaderMaterial.disableDepthWrite = true; - this.shaderMaterial.backFaceCulling = false; - - plane.material = this.shaderMaterial; - - this.engine.runRenderLoop(() => { - this.time += 0.005; - - if (this.triggerCamUpdate) - { - this.triggerCamUpdate = false; - camera.radius = this.cameraPosition; - } - - if (this.shaderMaterial) { - this.shaderMaterial.setFloat("time", this.time); - this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); - this.shaderMaterial.setVector3("cameraPosition", camera.position); - this.shaderMaterial.setVector3("targetPosition", camera.target); - this.shaderMaterial.setFloat("power", this.fractalPower); - this.shaderMaterial.setInt("fractalType", this.currentFractalType); - } - - this.scene.render(); - }); - - window.addEventListener('resize', () => this.engine.resize()); - } onFractalTypeChange(type: number): void { this.currentFractalType = type; - if (type === 0 ||type === 2) - { - this.cameraPosition = 4; - } - else { - this.cameraPosition = 15; - } - this.triggerCamUpdate = true; } - ngOnDestroy(): void { - if (this.engine) { - this.engine.dispose(); - } - } } diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.html b/src/app/shared/rendering/canvas/babylon-canvas.component.html new file mode 100644 index 0000000..ea533fa --- /dev/null +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.scss b/src/app/shared/rendering/canvas/babylon-canvas.component.scss new file mode 100644 index 0000000..daadde6 --- /dev/null +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.scss @@ -0,0 +1,2 @@ +.canvas-container { width: 100%; height: 1000px; } +canvas { width: 100%; height: 100%; touch-action: none; border-width: 0; border-color: transparent; border-style: hidden; } diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/rendering/canvas/babylon-canvas.component.ts new file mode 100644 index 0000000..92802bb --- /dev/null +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.ts @@ -0,0 +1,154 @@ +import {AfterViewInit, Component, ElementRef, inject, Input, NgZone, OnDestroy, ViewChild} from '@angular/core'; +import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core'; + +export interface RenderConfig { + mode: '2D' | '3D'; + vertexShader: string; + fragmentShader: string; + uniformNames: string[]; +} + +export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void; + +@Component({ + selector: 'app-babylon-canvas', + imports: [], + templateUrl: './babylon-canvas.component.html', + styleUrl: './babylon-canvas.component.scss', +}) +export class BabylonCanvas implements AfterViewInit, OnDestroy { + readonly ngZone = inject(NgZone); + + @ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef; + + @Input({ required: true }) config!: RenderConfig; + @Input() renderCallback?: RenderCallback; + + private engine!: Engine; + private scene!: Scene; + private shaderMaterial!: ShaderMaterial; + private camera!: Camera; + + ngAfterViewInit(): void { + this.ngZone.runOutsideAngular(() => { + this.initBabylon(); + }); + } + + /*ngOnChanges(changes: SimpleChanges): void { + //if something changes during runtime, new materials are necessary ans needs maybe build here + }*/ + + ngOnDestroy(): void { + const canvas = this.canvasRef?.nativeElement; + if (canvas) { + //remove listener if needed + } + if (this.engine) { + this.engine.dispose(); + } + } + + private initBabylon(): void { + const canvas = this.canvasRef.nativeElement; + this.engine = new Engine(canvas, true); + this.scene = new Scene(this.engine); + this.setupCamera(canvas); + canvas.addEventListener('wheel', (evt: WheelEvent) => evt.preventDefault(), { passive: false }); + this.createShaderMaterial(); + this.createFullScreenRect(); + this.addRenderLoop(canvas); + this.addResizeHandler(); + } + + private setupCamera(canvas: HTMLCanvasElement) { + if (this.config.mode === '3D') { + this.setup3dCamera(canvas); + return; + } + + this.setup2dCamera(canvas); + } + + private setup2dCamera(canvas: HTMLCanvasElement) { + const cam = new ArcRotateCamera("Camera2D", -Math.PI / 2, Math.PI / 2, 10, Vector3.Zero(), this.scene); + cam.mode = Camera.ORTHOGRAPHIC_CAMERA; + + const aspect = canvas.width / canvas.height; + const viewSize = 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; + } + + private setup3dCamera(canvas: HTMLCanvasElement) { + const cam = new ArcRotateCamera("Camera", 0, Math.PI / 2, 4, Vector3.Zero(), this.scene); + cam.wheelPrecision = 100; + cam.minZ = 0.1; + cam.maxZ = 100; + cam.lowerRadiusLimit = 1.5; + cam.upperRadiusLimit = 20; + cam.attachControl(canvas, true); + this.camera = cam; + } + + private createFullScreenRect() { + const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene); + + if (this.config.mode === '3D') { + plane.parent = this.camera; + plane.position.z = 1; + } else { + plane.lookAt(this.camera.position); + } + plane.alwaysSelectAsActiveMesh = true; + + plane.material = this.shaderMaterial; + } + + private createShaderMaterial() { + this.shaderMaterial = new ShaderMaterial( + "shaderMaterial", + this.scene, + { + vertexSource: this.config.vertexShader, + fragmentSource: this.config.fragmentShader + }, + { + attributes: ["position", "uv"], + uniforms: ["time", "resolution", "cameraPosition", "targetPosition", ...this.config.uniformNames] + } + ); + this.shaderMaterial.disableDepthWrite = true; + this.shaderMaterial.backFaceCulling = false; + } + + private addRenderLoop(canvas: HTMLCanvasElement) { + this.engine.runRenderLoop(() => { + + // callback call to call specific uniforms + if (this.renderCallback) { + this.renderCallback(this.shaderMaterial, this.camera, canvas, this.scene); + } + + // default uniforms which maybe each scene has + this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); + this.shaderMaterial.setVector3("cameraPosition", this.camera.position); + + this.scene.render(); + }); + } + + private addResizeHandler() { + window.addEventListener('resize', () => { + this.engine.resize(); + if (this.config.mode === '2D' && this.camera instanceof ArcRotateCamera && this.camera.mode === Camera.ORTHOGRAPHIC_CAMERA) { + //maybe update the aspect ratio here + } + }); + } +} From c409cd08b15461162c758a2b137ee454924b0c6e Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Thu, 12 Feb 2026 10:14:22 +0100 Subject: [PATCH 3/3] Changed 2d fractals to webgl for more performance --- .../algorithms/fractal/fractal.component.html | 74 ++--- .../algorithms/fractal/fractal.component.ts | 232 +++++++++------- .../pages/algorithms/fractal/fractal.model.ts | 33 --- .../algorithms/fractal/fractal.shader.ts | 155 +++++++++++ .../fractal/service/fractal.service.ts | 256 ------------------ .../fractal3d/fractal3d.component.html | 4 +- .../fractal3d/fractal3d.component.scss | 2 - .../fractal3d/fractal3d.component.ts | 5 +- ...{fractal.shader.ts => fractal3d.shader.ts} | 0 .../canvas/babylon-canvas.component.ts | 14 +- src/assets/i18n/de.json | 4 +- src/assets/i18n/en.json | 5 +- 12 files changed, 341 insertions(+), 443 deletions(-) delete mode 100644 src/app/pages/algorithms/fractal/fractal.model.ts create mode 100644 src/app/pages/algorithms/fractal/fractal.shader.ts delete mode 100644 src/app/pages/algorithms/fractal/service/fractal.service.ts rename src/app/pages/algorithms/fractal3d/{fractal.shader.ts => fractal3d.shader.ts} (100%) diff --git a/src/app/pages/algorithms/fractal/fractal.component.html b/src/app/pages/algorithms/fractal/fractal.component.html index 8ff55e7..9398cad 100644 --- a/src/app/pages/algorithms/fractal/fractal.component.html +++ b/src/app/pages/algorithms/fractal/fractal.component.html @@ -4,52 +4,36 @@ -
- - {{ 'FRACTAL.ALGORITHM' | translate }} - - @for (algo of algoInformation.entries; track algo.name) { - {{ algo.name }} - } - +
+
+ + {{ 'FRACTAL.ALGORITHM' | translate }} + + Mandelbrot + Julia + Burning Ship + Newton + + + + {{ 'FRACTAL.COLOR_SCHEME' | translate }} + + Blue-Gold + Greyscale + Fire + Rainbow + + - - - {{ 'FRACTAL.COLOR_SCHEME' | translate }} - - @for (name of FRACTAL_COLOR_SCHEMES; track name) { - {{ name }} - } - - - - - {{ 'FRACTAL.MAX_ITERATION' | translate }} - - -
-
- - + +
+ diff --git a/src/app/pages/algorithms/fractal/fractal.component.ts b/src/app/pages/algorithms/fractal/fractal.component.ts index 8c6999b..b70ac0e 100644 --- a/src/app/pages/algorithms/fractal/fractal.component.ts +++ b/src/app/pages/algorithms/fractal/fractal.component.ts @@ -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; + 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; + private onPointerMove(info: PointerInfo): void { + if (!this.isDragging || !this.dragStartPoint) { + return; + } - const deltaX = event.clientX - this.dragStartX; - const deltaY = event.clientY - this.dragStartY; + const event = info.event as PointerEvent; - const reScale = 4 / (this.config.width * this.config.zoom); - const imScale = 4 / (this.config.height * this.config.zoom); + const deltaX = event.clientX - this.dragStartPoint.x; + const deltaY = event.clientY - this.dragStartPoint.y; - this.config.offsetX -= deltaX * reScale; - this.config.offsetY -= deltaY * imScale; + const element = event.target as HTMLElement; + const height = element.clientHeight; - this.dragStartX = event.clientX; - this.dragStartY = event.clientY; + const scaleFactor = 1 / (height * this.zoom); - this.draw(); + this.offsetX += deltaX * scaleFactor; + this.offsetY += deltaY * scaleFactor; + + this.dragStartPoint = { x: event.clientX, y: event.clientY }; } - onWheel(event: WheelEvent): void { + 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); + } } diff --git a/src/app/pages/algorithms/fractal/fractal.model.ts b/src/app/pages/algorithms/fractal/fractal.model.ts deleted file mode 100644 index 52f541f..0000000 --- a/src/app/pages/algorithms/fractal/fractal.model.ts +++ /dev/null @@ -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; diff --git a/src/app/pages/algorithms/fractal/fractal.shader.ts b/src/app/pages/algorithms/fractal/fractal.shader.ts new file mode 100644 index 0000000..d4fbff6 --- /dev/null +++ b/src/app/pages/algorithms/fractal/fractal.shader.ts @@ -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); + } +`; diff --git a/src/app/pages/algorithms/fractal/service/fractal.service.ts b/src/app/pages/algorithms/fractal/service/fractal.service.ts deleted file mode 100644 index 967855b..0000000 --- a/src/app/pages/algorithms/fractal/service/fractal.service.ts +++ /dev/null @@ -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) { - - 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]; - } -} diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.html b/src/app/pages/algorithms/fractal3d/fractal3d.component.html index abb834c..9663325 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.html +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.html @@ -13,7 +13,7 @@
- + [renderCallback]="onRender" + />
diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.scss b/src/app/pages/algorithms/fractal3d/fractal3d.component.scss index daadde6..e69de29 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.scss +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.scss @@ -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; } diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index 3927dbe..b65351b 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -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; diff --git a/src/app/pages/algorithms/fractal3d/fractal.shader.ts b/src/app/pages/algorithms/fractal3d/fractal3d.shader.ts similarity index 100% rename from src/app/pages/algorithms/fractal3d/fractal.shader.ts rename to src/app/pages/algorithms/fractal3d/fractal3d.shader.ts diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/rendering/canvas/babylon-canvas.component.ts index 92802bb..8eb1fc9 100644 --- a/src/app/shared/rendering/canvas/babylon-canvas.component.ts +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.ts @@ -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(); + 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; diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index d013a9b..0fa1e57 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -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": { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 12df05b..f770896 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -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",