feature/webGPU #25
@@ -13,6 +13,7 @@ export const routes: Routes = [
|
||||
{ path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT},
|
||||
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT},
|
||||
{ path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT},
|
||||
{ path: RouterConstants.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT}
|
||||
{ path: RouterConstants.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT},
|
||||
{ path: RouterConstants.PENDULUM.PATH, component: RouterConstants.PENDULUM.COMPONENT}
|
||||
];
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.comp
|
||||
import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/labyrinth.component';
|
||||
import {FractalComponent} from '../pages/algorithms/fractal/fractal.component';
|
||||
import {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component';
|
||||
import PendulumComponent from '../pages/algorithms/pendulum/pendulum.component';
|
||||
|
||||
export class RouterConstants {
|
||||
|
||||
@@ -65,6 +66,12 @@ export class RouterConstants {
|
||||
COMPONENT: Fractal3dComponent
|
||||
};
|
||||
|
||||
static readonly PENDULUM = {
|
||||
PATH: 'algorithms/pendulum',
|
||||
LINK: '/algorithms/pendulum',
|
||||
COMPONENT: PendulumComponent
|
||||
};
|
||||
|
||||
static readonly IMPRINT = {
|
||||
PATH: 'imprint',
|
||||
LINK: '/imprint',
|
||||
|
||||
@@ -17,4 +17,5 @@
|
||||
static readonly MANDELBULB_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle'
|
||||
static readonly MANDELBOX_WIKI = 'https://de.wikipedia.org/wiki/Mandelbox'
|
||||
static readonly JULIA3D_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle'
|
||||
static readonly DOUBLE_PENDULUM_WIKI = 'https://de.wikipedia.org/wiki/Doppelpendel'
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
}
|
||||
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p>
|
||||
</div>
|
||||
<div class="grid-size">
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<div class="input-container">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
@@ -47,7 +47,7 @@
|
||||
(ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
@@ -58,7 +58,7 @@
|
||||
(ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'GOL.SPEED' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
|
||||
@@ -8,9 +8,9 @@ import {MatSelect} from '@angular/material/select';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/rendering/canvas/babylon-canvas.component';
|
||||
import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/rendering/canvas/babylon-canvas.component';
|
||||
import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader';
|
||||
import {PointerEventTypes, PointerInfo, Scene, ShaderMaterial, Vector2} from '@babylonjs/core';
|
||||
import {PointerEventTypes, PointerInfo, ShaderMaterial, Vector2} from '@babylonjs/core';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||
@@ -155,8 +155,8 @@ export class FractalComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
onSceneReady(scene: Scene): void {
|
||||
scene.onPointerObservable.add((pointerInfo) => {
|
||||
onSceneReady(event: SceneEventData): void {
|
||||
event.scene.onPointerObservable.add((pointerInfo) => {
|
||||
switch (pointerInfo.type) {
|
||||
|
||||
case PointerEventTypes.POINTERDOWN:
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
<div class="controls-panel">
|
||||
<div class="grid-size">
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<div class="input-container">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
@@ -38,7 +38,7 @@
|
||||
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||
/> </mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
|
||||
54
src/app/pages/algorithms/pendulum/pendulum.component.html
Normal file
54
src/app/pages/algorithms/pendulum/pendulum.component.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<mat-card class="container">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'PENDULUM.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
<div class="controls-container">
|
||||
<div class="slider-control-container">
|
||||
<p style="white-space: nowrap">{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}</p>
|
||||
<ngx-slider [(value)]="simParams.trailDecay" [options]="trailDecayOptions" ></ngx-slider>
|
||||
<p style="white-space: nowrap">{{ 'PENDULUM.ATTRACTION' | translate }}</p>
|
||||
<ngx-slider [(value)]="simParams.g" [options]="gravityOptions" ></ngx-slider>
|
||||
</div>
|
||||
<div class="slider-control-container">
|
||||
<p style="white-space: nowrap">{{ 'PENDULUM.L1_LENGTH' | translate }}</p>
|
||||
<ngx-slider [(value)]="simParams.l1" [options]="lengthOptions" ></ngx-slider>
|
||||
<p style="white-space: nowrap">{{ 'PENDULUM.L2_LENGTH' | translate }}</p>
|
||||
<ngx-slider [(value)]="simParams.l2" [options]="lengthOptions" ></ngx-slider>
|
||||
</div>
|
||||
<div class="slider-control-container">
|
||||
<p style="white-space: nowrap">{{ 'PENDULUM.M1_MASS' | translate }}</p>
|
||||
<ngx-slider [(value)]="simParams.m1" [options]="massOptions" ></ngx-slider>
|
||||
<p style="white-space: nowrap">{{ 'PENDULUM.M2_MASS' | translate }}</p>
|
||||
<ngx-slider [(value)]="simParams.m2" [options]="massOptions" ></ngx-slider>
|
||||
</div>
|
||||
<div class="slider-control-container">
|
||||
<p style="white-space: nowrap">{{ 'PENDULUM.DAMPING' | translate }}</p>
|
||||
<ngx-slider [(value)]="simParams.damping" [options]="dampingOptions" ></ngx-slider>
|
||||
</div>
|
||||
<div class="slider-control-container">
|
||||
<button mat-raised-button color="primary" (click)="pushPendulum(true)">
|
||||
{{ 'PENDULUM.POKE_M1' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="pushPendulum(false)">
|
||||
{{ 'PENDULUM.POKE_M2' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="resetPendulum()">
|
||||
{{ 'PENDULUM.RESET' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="legend" style="margin-top: 10px">
|
||||
<span><span class="legend-color L1"></span> L1</span>
|
||||
<span><span class="legend-color L2"></span> L2</span>
|
||||
<span><span class="legend-color M1"></span> M1</span>
|
||||
<span><span class="legend-color M2"></span> M2</span>
|
||||
</div>
|
||||
</div>
|
||||
<app-babylon-canvas
|
||||
[config]="renderConfig"
|
||||
(sceneReady)="onSceneReady($event)"
|
||||
(sceneResized)="onSceneReady($event)"
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
240
src/app/pages/algorithms/pendulum/pendulum.component.ts
Normal file
240
src/app/pages/algorithms/pendulum/pendulum.component.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/rendering/canvas/babylon-canvas.component';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {ComputeShader, ShaderLanguage, StorageBuffer} from '@babylonjs/core';
|
||||
import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PENDULUM_RENDER_COMPUTE_SHADER_WGSL, PENDULUM_VERTEX_SHADER_WGSL} from './pendulum.shader';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||
import {DEFAULT_DAMPING, DEFAULT_G, DEFAULT_L1_LENGTH, DEFAULT_M1_MASS, DEFAULT_L2_LENGTH, DEFAULT_M2_MASS, DEFAULT_TRAIL_DECAY, MAX_DAMPING, MAX_G, MAX_LENGTH, MAX_MASS, MAX_TRAIL_DECAY, MIN_DAMPING, MIN_G, MIN_LENGTH, MIN_MASS, MIN_TRAIL_DECAY, IMPULSE_M2, IMPULSE_M1} from './pendulum.model';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pendulum',
|
||||
imports: [
|
||||
BabylonCanvas,
|
||||
MatCard,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
FormsModule,
|
||||
NgxSliderModule,
|
||||
TranslatePipe,
|
||||
MatButton,
|
||||
Information,
|
||||
],
|
||||
templateUrl: './pendulum.component.html',
|
||||
styleUrl: './pendulum.component.scss',
|
||||
})
|
||||
class PendulumComponent {
|
||||
|
||||
// --- CONFIGURATION ---
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'PENDULUM.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: '',
|
||||
description: 'PENDULUM.EXPLANATION.EXPLANATION',
|
||||
link: UrlConstants.DOUBLE_PENDULUM_WIKI
|
||||
}
|
||||
],
|
||||
disclaimer: 'PENDULUM.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: 'PENDULUM.EXPLANATION.DISCLAIMER_BOTTOM',
|
||||
disclaimerListEntry: ['PENDULUM.EXPLANATION.DISCLAIMER_1', 'PENDULUM.EXPLANATION.DISCLAIMER_2', 'PENDULUM.EXPLANATION.DISCLAIMER_3', 'PENDULUM.EXPLANATION.DISCLAIMER_4']
|
||||
};
|
||||
|
||||
|
||||
renderConfig: RenderConfig = {
|
||||
mode: '2D',
|
||||
initialViewSize: 2,
|
||||
shaderLanguage: ShaderLanguage.WGSL,
|
||||
vertexShader: PENDULUM_VERTEX_SHADER_WGSL,
|
||||
fragmentShader: PENDULUM_FRAGMENT_SHADER_WGSL,
|
||||
uniformNames: [],
|
||||
uniformBufferNames: []
|
||||
};
|
||||
|
||||
trailDecayOptions: Options = {
|
||||
floor: MIN_TRAIL_DECAY,
|
||||
ceil: MAX_TRAIL_DECAY,
|
||||
logScale: false,
|
||||
step: 0.001,
|
||||
showTicks: false,
|
||||
hideLimitLabels: false,
|
||||
hidePointerLabels: false
|
||||
};
|
||||
|
||||
gravityOptions: Options = {
|
||||
floor: MIN_G,
|
||||
ceil: MAX_G,
|
||||
logScale: false,
|
||||
step: 0.01,
|
||||
showTicks: false,
|
||||
hideLimitLabels: false,
|
||||
hidePointerLabels: false
|
||||
};
|
||||
|
||||
dampingOptions: Options = {
|
||||
floor: MAX_DAMPING,
|
||||
ceil: MIN_DAMPING,
|
||||
logScale: false,
|
||||
step: 0.001,
|
||||
showTicks: false,
|
||||
hideLimitLabels: false,
|
||||
hidePointerLabels: false
|
||||
};
|
||||
|
||||
lengthOptions: Options = {
|
||||
floor: MIN_LENGTH,
|
||||
ceil: MAX_LENGTH,
|
||||
logScale: false,
|
||||
step: 0.1,
|
||||
showTicks: false,
|
||||
hideLimitLabels: false,
|
||||
hidePointerLabels: false
|
||||
};
|
||||
|
||||
massOptions: Options = {
|
||||
floor: MIN_MASS,
|
||||
ceil: MAX_MASS,
|
||||
logScale: false,
|
||||
step: 0.1,
|
||||
showTicks: false,
|
||||
hideLimitLabels: false,
|
||||
hidePointerLabels: false
|
||||
};
|
||||
|
||||
// Central management of physics parameters
|
||||
readonly simParams = {
|
||||
time: 0,
|
||||
dt: 0.015,
|
||||
g: DEFAULT_G,
|
||||
m1: DEFAULT_M1_MASS,
|
||||
m2: DEFAULT_M2_MASS,
|
||||
l1: DEFAULT_L1_LENGTH,
|
||||
l2: DEFAULT_L2_LENGTH,
|
||||
damping: DEFAULT_DAMPING,
|
||||
trailDecay: DEFAULT_TRAIL_DECAY,
|
||||
impulseM1: 0.0,
|
||||
impulseM2: 0.0,
|
||||
};
|
||||
|
||||
private currentSceneData: SceneEventData | null = null;
|
||||
|
||||
onSceneReady(event: SceneEventData) {
|
||||
this.currentSceneData = event;
|
||||
this.createSimulation();
|
||||
}
|
||||
|
||||
private createSimulation() {
|
||||
if (!this.currentSceneData){
|
||||
return;
|
||||
}
|
||||
const {engine, scene} = this.currentSceneData;
|
||||
engine.resize();
|
||||
|
||||
const width = engine.getRenderWidth();
|
||||
const height = engine.getRenderHeight();
|
||||
const totalPixels = width * height;
|
||||
|
||||
// --- 1. BUFFERS ---
|
||||
const pixelBuffer = new StorageBuffer(engine, totalPixels * 4);
|
||||
|
||||
const stateBuffer = new StorageBuffer(engine, 4 * 4);
|
||||
stateBuffer.update(new Float32Array([Math.PI / 4, Math.PI / 2, 0, 0])); // Initial angles
|
||||
|
||||
const paramsBuffer = new StorageBuffer(engine, 14 * 4);
|
||||
const paramsData = new Float32Array(14);
|
||||
|
||||
// --- 2. SHADERS ---
|
||||
const csPhysics = new ComputeShader("physics", engine,
|
||||
{computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL},
|
||||
{bindingsMapping: {"state": {group: 0, binding: 0}, "p": {group: 0, binding: 1}}}
|
||||
);
|
||||
csPhysics.setStorageBuffer("state", stateBuffer);
|
||||
csPhysics.setStorageBuffer("p", paramsBuffer);
|
||||
|
||||
const csRender = new ComputeShader("render", engine,
|
||||
{computeSource: PENDULUM_RENDER_COMPUTE_SHADER_WGSL},
|
||||
{bindingsMapping: {"pixelBuffer": {group: 0, binding: 0}, "p": {group: 0, binding: 1}, "state": {group: 0, binding: 2}}}
|
||||
);
|
||||
csRender.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||
csRender.setStorageBuffer("p", paramsBuffer);
|
||||
csRender.setStorageBuffer("state", stateBuffer);
|
||||
|
||||
// --- 3. MATERIAL ---
|
||||
const plane = scene.getMeshByName("plane");
|
||||
if (plane?.material) {
|
||||
const mat = plane.material as any;
|
||||
mat.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||
mat.setStorageBuffer("p", paramsBuffer);
|
||||
}
|
||||
|
||||
//remove old observables if available
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
// --- 4. RENDER LOOP ---
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
this.simParams.time += this.simParams.dt;
|
||||
|
||||
const currentWidth = engine.getRenderWidth();
|
||||
const currentHeight = engine.getRenderHeight();
|
||||
|
||||
// Fill parameter array (must match the exact order of the WGSL struct!)
|
||||
paramsData[0] = currentWidth;
|
||||
paramsData[1] = currentHeight;
|
||||
paramsData[2] = this.simParams.time;
|
||||
paramsData[3] = this.simParams.dt;
|
||||
paramsData[4] = this.simParams.g;
|
||||
paramsData[5] = this.simParams.m1;
|
||||
paramsData[6] = this.simParams.m2;
|
||||
paramsData[7] = this.simParams.l1;
|
||||
paramsData[8] = this.simParams.l2;
|
||||
paramsData[9] = this.simParams.damping;
|
||||
paramsData[10] = this.simParams.trailDecay;
|
||||
paramsData[11] = this.simParams.impulseM1;
|
||||
paramsData[12] = this.simParams.impulseM2;
|
||||
paramsData[13] = 0; // Pad
|
||||
|
||||
this.resetImpulses();
|
||||
|
||||
paramsBuffer.update(paramsData);
|
||||
|
||||
// Trigger simulation and rendering
|
||||
csPhysics.dispatch(1, 1, 1);
|
||||
|
||||
const dispatchCount = Math.ceil((currentWidth * currentHeight) / 64);
|
||||
csRender.dispatch(dispatchCount, 1, 1);
|
||||
});
|
||||
}
|
||||
|
||||
private resetImpulses() {
|
||||
if (this.simParams.impulseM1 !== 0.0) {
|
||||
this.simParams.impulseM1 = 0;
|
||||
}
|
||||
|
||||
if (this.simParams.impulseM2 !== 0.0) {
|
||||
this.simParams.impulseM2 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pushPendulum(m1: boolean) {
|
||||
if (m1)
|
||||
{
|
||||
this.simParams.impulseM1 = IMPULSE_M1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.simParams.impulseM2 = IMPULSE_M2;
|
||||
}
|
||||
|
||||
resetPendulum() {
|
||||
this.createSimulation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default PendulumComponent
|
||||
24
src/app/pages/algorithms/pendulum/pendulum.model.ts
Normal file
24
src/app/pages/algorithms/pendulum/pendulum.model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const DEFAULT_G = 9.81;
|
||||
export const MIN_G = 2;
|
||||
export const MAX_G = 15;
|
||||
|
||||
export const DEFAULT_DAMPING = 0.999;
|
||||
export const MIN_DAMPING = 1;
|
||||
export const MAX_DAMPING = 0.7;
|
||||
|
||||
export const DEFAULT_TRAIL_DECAY = 0.96;
|
||||
export const MIN_TRAIL_DECAY = 0.2;
|
||||
export const MAX_TRAIL_DECAY = 0.9999;
|
||||
|
||||
export const DEFAULT_L1_LENGTH = 1.5;
|
||||
export const DEFAULT_L2_LENGTH = 1.2;
|
||||
export const MIN_LENGTH = 0.2;
|
||||
export const MAX_LENGTH = 3;
|
||||
|
||||
export const DEFAULT_M1_MASS = 2;
|
||||
export const DEFAULT_M2_MASS = 1;
|
||||
export const MIN_MASS = 0.1;
|
||||
export const MAX_MASS = 5;
|
||||
|
||||
export const IMPULSE_M1 = 7;
|
||||
export const IMPULSE_M2 = 15;
|
||||
236
src/app/pages/algorithms/pendulum/pendulum.shader.ts
Normal file
236
src/app/pages/algorithms/pendulum/pendulum.shader.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
//Simple Pass-Through Shader
|
||||
export const PENDULUM_VERTEX_SHADER_WGSL = `
|
||||
attribute position : vec3<f32>;
|
||||
|
||||
@vertex
|
||||
fn main(input : VertexInputs) -> FragmentInputs {
|
||||
var output : FragmentInputs;
|
||||
output.position = vec4<f32>(input.position, 1.0);
|
||||
return output;
|
||||
}
|
||||
`;
|
||||
|
||||
// --- SHARED DATA STRUCTURES ---
|
||||
// These structs map exactly to the Float32Array in the TypeScript code.
|
||||
const SHARED_STRUCTS = `
|
||||
struct Params {
|
||||
width: f32,
|
||||
height: f32,
|
||||
time: f32,
|
||||
dt: f32,
|
||||
g: f32,
|
||||
m1: f32,
|
||||
m2: f32,
|
||||
l1: f32,
|
||||
l2: f32,
|
||||
damping: f32,
|
||||
trailDecay: f32,
|
||||
impulseM1: f32,
|
||||
impulseM2: f32,
|
||||
pad: f32 // <-- Padding for safe 16-byte memory alignment
|
||||
};
|
||||
|
||||
struct State {
|
||||
theta1: f32,
|
||||
theta2: f32,
|
||||
v1: f32,
|
||||
v2: f32
|
||||
};
|
||||
`;
|
||||
|
||||
//Fragment Shader to display the pixel buffer
|
||||
export const PENDULUM_FRAGMENT_SHADER_WGSL = SHARED_STRUCTS + `
|
||||
var<storage, read> pixelBuffer : array<f32>;
|
||||
var<storage, read> p : Params;
|
||||
|
||||
@fragment
|
||||
fn main(input : FragmentInputs) -> FragmentOutputs {
|
||||
let width = u32(p.width);
|
||||
let height = u32(p.height);
|
||||
|
||||
if (width == 0u || height == 0u) {
|
||||
fragmentOutputs.color = vec4<f32>(0.5, 0.0, 0.0, 1.0);
|
||||
return fragmentOutputs;
|
||||
}
|
||||
|
||||
let x = u32(input.position.x);
|
||||
let y = u32(input.position.y);
|
||||
|
||||
if (x >= width || y >= height) {
|
||||
fragmentOutputs.color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||
return fragmentOutputs;
|
||||
}
|
||||
|
||||
let index = y * width + x;
|
||||
|
||||
// --- THE MAGIC DECODING ---
|
||||
var val = pixelBuffer[index];
|
||||
var isLine1 = false;
|
||||
var isLine2 = false;
|
||||
|
||||
// 1. Check for overlays (Lines)
|
||||
if (val >= 20.0) {
|
||||
isLine2 = true;
|
||||
val = val - 20.0;
|
||||
} else if (val >= 10.0) {
|
||||
isLine1 = true;
|
||||
val = val - 10.0;
|
||||
}
|
||||
|
||||
// 2. Check which trail it is
|
||||
var isTrail2 = false;
|
||||
if (val >= 2.0) {
|
||||
isTrail2 = true;
|
||||
val = val - 2.0;
|
||||
}
|
||||
|
||||
// 3. What remains is purely the fading intensity (0.0 to 1.0)
|
||||
let trailIntensity = val;
|
||||
|
||||
// --- COLORS ---
|
||||
let bgColor = vec3<f32>(0.1, 0.1, 0.15);
|
||||
let mass1Color = vec3<f32>(1.0, 0.0, 0.0); // Red
|
||||
let mass2Color = vec3<f32>(0.0, 1.0, 0.0); // Green
|
||||
let line1Color = vec3<f32>(1.0, 1.0, 0.0); // Yellow
|
||||
let line2Color = vec3<f32>(1.0, 0.0, 1.0); // Magenta
|
||||
|
||||
var massColor = mass1Color;
|
||||
if (isTrail2) {
|
||||
massColor = mass2Color;
|
||||
}
|
||||
|
||||
// Calculate background blending with the trail
|
||||
var finalColor = mix(bgColor, massColor, clamp(trailIntensity, 0.0, 1.0));
|
||||
|
||||
// Overwrite with the line colors if necessary
|
||||
if (isLine1) { finalColor = line1Color; }
|
||||
if (isLine2) { finalColor = line2Color; }
|
||||
|
||||
fragmentOutputs.color = vec4<f32>(finalColor, 1.0);
|
||||
return fragmentOutputs;
|
||||
}
|
||||
`;
|
||||
|
||||
//Math for the double pendulum
|
||||
//https://en.wikipedia.org/wiki/Double_pendulum
|
||||
export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + `
|
||||
@group(0) @binding(0) var<storage, read_write> state : State;
|
||||
@group(0) @binding(1) var<storage, read> p : Params;
|
||||
|
||||
@compute @workgroup_size(1)
|
||||
fn main() {
|
||||
let t1 = state.theta1;
|
||||
let t2 = state.theta2;
|
||||
let v1 = state.v1;
|
||||
let v2 = state.v2;
|
||||
|
||||
let delta_t = t1 - t2;
|
||||
|
||||
let num1 = -p.g * (2.0 * p.m1 + p.m2) * sin(t1)
|
||||
- p.m2 * p.g * sin(t1 - 2.0 * t2)
|
||||
- 2.0 * sin(delta_t) * p.m2 * (v2 * v2 * p.l2 + v1 * v1 * p.l1 * cos(delta_t));
|
||||
let den1 = p.l1 * (2.0 * p.m1 + p.m2 - p.m2 * cos(2.0 * delta_t));
|
||||
let a1 = num1 / den1;
|
||||
|
||||
let num2 = 2.0 * sin(delta_t) * (v1 * v1 * p.l1 * (p.m1 + p.m2) + p.g * (p.m1 + p.m2) * cos(t1) + v2 * v2 * p.l2 * p.m2 * cos(delta_t));
|
||||
let den2 = p.l2 * (2.0 * p.m1 + p.m2 - p.m2 * cos(2.0 * delta_t));
|
||||
let a2 = num2 / den2;
|
||||
|
||||
let new_v1 = (v1 + a1 * p.dt) * p.damping + p.impulseM1;
|
||||
let new_v2 = (v2 + a2 * p.dt) * p.damping + p.impulseM2;
|
||||
|
||||
state.v1 = new_v1;
|
||||
state.v2 = new_v2;
|
||||
state.theta1 = t1 + new_v1 * p.dt;
|
||||
state.theta2 = t2 + new_v2 * p.dt;
|
||||
}
|
||||
`;
|
||||
|
||||
//Pixel data to visualize the pendulum
|
||||
export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + `
|
||||
@group(0) @binding(0) var<storage, read_write> pixelBuffer : array<f32>;
|
||||
@group(0) @binding(1) var<storage, read> p : Params;
|
||||
@group(0) @binding(2) var<storage, read> state : State;
|
||||
|
||||
fn sdSegment(point: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
|
||||
let pa = point - a;
|
||||
let ba = b - a;
|
||||
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
return length(pa - ba * h);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||
let index = global_id.x;
|
||||
let width = u32(p.width);
|
||||
let height = u32(p.height);
|
||||
|
||||
if (index >= width * height) { return; }
|
||||
|
||||
let x = f32(index % width);
|
||||
let y = f32(index / width);
|
||||
let uv = vec2<f32>(x / p.width, y / p.height);
|
||||
|
||||
let aspect = p.width / p.height;
|
||||
let uv_corr = vec2<f32>(uv.x * aspect, uv.y);
|
||||
|
||||
// --- 1. EXTRACT & DECAY OLD MEMORY ---
|
||||
var memory = pixelBuffer[index];
|
||||
|
||||
// Strip line overlays from the previous frame
|
||||
if (memory >= 20.0) { memory = memory - 20.0; }
|
||||
else if (memory >= 10.0) { memory = memory - 10.0; }
|
||||
|
||||
// Check if the memory belongs to Trail 2
|
||||
var isTrail2 = false;
|
||||
if (memory >= 2.0) {
|
||||
isTrail2 = true;
|
||||
memory = memory - 2.0;
|
||||
}
|
||||
|
||||
// Apply decay to the pure intensity
|
||||
memory = memory * p.trailDecay;
|
||||
|
||||
// --- 2. CALCULATE GEOMETRY ---
|
||||
let origin = vec2<f32>(0.5 * aspect, 0.3);
|
||||
let displayScale = 0.15;
|
||||
|
||||
let p1 = origin + vec2<f32>(sin(state.theta1), cos(state.theta1)) * p.l1 * displayScale;
|
||||
let p2 = p1 + vec2<f32>(sin(state.theta2), cos(state.theta2)) * p.l2 * displayScale;
|
||||
|
||||
let dLine1 = sdSegment(uv_corr, origin, p1);
|
||||
let dLine2 = sdSegment(uv_corr, p1, p2);
|
||||
let dMass1 = length(uv_corr - p1);
|
||||
let dMass2 = length(uv_corr - p2);
|
||||
|
||||
// --- 3. SMART LAYERING ---
|
||||
var baseVal = 0.0;
|
||||
|
||||
// Base Layer (Masses & Trails)
|
||||
if (dMass1 < 0.02) {
|
||||
baseVal = 1.0; // Mass 1 = 1.0 (Trail 1 Max)
|
||||
} else if (dMass2 < 0.02) {
|
||||
baseVal = 3.0; // Mass 2 = 2.0 (Flag) + 1.0 (Trail 2 Max)
|
||||
} else {
|
||||
// Write fading memory back
|
||||
if (isTrail2) {
|
||||
baseVal = memory + 2.0;
|
||||
} else {
|
||||
baseVal = memory;
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay Layer (Lines)
|
||||
var overlay = 0.0;
|
||||
// Don't draw lines over the masses (Clean Z-Index)
|
||||
if (dMass1 < 0.02 || dMass2 < 0.02) {
|
||||
overlay = 0.0;
|
||||
} else if (dLine1 < 0.003) {
|
||||
overlay = 10.0;
|
||||
} else if (dLine2 < 0.003) {
|
||||
overlay = 20.0;
|
||||
}
|
||||
|
||||
pixelBuffer[index] = baseVal + overlay;
|
||||
}
|
||||
`;
|
||||
@@ -44,6 +44,12 @@ export class AlgorithmsService {
|
||||
title: 'ALGORITHM.FRACTAL3D.TITLE',
|
||||
description: 'ALGORITHM.FRACTAL3D.DESCRIPTION',
|
||||
routerLink: RouterConstants.FRACTAL3d.LINK
|
||||
},
|
||||
{
|
||||
id: 'pendulum',
|
||||
title: 'ALGORITHM.PENDULUM.TITLE',
|
||||
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
||||
routerLink: RouterConstants.PENDULUM.LINK
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
.canvas-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #1a1a1a;
|
||||
width: 100%;
|
||||
|
||||
max-width: 1000px;
|
||||
max-height: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
canvas {
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
@@ -18,8 +21,7 @@ canvas {
|
||||
max-height: 1000px;
|
||||
|
||||
touch-action: none;
|
||||
display: block;
|
||||
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
|
||||
import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core';
|
||||
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
||||
|
||||
export interface RenderConfig {
|
||||
mode: '2D' | '3D';
|
||||
shaderLanguage?: number; //0 GLSL, 1 WGSL
|
||||
initialViewSize: number;
|
||||
vertexShader: string;
|
||||
fragmentShader: string;
|
||||
uniformNames: string[];
|
||||
uniformBufferNames?: string[];
|
||||
}
|
||||
|
||||
export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void;
|
||||
|
||||
export interface SceneEventData {
|
||||
scene: Scene;
|
||||
engine: WebGPUEngine;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-babylon-canvas',
|
||||
imports: [],
|
||||
@@ -25,21 +32,20 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
@Input({ required: true }) config!: RenderConfig;
|
||||
@Input() renderCallback?: RenderCallback;
|
||||
|
||||
@Output() sceneReady = new EventEmitter<Scene>();
|
||||
@Output() sceneReady = new EventEmitter<SceneEventData>();
|
||||
@Output() sceneResized = new EventEmitter<SceneEventData>();
|
||||
|
||||
private engine!: Engine;
|
||||
private engine!: WebGPUEngine;
|
||||
private scene!: Scene;
|
||||
private shaderMaterial!: ShaderMaterial;
|
||||
private camera!: Camera;
|
||||
|
||||
//Listener
|
||||
private resizeHandler = () => this.handleResize();
|
||||
private wheelHandler = (evt: WheelEvent) => evt.preventDefault();
|
||||
private readonly resizeHandler = () => this.handleResize();
|
||||
private readonly wheelHandler = (evt: WheelEvent) => evt.preventDefault();
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
this.initBabylon();
|
||||
});
|
||||
this.initBabylon().then(() => { console.log("Engine initialized"); });
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -55,16 +61,21 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private initBabylon(): void {
|
||||
private async initBabylon(): Promise<void> {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
this.engine = new Engine(canvas, true);
|
||||
this.scene = new Scene(this.engine);
|
||||
this.setupCamera(canvas);
|
||||
this.addListener(canvas);
|
||||
this.createShaderMaterial();
|
||||
this.createFullScreenRect();
|
||||
this.sceneReady.emit(this.scene);
|
||||
this.addRenderLoop(canvas);
|
||||
this.engine = new WebGPUEngine(canvas);
|
||||
await this.engine.initAsync().then(() => {
|
||||
this.scene = new Scene(this.engine);
|
||||
this.setupCamera(canvas);
|
||||
this.addListener(canvas);
|
||||
this.createShaderMaterial();
|
||||
this.createFullScreenRect();
|
||||
this.sceneReady.emit({
|
||||
scene: this.scene,
|
||||
engine: this.engine
|
||||
});
|
||||
this.addRenderLoop(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
private addListener(canvas: HTMLCanvasElement) {
|
||||
@@ -109,7 +120,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
private createFullScreenRect() {
|
||||
const plane = MeshBuilder.CreatePlane("plane", {size: 110}, this.scene);
|
||||
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
|
||||
|
||||
if (this.config.mode === '3D') {
|
||||
plane.parent = this.camera;
|
||||
@@ -132,7 +143,9 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
},
|
||||
{
|
||||
attributes: ["position", "uv"],
|
||||
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames]
|
||||
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames],
|
||||
uniformBuffers: this.config.uniformBufferNames ?? [],
|
||||
shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL
|
||||
}
|
||||
);
|
||||
this.shaderMaterial.disableDepthWrite = true;
|
||||
@@ -167,5 +180,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
this.camera.orthoTop = viewSize / 2;
|
||||
this.camera.orthoBottom = -viewSize / 2;
|
||||
}
|
||||
|
||||
this.sceneResized?.emit({
|
||||
scene: this.scene,
|
||||
engine: this.engine
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"APP": {
|
||||
"TITLE": "Playground",
|
||||
"COPYRIGHT": "Bilder urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!"
|
||||
"COPYRIGHT": "Bilder und Sourcecode sind urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!"
|
||||
},
|
||||
"TOPBAR": {
|
||||
"ABOUT": "Über mich",
|
||||
@@ -415,6 +415,29 @@
|
||||
"DISCLAIMER_4": "Licht & Schatten: Um die Tiefe sichtbar zu machen, werden Lichtreflexionen und Schatten (Ambient Occlusion) basierend auf der Krümmung der Formel simuliert."
|
||||
}
|
||||
},
|
||||
"PENDULUM": {
|
||||
"TITLE": "Doppel-Pendel",
|
||||
"TRAIL_DECAY_TIME": "Spurlänge",
|
||||
"DAMPING": "Dämpfung",
|
||||
"ATTRACTION": "Anziehungskraft",
|
||||
"L1_LENGTH": "Länge L1",
|
||||
"L2_LENGTH": "Länge L2",
|
||||
"M1_MASS": "Masse M1",
|
||||
"M2_MASS": "Masse M2",
|
||||
"POKE_M1": "Schubse M1",
|
||||
"POKE_M2": "Schubse M2",
|
||||
"RESET": "Neustarten",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Chaostheorie: Das Doppelpendel",
|
||||
"EXPLANATION": "Das Doppelpendel ist eines der bekanntesten und faszinierendsten Beispiele der Physik für ein dynamisches System, das 'deterministisches Chaos' erzeugt. Es besteht schlicht aus einem einfachen Pendel, an dessen unterem Ende ein zweites Pendel befestigt ist. Obwohl die zugrundeliegenden Bewegungsgesetze der klassischen Mechanik streng mathematisch definiert sind, ist das Verhalten des Doppelpendels auf lange Sicht absolut unvorhersehbar. Es gilt in der Physik als das klassische Vorzeigeobjekt für den sogenannten Schmetterlingseffekt.",
|
||||
"DISCLAIMER": "Diese WebGPU-Simulation berechnet die Bewegungs- und Beschleunigungsgleichungen des Pendels 60-mal pro Sekunde in Echtzeit. Dabei gelten folgende Besonderheiten:",
|
||||
"DISCLAIMER_1": "Extreme Sensitivität: Winzigste Änderungen in den Startbedingungen (z.B. ein Tausendstel Grad Abweichung im Startwinkel oder bei der Masse) führen schon nach kurzer Zeit zu einer völlig anderen, chaotischen Flugbahn.",
|
||||
"DISCLAIMER_2": "Deterministisches Chaos: Die Bewegung wirkt zwar völlig wild und zufällig, ist es aber nicht. Startest du die Simulation mit exakt denselben Werten neu, wird das Pendel zu 100 % denselben Weg fliegen.",
|
||||
"DISCLAIMER_3": "Numerische Integration: Da Computer Zeit nicht stufenlos, sondern in winzigen Schritten (dt) berechnen, entstehen bei jedem Frame winzige mathematische Rundungsfehler. Diese summieren sich auf und beeinflussen das Chaos zusätzlich.",
|
||||
"DISCLAIMER_4": "Energieerhaltung & Reibung: In einem perfekten physikalischen System ohne Widerstand würde das Pendel ewig weiterschwingen. Für eine natürliche Optik nutzt der Algorithmus einen künstlichen Dämpfungsfaktor, der Luftreibung simuliert und das System irgendwann beruhigt.",
|
||||
"DISCLAIMER_BOTTOM": "HINWEIS: Wenn zuviele Impulse in das System gegeben werden, wird die Simulation instabil. Dann hängt das Pendel nur noch runter und es muss neu gestartet werden."
|
||||
}
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithmen",
|
||||
"PATHFINDING": {
|
||||
@@ -441,6 +464,10 @@
|
||||
"TITLE": "Fraktale 3D",
|
||||
"DESCRIPTION": "3D-Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)."
|
||||
},
|
||||
"PENDULUM": {
|
||||
"TITLE": "Doppel-Pendel",
|
||||
"DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU."
|
||||
},
|
||||
"NOTE": "HINWEIS",
|
||||
"GRID_HEIGHT": "Höhe",
|
||||
"GRID_WIDTH": "Beite"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"APP": {
|
||||
"TITLE": "Playground",
|
||||
"COPYRIGHT": "Images protected by copyright, no use without permission!"
|
||||
"COPYRIGHT": "Images and code protected by copyright, no use without permission!"
|
||||
},
|
||||
"TOPBAR": {
|
||||
"ABOUT": "About me",
|
||||
@@ -414,6 +414,29 @@
|
||||
"DISCLAIMER_4": "Light & Shadow: To visualize depth, light reflections and shadows (Ambient Occlusion) are simulated based on the curvature of the formula."
|
||||
}
|
||||
},
|
||||
"PENDULUM": {
|
||||
"TITLE": "Double pendulum",
|
||||
"TRAIL_DECAY_TIME": "Trail length",
|
||||
"DAMPING": "Damping",
|
||||
"ATTRACTION": "Attraction",
|
||||
"L1_LENGTH": "Length L1",
|
||||
"L2_LENGTH": "Length L2",
|
||||
"M1_MASS": "Mass M1",
|
||||
"M2_MASS": "Mass M2",
|
||||
"POKE_M1": "Poke M1",
|
||||
"POKE_M2": "Poke M2",
|
||||
"RESET": "Reset",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Chaos Theory: The Double Pendulum",
|
||||
"EXPLANATION": "The double pendulum is one of physics' most famous and fascinating examples of a dynamic system that generates 'deterministic chaos'. It simply consists of a standard pendulum with a second pendulum attached to its lower end. Although the underlying laws of classical mechanics are strictly mathematically defined, the long-term behavior of the double pendulum is absolutely unpredictable. In physics, it is considered the classic showcase object for the so-called butterfly effect.",
|
||||
"DISCLAIMER": "This WebGPU simulation calculates the motion and acceleration equations of the pendulum 60 times per second in real-time. The following characteristics apply:",
|
||||
"DISCLAIMER_1": "Extreme Sensitivity: The tiniest changes in the initial conditions (e.g., a thousandth of a degree deviation in the starting angle or mass) lead to a completely different, chaotic trajectory after just a short time.",
|
||||
"DISCLAIMER_2": "Deterministic Chaos: The movement may look completely wild and random, but it isn't. If you restart the simulation with the exact same values, the pendulum will follow 100% the same path.",
|
||||
"DISCLAIMER_3": "Numerical Integration: Since computers do not calculate time continuously but in tiny steps (dt), minute mathematical rounding errors occur in every frame. These add up over time and further influence the chaos.",
|
||||
"DISCLAIMER_4": "Energy Conservation & Friction: In a perfect physical system without resistance, the pendulum would swing forever. For a natural look, the algorithm uses an artificial damping factor that simulates air friction and eventually brings the system to a halt.",
|
||||
"DISCLAIMER_BOTTOM": "NOTE: If too many impulses are fed into the system, the simulation becomes unstable. The pendulum will then just hang down and the simulation will have to be restarted."
|
||||
}
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithms",
|
||||
"PATHFINDING": {
|
||||
@@ -440,6 +463,10 @@
|
||||
"TITLE": "Fractals 3D",
|
||||
"DESCRIPTION": "3D Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)."
|
||||
},
|
||||
"PENDULUM": {
|
||||
"TITLE": "Double pendulum",
|
||||
"DESCRIPTION": "Visualisation of a chaotic double pendulum simulation with WebGPU."
|
||||
},
|
||||
"NOTE": "Note",
|
||||
"GRID_HEIGHT": "Height",
|
||||
"GRID_WIDTH": "Width"
|
||||
|
||||
@@ -262,13 +262,13 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.grid-size {
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.grid-field {
|
||||
.input-field {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
@@ -301,6 +301,10 @@ canvas {
|
||||
&.path { background-color: gold; }
|
||||
&.empty { background-color: lightgray; }
|
||||
&.alive { background-color: black; }
|
||||
&.L1 { background-color: yellow; }
|
||||
&.L2 { background-color: magenta; }
|
||||
&.M1 { background-color: red; }
|
||||
&.M2 { background-color: green; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +314,12 @@ canvas {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.slider-control-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Sorting Visualization */
|
||||
.sorting-visualization-area {
|
||||
display: flex;
|
||||
@@ -339,4 +349,4 @@ canvas {
|
||||
background-color: #4caf50; /* Green for sorted */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user