Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -101,10 +101,10 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(entry.key !== xpKeys.at(xpKeys.length-1)?.key)
|
||||
{
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
@if(entry.key !== xpKeys.at(xpKeys.length-1)?.key)
|
||||
{
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {SharedFunctions} from '../../shared/SharedFunctions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgOptimizedImage,
|
||||
MatCardModule,
|
||||
|
||||
@@ -3,4 +3,5 @@ export interface AlgorithmCategory {
|
||||
title: string;
|
||||
description: string;
|
||||
routerLink: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<div class="card-grid">
|
||||
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
|
||||
<h1 class="algo-page-title">{{ 'ALGORITHM.TITLE' | translate }}</h1>
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
@for (category of categories$ | async; track category.id) {
|
||||
@for (category of categories; track category.id) {
|
||||
<mat-card class="algo-card" [routerLink]="[category.routerLink]">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ category.title | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>{{ category.description | translate}}</p>
|
||||
<div class="algo-icon-wrap">
|
||||
<mat-icon>{{ category.icon }}</mat-icon>
|
||||
</div>
|
||||
<h3 class="algo-card-title">{{ category.title | translate }}</h3>
|
||||
<p class="algo-card-desc">{{ category.description | translate }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AlgorithmsService } from './algorithms.service';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-algorithms',
|
||||
templateUrl: './algorithms.component.html',
|
||||
styleUrls: ['./algorithms.component.scss'],
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, MatCardModule, TranslatePipe],
|
||||
styleUrl: './algorithms.component.scss',
|
||||
imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe],
|
||||
})
|
||||
export class AlgorithmsComponent implements OnInit {
|
||||
export class AlgorithmsComponent {
|
||||
private readonly algorithmsService = inject(AlgorithmsService);
|
||||
|
||||
categories$: Observable<AlgorithmCategory[]> | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.categories$ = this.algorithmsService.getCategories();
|
||||
}
|
||||
readonly categories: AlgorithmCategory[] = this.algorithmsService.getCategories();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {RouterConstants} from '../../constants/RouterConstants';
|
||||
|
||||
@Injectable({
|
||||
@@ -13,53 +12,68 @@ export class AlgorithmsService {
|
||||
id: 'pathfinding',
|
||||
title: 'ALGORITHM.PATHFINDING.TITLE',
|
||||
description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
|
||||
routerLink: RouterConstants.PATHFINDING.LINK
|
||||
routerLink: RouterConstants.PATHFINDING.LINK,
|
||||
icon: 'route'
|
||||
},
|
||||
{
|
||||
id: 'sorting',
|
||||
title: 'ALGORITHM.SORTING.TITLE',
|
||||
description: 'ALGORITHM.SORTING.DESCRIPTION',
|
||||
routerLink: RouterConstants.SORTING.LINK
|
||||
routerLink: RouterConstants.SORTING.LINK,
|
||||
icon: 'sort'
|
||||
},
|
||||
{
|
||||
id: 'gameOfLife',
|
||||
title: 'ALGORITHM.GOL.TITLE',
|
||||
description: 'ALGORITHM.GOL.DESCRIPTION',
|
||||
routerLink: RouterConstants.GOL.LINK
|
||||
routerLink: RouterConstants.GOL.LINK,
|
||||
icon: 'grid_on'
|
||||
},
|
||||
{
|
||||
id: 'labyrinth',
|
||||
title: 'ALGORITHM.LABYRINTH.TITLE',
|
||||
description: 'ALGORITHM.LABYRINTH.DESCRIPTION',
|
||||
routerLink: RouterConstants.LABYRINTH.LINK
|
||||
routerLink: RouterConstants.LABYRINTH.LINK,
|
||||
icon: 'grid_view'
|
||||
},
|
||||
{
|
||||
id: 'fractal',
|
||||
title: 'ALGORITHM.FRACTAL.TITLE',
|
||||
description: 'ALGORITHM.FRACTAL.DESCRIPTION',
|
||||
routerLink: RouterConstants.FRACTAL.LINK
|
||||
routerLink: RouterConstants.FRACTAL.LINK,
|
||||
icon: 'blur_on'
|
||||
},
|
||||
{
|
||||
id: 'fractal3d',
|
||||
title: 'ALGORITHM.FRACTAL3D.TITLE',
|
||||
description: 'ALGORITHM.FRACTAL3D.DESCRIPTION',
|
||||
routerLink: RouterConstants.FRACTAL3d.LINK
|
||||
routerLink: RouterConstants.FRACTAL3d.LINK,
|
||||
icon: 'view_in_ar'
|
||||
},
|
||||
{
|
||||
id: 'pendulum',
|
||||
title: 'ALGORITHM.PENDULUM.TITLE',
|
||||
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
||||
routerLink: RouterConstants.PENDULUM.LINK
|
||||
routerLink: RouterConstants.PENDULUM.LINK,
|
||||
icon: 'rotate_right'
|
||||
},
|
||||
{
|
||||
id: 'cloth',
|
||||
title: 'ALGORITHM.CLOTH.TITLE',
|
||||
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
||||
routerLink: RouterConstants.CLOTH.LINK
|
||||
routerLink: RouterConstants.CLOTH.LINK,
|
||||
icon: 'texture'
|
||||
},
|
||||
{
|
||||
id: 'fourColor',
|
||||
title: 'ALGORITHM.FOUR_COLOR.TITLE',
|
||||
description: 'ALGORITHM.FOUR_COLOR.DESCRIPTION',
|
||||
routerLink: RouterConstants.FOUR_COLOR.LINK,
|
||||
icon: 'palette'
|
||||
}
|
||||
];
|
||||
|
||||
getCategories(): Observable<AlgorithmCategory[]> {
|
||||
return of(this.categories);
|
||||
getCategories(): AlgorithmCategory[] {
|
||||
return this.categories;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
<button mat-raised-button color="primary" (click)="toggleMesh()">
|
||||
{{ isOutlineActive ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }}
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="restartSimulation()">
|
||||
{{ 'CLOTH.RESTART_SIMULATION' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="sliders-panel">
|
||||
<span>{{ 'CLOTH.ELONGATION' | translate }}: {{ elongation }}</span>
|
||||
<mat-slider min="0.5" max="2.0" step="0.1">
|
||||
<input matSliderThumb [(ngModel)]="elongation">
|
||||
</mat-slider>
|
||||
</div>
|
||||
</div>
|
||||
<app-babylon-canvas
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { BabylonCanvas, RenderConfig, SceneEventData } from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh, WebGPUEngine, Scene} from '@babylonjs/core';
|
||||
@@ -31,6 +33,8 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
TranslatePipe,
|
||||
BabylonCanvas,
|
||||
MatButton,
|
||||
MatSliderModule,
|
||||
FormsModule,
|
||||
Information
|
||||
],
|
||||
templateUrl: './cloth.component.html',
|
||||
@@ -42,6 +46,9 @@ export class ClothComponent {
|
||||
private clothMesh: GroundMesh | null = null;
|
||||
public isWindActive: boolean = false;
|
||||
public isOutlineActive: boolean = false;
|
||||
public stiffness: number = 80;
|
||||
// Elongation along the vertical (Y) axis, 0.5 = compressed, 2.0 = stretched
|
||||
public elongation: number = 1.0;
|
||||
|
||||
public renderConfig: RenderConfig = {
|
||||
mode: '3D',
|
||||
@@ -103,6 +110,11 @@ export class ClothComponent {
|
||||
this.clothMesh.material.wireframe = this.isOutlineActive;
|
||||
}
|
||||
|
||||
public restartSimulation(): void {
|
||||
this.simulationTime = 0;
|
||||
this.createSimulation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and starts the cloth simulation.
|
||||
*/
|
||||
@@ -164,9 +176,13 @@ export class ClothComponent {
|
||||
const constraintsP2: number[] = [];
|
||||
const constraintsP3: number[] = [];
|
||||
|
||||
const addConstraint = (arr: number[], a: number, b: number): void => {
|
||||
// Type 1.0 = horizontal/diagonal (no elongation), Type 2.0 = vertical (elongation applies)
|
||||
const addHorizontalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 1.0);
|
||||
};
|
||||
const addVerticalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 2.0);
|
||||
};
|
||||
|
||||
// Fill positions (Pin top row)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
@@ -186,14 +202,14 @@ export class ClothComponent {
|
||||
|
||||
// Graph Coloring (4 Phases)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) addHorizontalConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) addHorizontalConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
}
|
||||
for (let y = 0; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
for (let x = 0; x < config.gridWidth; x++) addVerticalConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
for (let y = 1; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
for (let x = 0; x < config.gridWidth; x++) addVerticalConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
|
||||
const constraintsP4: number[] = [];
|
||||
@@ -228,7 +244,7 @@ export class ClothComponent {
|
||||
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||
],
|
||||
params: new Float32Array(8)
|
||||
params: new Float32Array(9)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -331,7 +347,7 @@ export class ClothComponent {
|
||||
// 6. RENDER LOOP
|
||||
// ========================================================================
|
||||
private startRenderLoop(engine: WebGPUEngine, scene: Scene, config: ClothConfig, buffers: ClothBuffers, pipelines: ClothPipelines): void {
|
||||
const paramsData = new Float32Array(8);
|
||||
const paramsData = new Float32Array(9);
|
||||
|
||||
// Pre-calculate constraint dispatch sizes for the 4 phases
|
||||
const constraintsLength = buffers.constraints.map(b => (b as any)._buffer.capacity / 4 / 4); // Elements / vec4 length
|
||||
@@ -347,16 +363,23 @@ export class ClothComponent {
|
||||
const windX = this.isWindActive ? 5.0 : 0.0;
|
||||
const windY = 0.0;
|
||||
const windZ = this.isWindActive ? 15.0 : 0.0;
|
||||
const scaledCompliance = 0.00001 * config.particleInvMass * config.spacing;
|
||||
|
||||
// Logarithmic compliance: stiffness=1 → very soft fabric, stiffness=100 → rigid metal sheet.
|
||||
// alpha = compliance / dt² must be >> wSum (≈800) to be soft, << wSum to be rigid.
|
||||
const softCompliance = 10.0;
|
||||
const rigidCompliance = 0.00001;
|
||||
const t = (this.stiffness - 1) / 99.0;
|
||||
const compliance = softCompliance * Math.pow(rigidCompliance / softCompliance, t);
|
||||
|
||||
paramsData[0] = 0.016; // dt
|
||||
paramsData[1] = -9.81; // gravity
|
||||
paramsData[2] = scaledCompliance;
|
||||
paramsData[2] = compliance;
|
||||
paramsData[3] = config.numVertices;
|
||||
paramsData[4] = windX;
|
||||
paramsData[5] = windY;
|
||||
paramsData[6] = windZ;
|
||||
paramsData[7] = this.simulationTime;
|
||||
paramsData[8] = this.elongation;
|
||||
|
||||
buffers.params.update(paramsData);
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ export const CLOTH_SHARED_STRUCTS = `
|
||||
wind_x: f32,
|
||||
wind_y: f32,
|
||||
wind_z: f32,
|
||||
time: f32
|
||||
time: f32,
|
||||
elongation: f32
|
||||
};
|
||||
`;
|
||||
|
||||
@@ -26,9 +27,8 @@ export const CLOTH_VERTEX_SHADER_WGSL = `
|
||||
|
||||
uniform viewProjection : mat4x4<f32>;
|
||||
|
||||
// Varyings, um Daten an den Fragment-Shader zu senden
|
||||
varying vUV : vec2<f32>;
|
||||
varying vWorldPos : vec3<f32>; // NEU: Wir brauchen die 3D-Position für das Licht!
|
||||
varying vWorldPos : vec3<f32>;
|
||||
|
||||
@vertex
|
||||
fn main(input : VertexInputs) -> FragmentInputs {
|
||||
@@ -38,7 +38,7 @@ export const CLOTH_VERTEX_SHADER_WGSL = `
|
||||
output.position = uniforms.viewProjection * vec4<f32>(worldPos, 1.0);
|
||||
|
||||
output.vUV = input.uv;
|
||||
output.vWorldPos = worldPos; // Position weitergeben
|
||||
output.vWorldPos = worldPos;
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -133,13 +133,15 @@ export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
||||
if (idx >= arrayLength(&constraints)) { return; }
|
||||
|
||||
let constraint = constraints[idx];
|
||||
let isActive = constraint.w;
|
||||
|
||||
if (isActive < 0.5) { return; }
|
||||
// constraint.w: 0.0 = inactive, 1.0 = horizontal/diagonal, 2.0 = vertical
|
||||
if (constraint.w < 0.5) { return; }
|
||||
|
||||
let idA = u32(constraint.x);
|
||||
let idB = u32(constraint.y);
|
||||
let restLength = constraint.z;
|
||||
|
||||
// constraint.w encodes type: 1.0 = horizontal/diagonal, 2.0 = vertical (elongation applies)
|
||||
let restLength =constraint.z * p.elongation;
|
||||
|
||||
var pA = positions[idA];
|
||||
var pB = positions[idB];
|
||||
|
||||
@@ -178,10 +178,9 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
|
||||
async startGame(): Promise<void> {
|
||||
this.gameStarted.set(true);
|
||||
let lifeIsDead = false;
|
||||
while (this.gameStarted()){
|
||||
const startTime = performance.now();
|
||||
lifeIsDead = true;
|
||||
let lifeIsDead = true;
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
lifeIsDead = this.checkLifeRules(row, col, this.writeGrid) && lifeIsDead;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<mat-card class="algo-container">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'FOUR_COLOR.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
|
||||
<div class="controls-container">
|
||||
<div class="controls-panel">
|
||||
<button mat-flat-button color="primary" (click)="generateNewMap()">{{ 'FOUR_COLOR.GENERATE' | translate }}</button>
|
||||
<button mat-flat-button color="accent" (click)="autoSolve()">{{ 'FOUR_COLOR.SOLVE' | translate }}</button>
|
||||
<button mat-stroked-button (click)="resetColors()">{{ 'FOUR_COLOR.CLEAR' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-panel">
|
||||
<div class="input-container">
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridRows"
|
||||
(ngModelChange)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="input-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridCols"
|
||||
(ngModelChange)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><span class="legend-color color1"></span> {{ 'FOUR_COLOR.COLOR_1' | translate }}</span>
|
||||
<span><span class="legend-color color2"></span> {{ 'FOUR_COLOR.COLOR_2' | translate }}</span>
|
||||
<span><span class="legend-color color3"></span> {{ 'FOUR_COLOR.COLOR_3' | translate }}</span>
|
||||
<span><span class="legend-color color4"></span> {{ 'FOUR_COLOR.COLOR_4' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="status-panel" [ngClass]="solutionStatus.toLowerCase()">
|
||||
<span class="status-label">{{ 'FOUR_COLOR.STATUS.LABEL' | translate }}:</span>
|
||||
<span class="status-message">{{ 'FOUR_COLOR.STATUS.' + solutionStatus | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-container">
|
||||
<canvas #fourColorCanvas
|
||||
(mousedown)="onMouseDown($event)"
|
||||
(mousemove)="onMouseMove($event)"
|
||||
(touchstart)="onTouchStart($event)"
|
||||
(touchmove)="onTouchMove($event)"
|
||||
></canvas>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
.status-panel {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--app-fg);
|
||||
border-left: 5px solid #9e9e9e;
|
||||
font-weight: 500;
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-panel.incomplete {
|
||||
border-left-color: #9e9e9e;
|
||||
background-color: var(--app-bg);
|
||||
}
|
||||
|
||||
.status-panel.solved {
|
||||
border-left-color: #4CAF50;
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-panel.conflicts {
|
||||
border-left-color: #ff9800;
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.status-panel.invalid {
|
||||
border-left-color: #f44336;
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.color1 { background-color: #FF5252; }
|
||||
.color2 { background-color: #448AFF; }
|
||||
.color3 { background-color: #4CAF50; }
|
||||
.color4 { background-color: #FFEB3B; }
|
||||
|
||||
canvas {
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
374
src/app/pages/algorithms/four-color/four-color.component.ts
Normal file
374
src/app/pages/algorithms/four-color/four-color.component.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, FourColorNode, Region} from './four-color.models';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {Information} from '../information/information';
|
||||
import {GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||
import {SharedFunctions} from '../../../shared/SharedFunctions';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-four-color',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
TranslateModule,
|
||||
Information
|
||||
],
|
||||
templateUrl: './four-color.component.html',
|
||||
styleUrl: './four-color.component.scss'
|
||||
})
|
||||
export class FourColorComponent implements AfterViewInit {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'FOUR_COLOR.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: 'FOUR_COLOR.TITLE',
|
||||
translateName: true,
|
||||
description: 'FOUR_COLOR.EXPLANATION.EXPLANATION',
|
||||
link: UrlConstants.FOUR_COLOR_THEOREM
|
||||
}
|
||||
],
|
||||
disclaimer: 'FOUR_COLOR.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: 'FOUR_COLOR.EXPLANATION.DISCLAIMER_BOTTOM',
|
||||
disclaimerListEntry: [
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_1',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_2',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_3',
|
||||
'FOUR_COLOR.EXPLANATION.DISCLAIMER_4'
|
||||
]
|
||||
};
|
||||
|
||||
gridRows = DEFAULT_GRID_ROWS;
|
||||
gridCols = DEFAULT_GRID_COLS;
|
||||
grid: FourColorNode[][] = [];
|
||||
regions: Region[] = [];
|
||||
executionTime = 0;
|
||||
solutionStatus: 'INCOMPLETE' | 'SOLVED' | 'CONFLICTS' | 'INVALID' = 'INCOMPLETE';
|
||||
|
||||
@ViewChild('fourColorCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
private nodeSize = 0;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ctx = this.canvasRef.nativeElement.getContext('2d')!;
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
applyGridSize(): void {
|
||||
if (this.gridRows < MIN_GRID_SIZE) this.gridRows = MIN_GRID_SIZE;
|
||||
if (this.gridRows > MAX_GRID_SIZE) this.gridRows = MAX_GRID_SIZE;
|
||||
if (this.gridCols < MIN_GRID_SIZE) this.gridCols = MIN_GRID_SIZE;
|
||||
if (this.gridCols > MAX_GRID_SIZE) this.gridCols = MAX_GRID_SIZE;
|
||||
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
initializeGrid(): void {
|
||||
this.grid = [];
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
const row: FourColorNode[] = [];
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
row.push({
|
||||
row: r,
|
||||
col: c,
|
||||
regionId: -1,
|
||||
color: 0,
|
||||
});
|
||||
}
|
||||
this.grid.push(row);
|
||||
}
|
||||
|
||||
this.generateRegions();
|
||||
this.resizeCanvas();
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
private resizeCanvas(): void {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const maxDim = Math.max(this.gridRows, this.gridCols);
|
||||
this.nodeSize = Math.floor(MAX_GRID_PX / maxDim);
|
||||
|
||||
canvas.width = this.gridCols * this.nodeSize;
|
||||
canvas.height = this.gridRows * this.nodeSize;
|
||||
}
|
||||
|
||||
generateRegions(): void {
|
||||
const numRegions = Math.floor((this.gridRows * this.gridCols) / 30);
|
||||
this.regions = [];
|
||||
const seeds = this.determineSeeds(numRegions);
|
||||
this.regionGrowth(seeds);
|
||||
this.determineAdjacency();
|
||||
}
|
||||
|
||||
private determineAdjacency() {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
const currentRegionId = this.grid[row][col].regionId;
|
||||
const neighbors = this.getNeighbors(row, col);
|
||||
for (const neighbor of neighbors) {
|
||||
const neighborRegionId = this.grid[neighbor.row][neighbor.col].regionId;
|
||||
if (neighborRegionId !== -1 && neighborRegionId !== currentRegionId) {
|
||||
this.regions[currentRegionId].neighbors.add(neighborRegionId);
|
||||
this.regions[neighborRegionId].neighbors.add(currentRegionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private regionGrowth(seeds: GridPos[]) {
|
||||
const queue: GridPos[] = [...seeds];
|
||||
while (queue.length > 0) {
|
||||
const {row, col} = queue.shift()!;
|
||||
const regionId = this.grid[row][col].regionId;
|
||||
|
||||
const neighbors = this.getNeighbors(row, col);
|
||||
for (const neighbor of neighbors) {
|
||||
if (this.grid[neighbor.row][neighbor.col].regionId === -1) {
|
||||
this.grid[neighbor.row][neighbor.col].regionId = regionId;
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private determineSeeds(numRegions: number) {
|
||||
const seeds: GridPos[] = [];
|
||||
for (let i = 0; i < numRegions; i++) {
|
||||
let r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
let c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
while (this.grid[r][c].regionId !== -1) {
|
||||
r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
}
|
||||
this.grid[r][c].regionId = i;
|
||||
seeds.push({row: r, col: c});
|
||||
this.regions.push({id: i, color: 0, neighbors: new Set<number>()});
|
||||
}
|
||||
return seeds;
|
||||
}
|
||||
|
||||
private getNeighbors(row: number, col: number): GridPos[] {
|
||||
const res: GridPos[] = [];
|
||||
if (row > 0) res.push({row: row - 1, col});
|
||||
if (row < this.gridRows - 1) res.push({row: row + 1, col});
|
||||
if (col > 0) res.push({row, col: col - 1});
|
||||
if (col < this.gridCols - 1) res.push({row, col: col + 1});
|
||||
return res;
|
||||
}
|
||||
|
||||
drawGrid(): void {
|
||||
if (!this.ctx) return;
|
||||
|
||||
this.ctx.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height);
|
||||
|
||||
// 1. Draw Cell Backgrounds
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
const node = this.grid[r][c];
|
||||
this.ctx.fillStyle = this.getNodeColor(node);
|
||||
this.ctx.fillRect(c * this.nodeSize, r * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw Region Borders
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
const currentRegion = this.grid[r][c].regionId;
|
||||
|
||||
// Right border
|
||||
if (c < this.gridCols - 1 && this.grid[r][c+1].regionId !== currentRegion) {
|
||||
this.ctx.moveTo((c + 1) * this.nodeSize, r * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
if (r < this.gridRows - 1 && this.grid[r+1][c].regionId !== currentRegion) {
|
||||
this.ctx.moveTo(c * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.ctx.stroke();
|
||||
|
||||
// 3. Draw Outer Border
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.strokeRect(0, 0, this.gridCols * this.nodeSize, this.gridRows * this.nodeSize);
|
||||
}
|
||||
|
||||
private getNodeColor(node: FourColorNode): string {
|
||||
switch (node.color) {
|
||||
case 1: return '#FF5252'; // Red
|
||||
case 2: return '#448AFF'; // Blue
|
||||
case 3: return '#4CAF50'; // Green
|
||||
case 4: return '#FFEB3B'; // Yellow
|
||||
default: return 'white';
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent): void {
|
||||
const pos = this.getGridPos(event);
|
||||
if (pos) this.handleInteraction(pos);
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent): void {
|
||||
if (event.buttons !== 1){
|
||||
return;
|
||||
}
|
||||
this.getGridPos(event);
|
||||
}
|
||||
|
||||
onTouchStart(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
const touch = event.touches[0];
|
||||
const pos = this.getGridPos(touch);
|
||||
if (pos) this.handleInteraction(pos);
|
||||
}
|
||||
|
||||
onTouchMove(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private getGridPos(event: MouseEvent | Touch): GridPos | null {
|
||||
const rect = this.canvasRef.nativeElement.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
const col = Math.floor(x / (rect.width / this.gridCols));
|
||||
const row = Math.floor(y / (rect.height / this.gridRows));
|
||||
|
||||
if (row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols) {
|
||||
return {row, col};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private handleInteraction(pos: GridPos): void {
|
||||
const node = this.grid[pos.row][pos.col];
|
||||
if (node.regionId === -1){
|
||||
return;
|
||||
}
|
||||
|
||||
const region = this.regions[node.regionId];
|
||||
region.color = (region.color % 4) + 1;
|
||||
this.updateRegionColors(region);
|
||||
this.checkSolution();
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
private updateRegionColors(region: Region): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
if (this.grid[row][col].regionId === region.id) {
|
||||
this.grid[row][col].color = region.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetColors(): void {
|
||||
for (const region of this.regions) {
|
||||
region.color = 0;
|
||||
this.updateRegionColors(region);
|
||||
}
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
autoSolve(): void {
|
||||
const startTime = performance.now();
|
||||
this.resetColors();
|
||||
|
||||
const success = this.backtrackSolve(0);
|
||||
const endTime = performance.now();
|
||||
this.executionTime = endTime - startTime;
|
||||
|
||||
if (success) {
|
||||
this.checkSolution();
|
||||
this.drawGrid();
|
||||
} else {
|
||||
const message = this.translate.instant('FOUR_COLOR.ALERT.NO_SOLUTION');
|
||||
this.snackBar.open(message, 'ALERT');
|
||||
}
|
||||
}
|
||||
|
||||
private backtrackSolve(regionIndex: number): boolean {
|
||||
if (regionIndex === this.regions.length) return true;
|
||||
|
||||
const region = this.regions[regionIndex];
|
||||
const availableColors = [1, 2, 3, 4];
|
||||
|
||||
for (const color of availableColors) {
|
||||
if (this.isColorValid(region, color)) {
|
||||
region.color = color;
|
||||
this.updateRegionColors(region);
|
||||
|
||||
if (this.backtrackSolve(regionIndex + 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
region.color = 0;
|
||||
//this.updateRegionColors(region);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isColorValid(region: Region, color: number): boolean {
|
||||
for (const neighborId of region.neighbors) {
|
||||
if (this.regions[neighborId].color === color){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
checkSolution(): void {
|
||||
let allColored = true;
|
||||
let hasConflicts = false;
|
||||
|
||||
for (const region of this.regions) {
|
||||
if (region.color === 0) {
|
||||
allColored = false;
|
||||
}
|
||||
if (region.color > 0 && !this.isColorValid(region, region.color)) {
|
||||
hasConflicts = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConflicts) {
|
||||
this.solutionStatus = allColored ? 'INVALID' : 'CONFLICTS';
|
||||
} else {
|
||||
this.solutionStatus = allColored ? 'SOLVED' : 'INCOMPLETE';
|
||||
}
|
||||
}
|
||||
|
||||
generateNewMap(): void {
|
||||
this.initializeGrid();
|
||||
}
|
||||
}
|
||||
18
src/app/pages/algorithms/four-color/four-color.models.ts
Normal file
18
src/app/pages/algorithms/four-color/four-color.models.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface FourColorNode {
|
||||
row: number;
|
||||
col: number;
|
||||
regionId: number;
|
||||
color: number;
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: number;
|
||||
color: number;
|
||||
neighbors: Set<number>;
|
||||
}
|
||||
|
||||
export const DEFAULT_GRID_ROWS = 25;
|
||||
export const DEFAULT_GRID_COLS = 25;
|
||||
export const MIN_GRID_SIZE = 20;
|
||||
export const MAX_GRID_SIZE = 38;
|
||||
export const MAX_GRID_PX = 600;
|
||||
@@ -6,6 +6,7 @@ import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
|
||||
@@ -27,7 +28,6 @@ enum NodeType {
|
||||
|
||||
@Component({
|
||||
selector: 'app-pathfinding',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@@ -48,6 +48,7 @@ enum NodeType {
|
||||
export class PathfindingComponent implements AfterViewInit {
|
||||
private readonly pathfindingService = inject(PathfindingService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
readonly NodeType = NodeType;
|
||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
@@ -483,7 +484,8 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
return true;
|
||||
}
|
||||
|
||||
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
|
||||
const message = this.translate.instant('PATHFINDING.ALERT.START_END_NODES');
|
||||
this.snackBar.open(message, 'OK', { duration: 5000, horizontalPosition: 'center', verticalPosition: 'top' });
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SortingAudioService {
|
||||
private audioContext: AudioContext | null = null;
|
||||
|
||||
private ensureContext(): AudioContext {
|
||||
this.audioContext ??= new AudioContext();
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
return this.audioContext;
|
||||
}
|
||||
|
||||
// Call this on the user gesture (button click) so the AudioContext can be created/resumed.
|
||||
initOnUserGesture(): void {
|
||||
this.ensureContext();
|
||||
}
|
||||
|
||||
playTone(value: number, maxValue: number, animationSpeedMs: number): void {
|
||||
const ctx = this.ensureContext();
|
||||
const frequency = this.valueToFrequency(value, maxValue);
|
||||
|
||||
// Keep tone duration slightly shorter than the animation frame to avoid overlap
|
||||
const duration = Math.min(0.1, (animationSpeedMs * 0.75) / 1000);
|
||||
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gainNode = ctx.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
|
||||
oscillator.type = 'sawtooth';
|
||||
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
|
||||
|
||||
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
||||
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + duration);
|
||||
}
|
||||
|
||||
playSortedSweep(sortedValues: number[], maxValue: number): void {
|
||||
const ctx = this.ensureContext();
|
||||
// Play a quick ascending sweep through all sorted bar values
|
||||
const step = Math.ceil(sortedValues.length / 40);
|
||||
sortedValues.forEach((value, i) => {
|
||||
if (i % step !== 0) {
|
||||
return;
|
||||
}
|
||||
const delayMs = (i / step) * 18;
|
||||
setTimeout(() => {
|
||||
const frequency = this.valueToFrequency(value, maxValue);
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gainNode = ctx.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
|
||||
|
||||
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.06);
|
||||
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + 0.06);
|
||||
}, delayMs);
|
||||
});
|
||||
}
|
||||
|
||||
// Maps a bar value linearly to the frequency range 180–1100 Hz (roughly 3 octaves)
|
||||
private valueToFrequency(value: number, maxValue: number): number {
|
||||
const minFreq = 400;
|
||||
const maxFreq = 800;
|
||||
return minFreq + (value / maxValue) * (maxFreq - minFreq);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class SortingService {
|
||||
|
||||
let start = -1;
|
||||
let end = array.length-1;
|
||||
let changed = false;
|
||||
let changed: boolean;
|
||||
do {
|
||||
changed = false;
|
||||
start += 1;
|
||||
@@ -224,6 +224,103 @@ export class SortingService {
|
||||
}
|
||||
}
|
||||
|
||||
// --- TIM SORT ---
|
||||
timSort(array: SortData[]): SortSnapshot[] {
|
||||
const snapshots: SortSnapshot[] = [];
|
||||
const arr = array.map(item => ({ ...item }));
|
||||
const n = arr.length;
|
||||
const RUN = 32;
|
||||
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
// Step 1: Sort small runs with insertion sort
|
||||
for (let i = 0; i < n; i += RUN) {
|
||||
const end = Math.min(i + RUN - 1, n - 1);
|
||||
this.insertionSortRange(arr, i, end, snapshots);
|
||||
}
|
||||
|
||||
// Step 2: Merge the sorted runs
|
||||
for (let size = RUN; size < n; size *= 2) {
|
||||
for (let left = 0; left < n; left += 2 * size) {
|
||||
const mid = Math.min(left + size - 1, n - 1);
|
||||
const right = Math.min(left + 2 * size - 1, n - 1);
|
||||
if (mid < right) {
|
||||
this.mergeRanges(arr, left, mid, right, snapshots);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arr.forEach(item => item.state = 'sorted');
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
private insertionSortRange(arr: SortData[], left: number, right: number, snapshots: SortSnapshot[]): void {
|
||||
for (let i = left + 1; i <= right; i++) {
|
||||
const tempValue = arr[i].value;
|
||||
arr[i].state = 'comparing';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
let j = i - 1;
|
||||
while (j >= left && arr[j].value > tempValue) {
|
||||
arr[j].state = 'comparing';
|
||||
arr[j + 1].value = arr[j].value;
|
||||
arr[j].state = 'unsorted';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
j--;
|
||||
}
|
||||
|
||||
arr[j + 1].value = tempValue;
|
||||
arr[i].state = 'unsorted';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
}
|
||||
}
|
||||
|
||||
private mergeRanges(arr: SortData[], left: number, mid: number, right: number, snapshots: SortSnapshot[]): void {
|
||||
const leftPart = arr.slice(left, mid + 1).map(item => ({ ...item }));
|
||||
const rightPart = arr.slice(mid + 1, right + 1).map(item => ({ ...item }));
|
||||
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
let k = left;
|
||||
|
||||
while (i < leftPart.length && j < rightPart.length) {
|
||||
// Highlight the write target and the right-side source (arr[mid+1+j] is still
|
||||
// untouched in the array since k never overtakes it during a merge)
|
||||
const rightSourceIdx = mid + 1 + j;
|
||||
arr[k].state = 'comparing';
|
||||
arr[rightSourceIdx].state = 'comparing';
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
|
||||
arr[rightSourceIdx].state = 'unsorted';
|
||||
|
||||
if (leftPart[i].value <= rightPart[j].value) {
|
||||
arr[k].value = leftPart[i].value;
|
||||
i++;
|
||||
} else {
|
||||
arr[k].value = rightPart[j].value;
|
||||
j++;
|
||||
}
|
||||
|
||||
arr[k].state = 'unsorted';
|
||||
k++;
|
||||
snapshots.push(this.createSnapshot(arr));
|
||||
}
|
||||
|
||||
while (i < leftPart.length) {
|
||||
arr[k].value = leftPart[i].value;
|
||||
i++;
|
||||
k++;
|
||||
}
|
||||
|
||||
while (j < rightPart.length) {
|
||||
arr[k].value = rightPart[j].value;
|
||||
j++;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
|
||||
private swap(arr: SortData[], i: number, j: number) {
|
||||
const temp = arr[i].value;
|
||||
arr[i].value = arr[j].value;
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
<button mat-raised-button (click)="generateNewArray()">
|
||||
<mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button (click)="toggleSound()">
|
||||
<mat-icon>{{ isSoundEnabled ? 'volume_up' : 'volume_off' }}</mat-icon>
|
||||
{{ isSoundEnabled ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-panel">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import { SortingService } from './service/sorting.service';
|
||||
import { SortingAudioService } from './service/sorting-audio.service';
|
||||
import {SortData, SortSnapshot} from './sorting.models';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {MatInput} from '@angular/material/input';
|
||||
@@ -15,14 +16,14 @@ import {AlgorithmInformation} from '../information/information.models';
|
||||
import {Information} from '../information/information';
|
||||
@Component({
|
||||
selector: 'app-sorting',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput, Information],
|
||||
templateUrl: './sorting.component.html',
|
||||
styleUrls: ['./sorting.component.scss']
|
||||
styleUrl: './sorting.component.scss'
|
||||
})
|
||||
export class SortingComponent implements OnInit {
|
||||
|
||||
private readonly sortingService: SortingService = inject(SortingService);
|
||||
private readonly audioService: SortingAudioService = inject(SortingAudioService);
|
||||
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
||||
|
||||
readonly MAX_ARRAY_SIZE: number = 200;
|
||||
@@ -50,6 +51,11 @@ export class SortingComponent implements OnInit {
|
||||
name: 'Heap Sort',
|
||||
description: 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION',
|
||||
link: UrlConstants.HEAP_SORT_WIKI
|
||||
},
|
||||
{
|
||||
name: 'Tim Sort',
|
||||
description: 'SORTING.EXPLANATION.TIMSORT_EXPLANATION',
|
||||
link: UrlConstants.TIM_SORT_WIKI
|
||||
}
|
||||
],
|
||||
disclaimer: 'SORTING.EXPLANATION.DISCLAIMER',
|
||||
@@ -66,9 +72,10 @@ export class SortingComponent implements OnInit {
|
||||
unsortedArrayCopy: SortData[] = [];
|
||||
arraySize = 50;
|
||||
maxArrayValue = 100;
|
||||
animationSpeed = 50; // Milliseconds per step
|
||||
animationSpeed = 100; // Milliseconds per step
|
||||
selectedAlgorithm: string = this.algoInformation.entries[0].name;
|
||||
executionTime = 0;
|
||||
isSoundEnabled = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.generateNewArray();
|
||||
@@ -113,8 +120,14 @@ export class SortingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSound(): void {
|
||||
this.isSoundEnabled = !this.isSoundEnabled;
|
||||
}
|
||||
|
||||
async startSorting(): Promise<void> {
|
||||
this.resetSorting();
|
||||
// Init the AudioContext on this user gesture so the browser allows sound
|
||||
this.audioService.initOnUserGesture();
|
||||
const startTime = performance.now();
|
||||
let snapshots: SortSnapshot[] = [];
|
||||
|
||||
@@ -131,6 +144,9 @@ export class SortingComponent implements OnInit {
|
||||
case 'Cocktail Sort':
|
||||
snapshots = this.sortingService.cocktailSort(this.sortArray);
|
||||
break;
|
||||
case 'Tim Sort':
|
||||
snapshots = this.sortingService.timSort(this.sortArray);
|
||||
break;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
@@ -149,11 +165,22 @@ export class SortingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isSoundEnabled) {
|
||||
// Play a tone for each comparing bar (max 2 at once to avoid noise)
|
||||
snapshot.array
|
||||
.filter(item => item.state === 'comparing')
|
||||
.slice(0, 2)
|
||||
.forEach(item => this.audioService.playTone(item.value, this.maxArrayValue, this.animationSpeed));
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
|
||||
if (index === snapshots.length - 1) {
|
||||
this.sortArray.forEach(item => item.state = 'sorted');
|
||||
this.cdr.detectChanges();
|
||||
if (this.isSoundEnabled) {
|
||||
this.audioService.playSortedSweep(this.sortArray.map(item => item.value), this.maxArrayValue);
|
||||
}
|
||||
}
|
||||
}, index * this.animationSpeed);
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ import {MatButton} from '@angular/material/button';
|
||||
@Component({
|
||||
selector: 'app-project-dialog',
|
||||
templateUrl: './project-dialog.component.html',
|
||||
styleUrls: ['./project-dialog.component.scss'],
|
||||
standalone: true,
|
||||
styleUrl: './project-dialog.component.scss',
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface Projects {
|
||||
|
||||
@Component({
|
||||
selector: 'app-projects',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatCardModule,
|
||||
MatChipsModule,
|
||||
|
||||
Reference in New Issue
Block a user