Implement improvements: remove GSAP, fix Math.max stack overflow, keyboard shortcuts, URL state persistence, performance metrics, mobile layout fixes
This commit is contained in:
parent
26838d5f14
commit
dc0705d235
1 changed files with 328 additions and 29 deletions
|
|
@ -37,8 +37,7 @@
|
||||||
<!-- Lucide Icons -->
|
<!-- Lucide Icons -->
|
||||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||||
|
|
||||||
<!-- GSAP -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* ── Theme Custom Properties ── */
|
/* ── Theme Custom Properties ── */
|
||||||
|
|
@ -134,6 +133,20 @@
|
||||||
:root:not(.dark) .hover\:text-white:hover { color: var(--c-accent) !important; }
|
:root:not(.dark) .hover\:text-white:hover { color: var(--c-accent) !important; }
|
||||||
:root:not(.dark) .bg-bg\/50 { background: color-mix(in srgb, var(--c-bg) 50%, transparent) !important; }
|
:root:not(.dark) .bg-bg\/50 { background: color-mix(in srgb, var(--c-bg) 50%, transparent) !important; }
|
||||||
|
|
||||||
|
/* ── Mobile-first responsive tweaks ── */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.stat-desc { display: none; }
|
||||||
|
.stat-card { padding: 0.5rem !important; }
|
||||||
|
.info-card { padding: 0.75rem !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile-first responsive tweaks ── */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.stat-desc { display: none; }
|
||||||
|
.stat-card { padding: 0.5rem !important; }
|
||||||
|
.info-card { padding: 0.75rem !important; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Sticky Sidebar (Desktop ≥1024px) ── */
|
/* ── Sticky Sidebar (Desktop ≥1024px) ── */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.app-layout {
|
.app-layout {
|
||||||
|
|
@ -153,6 +166,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Entry Animations ── */
|
||||||
|
@keyframes fadeSlideDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes fadeSlideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes popIn { from { opacity: 0; transform: scale(0.92); } to { opacity: 1; transform: scale(1); } }
|
||||||
|
.anim-header { animation: fadeSlideDown 0.5s ease-out both; }
|
||||||
|
.anim-section { animation: fadeSlideUp 0.45s ease-out both; }
|
||||||
|
.anim-stat { animation: popIn 0.35s cubic-bezier(0.34,1.56,0.64,1) both; }
|
||||||
|
|
||||||
|
/* ── Reduced Motion ── */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.anim-header, .anim-section, .anim-stat { animation: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Reduced Motion ── */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.anim-header, .anim-section, .anim-stat { animation: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Progress Bar ── */
|
/* ── Progress Bar ── */
|
||||||
.progress-track {
|
.progress-track {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
|
|
@ -194,7 +225,7 @@
|
||||||
<div class="app-layout">
|
<div class="app-layout">
|
||||||
|
|
||||||
<!-- ═══ SIDEBAR ═══ -->
|
<!-- ═══ SIDEBAR ═══ -->
|
||||||
<aside class="app-sidebar space-y-3 overflow-hidden">
|
<aside class="app-sidebar space-y-3">
|
||||||
|
|
||||||
<!-- Algorithm -->
|
<!-- Algorithm -->
|
||||||
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
||||||
|
|
@ -299,7 +330,7 @@
|
||||||
|
|
||||||
<!-- Transport Buttons -->
|
<!-- Transport Buttons -->
|
||||||
<section class="bg-surface border border-border rounded-xl p-4">
|
<section class="bg-surface border border-border rounded-xl p-4">
|
||||||
<div class="grid grid-cols-4 gap-2">
|
<div class="grid grid-cols-4 sm:grid-cols-5 gap-2">
|
||||||
<button id="btnStepBack"
|
<button id="btnStepBack"
|
||||||
class="flex items-center justify-center bg-surface2 border border-border text-muted rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:border-accent hover:text-white active:scale-95 transition-all min-h-[44px]"
|
class="flex items-center justify-center bg-surface2 border border-border text-muted rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:border-accent hover:text-white active:scale-95 transition-all min-h-[44px]"
|
||||||
aria-label="Schritt zurück" title="Schritt zurück">
|
aria-label="Schritt zurück" title="Schritt zurück">
|
||||||
|
|
@ -366,34 +397,71 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistics -->
|
<!-- Statistics -->
|
||||||
<section class="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
<section class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
<div class="stat-value font-bold text-accent font-mono leading-none" id="statCompares">0</div>
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statCompares">0</div>
|
||||||
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
<i data-lucide="git-compare" class="w-3.5 h-3.5"></i> Vergleiche
|
<i data-lucide="git-compare" class="w-3.5 h-3.5"></i> Vergleiche
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente miteinander verglichen wurden.</div>
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente miteinander verglichen wurden.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
<div class="stat-value font-bold text-accent font-mono leading-none" id="statSwaps">0</div>
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statSwaps">0</div>
|
||||||
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
<i data-lucide="arrow-left-right" class="w-3.5 h-3.5"></i> Tausche
|
<i data-lucide="arrow-left-right" class="w-3.5 h-3.5"></i> Tausche
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente ihre Plätze getauscht haben.</div>
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente ihre Plätze getauscht haben.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
<div class="stat-value font-bold text-accent font-mono leading-none" id="statMoves">0</div>
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statMoves">0</div>
|
||||||
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
<i data-lucide="move" class="w-3.5 h-3.5"></i> Verschiebungen
|
<i data-lucide="move" class="w-3.5 h-3.5"></i> Verschiebungen
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft ein Wert auf eine Position geschrieben oder verschoben wurde.</div>
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft ein Wert auf eine Position geschrieben oder verschoben wurde.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
<div class="stat-value font-bold text-accent font-mono leading-none" id="statStep">0</div>
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statStep">0</div>
|
||||||
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
<i data-lucide="footprints" class="w-3.5 h-3.5"></i> Schritt
|
<i data-lucide="footprints" class="w-3.5 h-3.5"></i> Schritt
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Der aktuell ausgeführte Visualisierungsschritt.</div>
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Der aktuell ausgeführte Visualisierungsschritt.</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statTime">0 ms</div>
|
||||||
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
|
<i data-lucide="clock" class="w-3.5 h-3.5"></i> Zeit
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Vergangene Echtzeit seit Start der Animation.</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente miteinander verglichen wurden.</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statSwaps">0</div>
|
||||||
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
|
<i data-lucide="arrow-left-right" class="w-3.5 h-3.5"></i> Tausche
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente ihre Plätze getauscht haben.</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statMoves">0</div>
|
||||||
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
|
<i data-lucide="move" class="w-3.5 h-3.5"></i> Verschiebungen
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft ein Wert auf eine Position geschrieben oder verschoben wurde.</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statStep">0</div>
|
||||||
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
|
<i data-lucide="footprints" class="w-3.5 h-3.5"></i> Schritt
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Der aktuell ausgeführte Visualisierungsschritt.</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
|
||||||
|
<div class="stat-value font-bold text-accent font-mono leading-none" id="statTime">0 ms</div>
|
||||||
|
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
|
||||||
|
<i data-lucide="clock" class="w-3.5 h-3.5"></i> Zeit
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Vergangene Echtzeit seit Start der Animation.</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -793,6 +861,7 @@ const $statComp = document.getElementById('statCompares');
|
||||||
const $statSwap = document.getElementById('statSwaps');
|
const $statSwap = document.getElementById('statSwaps');
|
||||||
const $statMove = document.getElementById('statMoves');
|
const $statMove = document.getElementById('statMoves');
|
||||||
const $statStep = document.getElementById('statStep');
|
const $statStep = document.getElementById('statStep');
|
||||||
|
const $statTime = document.getElementById('statTime');
|
||||||
const $btnPlayPause = document.getElementById('btnPlayPause');
|
const $btnPlayPause = document.getElementById('btnPlayPause');
|
||||||
const $btnStepBack = document.getElementById('btnStepBack');
|
const $btnStepBack = document.getElementById('btnStepBack');
|
||||||
const $btnStep = document.getElementById('btnStep');
|
const $btnStep = document.getElementById('btnStep');
|
||||||
|
|
@ -814,6 +883,10 @@ let animTimer = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isPaused = false;
|
let isPaused = false;
|
||||||
let arrayFresh = false; // true after Reset generated a new array, reset to false when sort starts
|
let arrayFresh = false; // true after Reset generated a new array, reset to false when sort starts
|
||||||
|
let startTime = 0; // performance.now() when animation started
|
||||||
|
let elapsedMs = 0; // accumulated ms across pauses
|
||||||
|
let startTime = 0; // performance.now() when animation started
|
||||||
|
let elapsedMs = 0; // accumulated ms across pauses
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// UI Helpers
|
// UI Helpers
|
||||||
|
|
@ -1539,8 +1612,8 @@ function buildSteps(algoName) {
|
||||||
if (n === 0) break;
|
if (n === 0) break;
|
||||||
// Counting Sort benötigt Integer-Indizes — Werte auf ganze Zahlen runden
|
// Counting Sort benötigt Integer-Indizes — Werte auf ganze Zahlen runden
|
||||||
for (let i = 0; i < n; i++) arr[i] = Math.round(arr[i]);
|
for (let i = 0; i < n; i++) arr[i] = Math.round(arr[i]);
|
||||||
const minVal = Math.min(...arr);
|
let minVal = arr[0], maxVal = arr[0];
|
||||||
const maxVal = Math.max(...arr);
|
for (let i = 1; i < n; i++) { if (arr[i] < minVal) minVal = arr[i]; if (arr[i] > maxVal) maxVal = arr[i]; }
|
||||||
const range = maxVal - minVal + 1;
|
const range = maxVal - minVal + 1;
|
||||||
const count = new Array(range).fill(0);
|
const count = new Array(range).fill(0);
|
||||||
// Zähle Vorkommen
|
// Zähle Vorkommen
|
||||||
|
|
@ -1567,7 +1640,8 @@ function buildSteps(algoName) {
|
||||||
if (n === 0) break;
|
if (n === 0) break;
|
||||||
// Radix Sort benötigt Integer — Werte auf ganze Zahlen runden
|
// Radix Sort benötigt Integer — Werte auf ganze Zahlen runden
|
||||||
for (let i = 0; i < n; i++) arr[i] = Math.round(arr[i]);
|
for (let i = 0; i < n; i++) arr[i] = Math.round(arr[i]);
|
||||||
const maxVal = Math.max(...arr);
|
let maxVal = arr[0];
|
||||||
|
for (let i = 1; i < n; i++) { if (arr[i] > maxVal) maxVal = arr[i]; }
|
||||||
const output = new Array(n);
|
const output = new Array(n);
|
||||||
let digitPos = 0;
|
let digitPos = 0;
|
||||||
for (let exp = 1; Math.floor(maxVal / exp) > 0; exp *= 10) {
|
for (let exp = 1; Math.floor(maxVal / exp) > 0; exp *= 10) {
|
||||||
|
|
@ -1809,8 +1883,8 @@ function buildSteps(algoName) {
|
||||||
case 'bucket': {
|
case 'bucket': {
|
||||||
const n = arr.length;
|
const n = arr.length;
|
||||||
if (n === 0) break;
|
if (n === 0) break;
|
||||||
const minVal = Math.min(...arr);
|
let minVal = arr[0], maxVal = arr[0];
|
||||||
const maxVal = Math.max(...arr);
|
for (let i = 1; i < n; i++) { if (arr[i] < minVal) minVal = arr[i]; if (arr[i] > maxVal) maxVal = arr[i]; }
|
||||||
const range = maxVal - minVal + 1;
|
const range = maxVal - minVal + 1;
|
||||||
const bucketCount = Math.max(1, Math.ceil(Math.sqrt(n)));
|
const bucketCount = Math.max(1, Math.ceil(Math.sqrt(n)));
|
||||||
const bucketSize = range / bucketCount;
|
const bucketSize = range / bucketCount;
|
||||||
|
|
@ -2011,7 +2085,9 @@ function drawBars(arr, highlights, allSorted, meta, connectedPair) {
|
||||||
ctx.lineTo(W - PAD_R, H - PAD_B + 0.5);
|
ctx.lineTo(W - PAD_R, H - PAD_B + 0.5);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
const maxVal = Math.max(...arr, 1);
|
let maxVal = arr[0];
|
||||||
|
for (let i = 1; i < n; i++) { if (arr[i] > maxVal) maxVal = arr[i]; }
|
||||||
|
if (maxVal < 1) maxVal = 1;
|
||||||
const slotW = areaW / n;
|
const slotW = areaW / n;
|
||||||
const gapW = Math.max(1, slotW * 0.1);
|
const gapW = Math.max(1, slotW * 0.1);
|
||||||
const barW = slotW - gapW;
|
const barW = slotW - gapW;
|
||||||
|
|
@ -2553,9 +2629,9 @@ function drawBitonicNetwork(state, W, H) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Memory View: Label + Resize (nur bei Typwechsel, nicht bei jedem Frame)
|
// Memory View: Label + Resize (Label nur bei Typwechsel, Resize immer)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
var _lastMemType = null;
|
let _lastMemType = null;
|
||||||
|
|
||||||
function updateMemView(memState) {
|
function updateMemView(memState) {
|
||||||
if (!memState) {
|
if (!memState) {
|
||||||
|
|
@ -2564,33 +2640,34 @@ function updateMemView(memState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$memContainer.classList.remove('hidden');
|
$memContainer.classList.remove('hidden');
|
||||||
// Label und Resize nur bei Typ-Wechsel (Performance)
|
// Label nur bei Typ-Wechsel aktualisieren (Performance)
|
||||||
if (memState.type !== _lastMemType) {
|
if (memState.type !== _lastMemType) {
|
||||||
_lastMemType = memState.type;
|
_lastMemType = memState.type;
|
||||||
if (memState.type === 'bst') {
|
if (memState.type === 'bst') {
|
||||||
$memLabel.innerHTML = '<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bin\u00e4rer Suchbaum (BST)';
|
$memLabel.innerHTML = '<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bin\u00e4rer Suchbaum (BST)';
|
||||||
resizeCanvas(memCanvas, memCtx, 0.4, 400, 160);
|
|
||||||
} else if (memState.type === 'heap') {
|
} else if (memState.type === 'heap') {
|
||||||
$memLabel.innerHTML = '<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bin\u00e4rer Heap';
|
$memLabel.innerHTML = '<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bin\u00e4rer Heap';
|
||||||
resizeCanvas(memCanvas, memCtx, 0.38, 380, 160);
|
|
||||||
} else if (memState.type === 'merge') {
|
} else if (memState.type === 'merge') {
|
||||||
$memLabel.innerHTML = '<i data-lucide="hard-drive" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Hilfs-Arrays (Out-of-Place)';
|
$memLabel.innerHTML = '<i data-lucide="hard-drive" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Hilfs-Arrays (Out-of-Place)';
|
||||||
resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
|
|
||||||
} else if (memState.type === 'counting') {
|
} else if (memState.type === 'counting') {
|
||||||
$memLabel.innerHTML = '<i data-lucide="bar-chart-3" class="w-3.5 h-3.5"></i> Arbeitsspeicher: H\u00e4ufigkeits-Array';
|
$memLabel.innerHTML = '<i data-lucide="bar-chart-3" class="w-3.5 h-3.5"></i> Arbeitsspeicher: H\u00e4ufigkeits-Array';
|
||||||
resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
|
|
||||||
} else if (memState.type === 'radix') {
|
} else if (memState.type === 'radix') {
|
||||||
$memLabel.innerHTML = '<i data-lucide="layers" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Radix-Buckets (10 Ziffern)';
|
$memLabel.innerHTML = '<i data-lucide="layers" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Radix-Buckets (10 Ziffern)';
|
||||||
resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
|
|
||||||
} else if (memState.type === 'buckets') {
|
} else if (memState.type === 'buckets') {
|
||||||
$memLabel.innerHTML = '<i data-lucide="inbox" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bucket-Verteilung';
|
$memLabel.innerHTML = '<i data-lucide="inbox" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bucket-Verteilung';
|
||||||
resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
|
|
||||||
} else if (memState.type === 'bitonic_network') {
|
} else if (memState.type === 'bitonic_network') {
|
||||||
$memLabel.innerHTML = '<i data-lucide="network" class="w-3.5 h-3.5"></i> Sortiernetz: ' + memState.stages.length + ' Phasen \u00b7 ' + memState.n + ' Dr\u00e4hte';
|
$memLabel.innerHTML = '<i data-lucide="network" class="w-3.5 h-3.5"></i> Sortiernetz: ' + memState.stages.length + ' Phasen \u00b7 ' + memState.n + ' Dr\u00e4hte';
|
||||||
resizeCanvas(memCanvas, memCtx, 0.35, 300, 140);
|
|
||||||
}
|
}
|
||||||
lucide.createIcons({ nodes: [$memLabel] });
|
lucide.createIcons({ nodes: [$memLabel] });
|
||||||
}
|
}
|
||||||
|
// Resize bei JEDEM Frame (fängt Window-Resize mit)
|
||||||
|
if (memState.type === 'bst') resizeCanvas(memCanvas, memCtx, 0.4, 400, 160);
|
||||||
|
else if (memState.type === 'heap') resizeCanvas(memCanvas, memCtx, 0.38, 380, 160);
|
||||||
|
else if (memState.type === 'merge') resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
|
||||||
|
else if (memState.type === 'counting') resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
|
||||||
|
else if (memState.type === 'radix') resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
|
||||||
|
else if (memState.type === 'buckets') resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
|
||||||
|
else if (memState.type === 'bitonic_network') resizeCanvas(memCanvas, memCtx, 0.35, 300, 140);
|
||||||
drawMemory(memState);
|
drawMemory(memState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2641,6 +2718,8 @@ function scheduleNext() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start timing on first tick
|
||||||
|
if (startTime === 0) startTime = performance.now();
|
||||||
animTimer = window.setInterval(tick, delay);
|
animTimer = window.setInterval(tick, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2648,6 +2727,9 @@ function tick() {
|
||||||
if (stepIndex >= steps.length) { finishAnimation(); return; }
|
if (stepIndex >= steps.length) { finishAnimation(); return; }
|
||||||
renderStep(steps[stepIndex], stepIndex + 1);
|
renderStep(steps[stepIndex], stepIndex + 1);
|
||||||
stepIndex++;
|
stepIndex++;
|
||||||
|
// Update elapsed time display
|
||||||
|
elapsedMs = Math.round(performance.now() - startTime);
|
||||||
|
$statTime.textContent = elapsedMs + ' ms';
|
||||||
if (stepIndex >= steps.length) finishAnimation();
|
if (stepIndex >= steps.length) finishAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2660,6 +2742,45 @@ function finishAnimation() {
|
||||||
drawBars(last.array, {}, true);
|
drawBars(last.array, {}, true);
|
||||||
setStats(last.compares, last.swaps, last.moves, steps.length);
|
setStats(last.compares, last.swaps, last.moves, steps.length);
|
||||||
}
|
}
|
||||||
|
elapsedMs = Math.round(performance.now() - startTime);
|
||||||
|
$statTime.textContent = elapsedMs + ' ms';
|
||||||
|
updateProgress(steps.length, steps.length);
|
||||||
|
$phaseLabel.innerHTML = ' ';
|
||||||
|
$stepExplanation.textContent = 'Sortierung abgeschlossen \u2713';
|
||||||
|
updateButtonStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (animTimer !== null) { clearInterval(animTimer); animTimer = null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start timing on first tick
|
||||||
|
if (startTime === 0) startTime = performance.now();
|
||||||
|
animTimer = window.setInterval(tick, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
if (stepIndex >= steps.length) { finishAnimation(); return; }
|
||||||
|
renderStep(steps[stepIndex], stepIndex + 1);
|
||||||
|
stepIndex++;
|
||||||
|
// Update elapsed time display
|
||||||
|
elapsedMs = Math.round(performance.now() - startTime);
|
||||||
|
$statTime.textContent = elapsedMs + ' ms';
|
||||||
|
if (stepIndex >= steps.length) finishAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishAnimation() {
|
||||||
|
if (animTimer !== null) { clearInterval(animTimer); animTimer = null; }
|
||||||
|
isRunning = false;
|
||||||
|
isPaused = false;
|
||||||
|
const last = steps[steps.length - 1];
|
||||||
|
if (last) {
|
||||||
|
drawBars(last.array, {}, true);
|
||||||
|
setStats(last.compares, last.swaps, last.moves, steps.length);
|
||||||
|
}
|
||||||
|
elapsedMs = Math.round(performance.now() - startTime);
|
||||||
|
$statTime.textContent = elapsedMs + ' ms';
|
||||||
updateProgress(steps.length, steps.length);
|
updateProgress(steps.length, steps.length);
|
||||||
$phaseLabel.innerHTML = ' ';
|
$phaseLabel.innerHTML = ' ';
|
||||||
$stepExplanation.textContent = 'Sortierung abgeschlossen \u2713';
|
$stepExplanation.textContent = 'Sortierung abgeschlossen \u2713';
|
||||||
|
|
@ -2680,9 +2801,12 @@ function doReset() {
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
steps = [];
|
steps = [];
|
||||||
stepIndex = 0;
|
stepIndex = 0;
|
||||||
|
startTime = 0;
|
||||||
|
elapsedMs = 0;
|
||||||
generateArray();
|
generateArray();
|
||||||
arrayFresh = true;
|
arrayFresh = true;
|
||||||
setStats(0, 0, 0, 0);
|
setStats(0, 0, 0, 0);
|
||||||
|
$statTime.textContent = '0 ms';
|
||||||
renderCurrent();
|
renderCurrent();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
}
|
}
|
||||||
|
|
@ -2707,7 +2831,10 @@ $btnPlayPause.addEventListener('click', function() {
|
||||||
renderCurrent();
|
renderCurrent();
|
||||||
steps = buildSteps($algoSelect.value);
|
steps = buildSteps($algoSelect.value);
|
||||||
stepIndex = 0;
|
stepIndex = 0;
|
||||||
|
startTime = 0;
|
||||||
|
elapsedMs = 0;
|
||||||
setStats(0, 0, 0, 0);
|
setStats(0, 0, 0, 0);
|
||||||
|
$statTime.textContent = '0 ms';
|
||||||
}
|
}
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
|
|
@ -2723,7 +2850,31 @@ $btnStep.addEventListener('click', function() {
|
||||||
renderCurrent();
|
renderCurrent();
|
||||||
steps = buildSteps($algoSelect.value);
|
steps = buildSteps($algoSelect.value);
|
||||||
stepIndex = 0;
|
stepIndex = 0;
|
||||||
|
startTime = 0;
|
||||||
|
elapsedMs = 0;
|
||||||
setStats(0, 0, 0, 0);
|
setStats(0, 0, 0, 0);
|
||||||
|
$statTime.textContent = '0 ms';
|
||||||
|
isRunning = true;
|
||||||
|
isPaused = true;
|
||||||
|
}
|
||||||
|
isRunning = true;
|
||||||
|
isPaused = false;
|
||||||
|
updateButtonStates();
|
||||||
|
scheduleNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step forward
|
||||||
|
$btnStep.addEventListener('click', function() {
|
||||||
|
if (!isRunning) {
|
||||||
|
if (!arrayFresh) { generateArray(); }
|
||||||
|
arrayFresh = false;
|
||||||
|
renderCurrent();
|
||||||
|
steps = buildSteps($algoSelect.value);
|
||||||
|
stepIndex = 0;
|
||||||
|
startTime = 0;
|
||||||
|
elapsedMs = 0;
|
||||||
|
setStats(0, 0, 0, 0);
|
||||||
|
$statTime.textContent = '0 ms';
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
isPaused = true;
|
isPaused = true;
|
||||||
}
|
}
|
||||||
|
|
@ -2761,16 +2912,19 @@ $btnReset.addEventListener('click', doReset);
|
||||||
$speedSlider.addEventListener('input', function() {
|
$speedSlider.addEventListener('input', function() {
|
||||||
updateSpeedLabel();
|
updateSpeedLabel();
|
||||||
if (isRunning && !isPaused) scheduleNext();
|
if (isRunning && !isPaused) scheduleNext();
|
||||||
|
writeUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
$sizeSlider.addEventListener('input', function() {
|
$sizeSlider.addEventListener('input', function() {
|
||||||
updateSizeLabel();
|
updateSizeLabel();
|
||||||
if (!isRunning) doReset();
|
if (!isRunning) doReset();
|
||||||
|
writeUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
$algoSelect.addEventListener('change', function() {
|
$algoSelect.addEventListener('change', function() {
|
||||||
updateThreadSlider();
|
updateThreadSlider();
|
||||||
if (!isRunning) doReset();
|
if (!isRunning) doReset();
|
||||||
|
writeUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
$threadSlider.addEventListener('input', function() {
|
$threadSlider.addEventListener('input', function() {
|
||||||
|
|
@ -2780,6 +2934,29 @@ $threadSlider.addEventListener('input', function() {
|
||||||
|
|
||||||
$presetSelect.addEventListener('change', function() {
|
$presetSelect.addEventListener('change', function() {
|
||||||
if (!isRunning) doReset();
|
if (!isRunning) doReset();
|
||||||
|
writeUrlState();
|
||||||
|
});
|
||||||
|
|
||||||
|
$sizeSlider.addEventListener('input', function() {
|
||||||
|
updateSizeLabel();
|
||||||
|
if (!isRunning) doReset();
|
||||||
|
writeUrlState();
|
||||||
|
});
|
||||||
|
|
||||||
|
$algoSelect.addEventListener('change', function() {
|
||||||
|
updateThreadSlider();
|
||||||
|
if (!isRunning) doReset();
|
||||||
|
writeUrlState();
|
||||||
|
});
|
||||||
|
|
||||||
|
$threadSlider.addEventListener('input', function() {
|
||||||
|
$threadVal.textContent = $threadSlider.value;
|
||||||
|
if (isRunning && !isPaused) scheduleNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
$presetSelect.addEventListener('change', function() {
|
||||||
|
if (!isRunning) doReset();
|
||||||
|
writeUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
$customArray.addEventListener('change', function() {
|
$customArray.addEventListener('change', function() {
|
||||||
|
|
@ -2799,6 +2976,76 @@ $btnTheme.addEventListener('click', function() {
|
||||||
applyTheme(newTheme);
|
applyTheme(newTheme);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Keyboard Shortcuts
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
// Ignoriere Events in Input-Feldern
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
|
switch (e.key) {
|
||||||
|
case ' ':
|
||||||
|
case 'k':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnPlayPause.click();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'l':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnStep.click();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'j':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnStepBack.click();
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnReset.click();
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnTheme.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Touch / Pointer Support for Sliders
|
||||||
|
// ================================================================
|
||||||
|
// Keyboard Shortcuts
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
// Ignoriere Events in Input-Feldern
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
|
switch (e.key) {
|
||||||
|
case ' ':
|
||||||
|
case 'k':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnPlayPause.click();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'l':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnStep.click();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'j':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnStepBack.click();
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnReset.click();
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
e.preventDefault();
|
||||||
|
$btnTheme.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Touch / Pointer Support for Sliders
|
// Touch / Pointer Support for Sliders
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
@ -2832,6 +3079,50 @@ makeTouchSlider($sizeSlider);
|
||||||
makeTouchSlider($speedSlider);
|
makeTouchSlider($speedSlider);
|
||||||
makeTouchSlider($threadSlider);
|
makeTouchSlider($threadSlider);
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// URL State Persistence
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function readUrlState() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.has('algo')) $algoSelect.value = params.get('algo');
|
||||||
|
if (params.has('preset')) $presetSelect.value = params.get('preset');
|
||||||
|
if (params.has('size')) $sizeSlider.value = Math.max(5, Math.min(200, parseInt(params.get('size'), 10) || 50));
|
||||||
|
if (params.has('speed')) $speedSlider.value = Math.max(1, Math.min(100, parseInt(params.get('speed'), 10) || 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeUrlState() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('algo', $algoSelect.value);
|
||||||
|
params.set('preset', $presetSelect.value);
|
||||||
|
params.set('size', $sizeSlider.value);
|
||||||
|
params.set('speed', $speedSlider.value);
|
||||||
|
const url = window.location.pathname + '?' + params.toString();
|
||||||
|
window.history.replaceState(null, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// URL State Persistence
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function readUrlState() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.has('algo')) $algoSelect.value = params.get('algo');
|
||||||
|
if (params.has('preset')) $presetSelect.value = params.get('preset');
|
||||||
|
if (params.has('size')) $sizeSlider.value = Math.max(5, Math.min(200, parseInt(params.get('size'), 10) || 50));
|
||||||
|
if (params.has('speed')) $speedSlider.value = Math.max(1, Math.min(100, parseInt(params.get('speed'), 10) || 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeUrlState() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('algo', $algoSelect.value);
|
||||||
|
params.set('preset', $presetSelect.value);
|
||||||
|
params.set('size', $sizeSlider.value);
|
||||||
|
params.set('speed', $speedSlider.value);
|
||||||
|
const url = window.location.pathname + '?' + params.toString();
|
||||||
|
window.history.replaceState(null, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Window Resize (ResizeObserver + fallback)
|
// Window Resize (ResizeObserver + fallback)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
@ -2865,6 +3156,8 @@ lucide.createIcons();
|
||||||
const prefersDark = stored ? stored === 'dark' : true;
|
const prefersDark = stored ? stored === 'dark' : true;
|
||||||
applyTheme(prefersDark, true); // skipRender=true during init
|
applyTheme(prefersDark, true); // skipRender=true during init
|
||||||
})();
|
})();
|
||||||
|
// Restore state from URL
|
||||||
|
readUrlState();
|
||||||
refreshColors();
|
refreshColors();
|
||||||
updateSpeedLabel();
|
updateSpeedLabel();
|
||||||
updateSizeLabel();
|
updateSizeLabel();
|
||||||
|
|
@ -2876,12 +3169,18 @@ renderCurrent();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
updateProgress(0, 0);
|
updateProgress(0, 0);
|
||||||
|
|
||||||
// FIX: Berücksichtigung von prefers-reduced-motion für die GSAP Eintritts-Animationen
|
// Entry-Animationen mit CSS @keyframes (GSAP entfernt)
|
||||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
if (!prefersReducedMotion) {
|
if (!prefersReducedMotion) {
|
||||||
gsap.from('header', { opacity: 0, y: -20, duration: 0.5, ease: 'power2.out' });
|
document.querySelector('header').classList.add('anim-header');
|
||||||
gsap.from('section, details', { opacity: 0, y: 16, duration: 0.45, stagger: 0.07, ease: 'power2.out', delay: 0.15 });
|
document.querySelectorAll('section, details').forEach(function(el, i) {
|
||||||
gsap.from('.stat-card', { opacity: 0, scale: 0.92, duration: 0.35, stagger: 0.06, ease: 'back.out(1.4)', delay: 0.35 });
|
el.style.animationDelay = (0.15 + i * 0.07) + 's';
|
||||||
|
el.classList.add('anim-section');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.stat-card').forEach(function(el, i) {
|
||||||
|
el.style.animationDelay = (0.35 + i * 0.06) + 's';
|
||||||
|
el.classList.add('anim-stat');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue