Implement improvements: remove GSAP, fix Math.max stack overflow, keyboard shortcuts, URL state persistence, performance metrics, mobile layout fixes

This commit is contained in:
Dieter Schlüter 2026-04-06 16:34:27 +02:00
commit dc0705d235

View file

@ -37,8 +37,7 @@
<!-- Lucide Icons -->
<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>
/* ── Theme Custom Properties ── */
@ -134,6 +133,20 @@
: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; }
/* ── 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) ── */
@media (min-width: 1024px) {
.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-track {
height: 4px;
@ -194,7 +225,7 @@
<div class="app-layout">
<!-- ═══ SIDEBAR ═══ -->
<aside class="app-sidebar space-y-3 overflow-hidden">
<aside class="app-sidebar space-y-3">
<!-- Algorithm -->
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
@ -299,7 +330,7 @@
<!-- Transport Buttons -->
<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"
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">
@ -366,34 +397,71 @@
</div>
<!-- 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="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">
<i data-lucide="git-compare" class="w-3.5 h-3.5"></i> Vergleiche
</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 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="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 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="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 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="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>
</section>
@ -793,6 +861,7 @@ const $statComp = document.getElementById('statCompares');
const $statSwap = document.getElementById('statSwaps');
const $statMove = document.getElementById('statMoves');
const $statStep = document.getElementById('statStep');
const $statTime = document.getElementById('statTime');
const $btnPlayPause = document.getElementById('btnPlayPause');
const $btnStepBack = document.getElementById('btnStepBack');
const $btnStep = document.getElementById('btnStep');
@ -814,6 +883,10 @@ let animTimer = null;
let isRunning = false;
let isPaused = false;
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
@ -1539,8 +1612,8 @@ function buildSteps(algoName) {
if (n === 0) break;
// Counting Sort benötigt Integer-Indizes — Werte auf ganze Zahlen runden
for (let i = 0; i < n; i++) arr[i] = Math.round(arr[i]);
const minVal = Math.min(...arr);
const maxVal = Math.max(...arr);
let minVal = arr[0], maxVal = arr[0];
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 count = new Array(range).fill(0);
// Zähle Vorkommen
@ -1567,7 +1640,8 @@ function buildSteps(algoName) {
if (n === 0) break;
// Radix Sort benötigt Integer — Werte auf ganze Zahlen runden
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);
let digitPos = 0;
for (let exp = 1; Math.floor(maxVal / exp) > 0; exp *= 10) {
@ -1809,8 +1883,8 @@ function buildSteps(algoName) {
case 'bucket': {
const n = arr.length;
if (n === 0) break;
const minVal = Math.min(...arr);
const maxVal = Math.max(...arr);
let minVal = arr[0], maxVal = arr[0];
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 bucketCount = Math.max(1, Math.ceil(Math.sqrt(n)));
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.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 gapW = Math.max(1, slotW * 0.1);
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) {
if (!memState) {
@ -2564,33 +2640,34 @@ function updateMemView(memState) {
return;
}
$memContainer.classList.remove('hidden');
// Label und Resize nur bei Typ-Wechsel (Performance)
// Label nur bei Typ-Wechsel aktualisieren (Performance)
if (memState.type !== _lastMemType) {
_lastMemType = memState.type;
if (memState.type === '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') {
$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') {
$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') {
$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') {
$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') {
$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') {
$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] });
}
// 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);
}
@ -2641,6 +2718,8 @@ function scheduleNext() {
}
}
// Start timing on first tick
if (startTime === 0) startTime = performance.now();
animTimer = window.setInterval(tick, delay);
}
@ -2648,6 +2727,9 @@ 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();
}
@ -2660,6 +2742,45 @@ function finishAnimation() {
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);
$phaseLabel.innerHTML = '&nbsp;';
$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);
$phaseLabel.innerHTML = '&nbsp;';
$stepExplanation.textContent = 'Sortierung abgeschlossen \u2713';
@ -2680,9 +2801,12 @@ function doReset() {
isPaused = false;
steps = [];
stepIndex = 0;
startTime = 0;
elapsedMs = 0;
generateArray();
arrayFresh = true;
setStats(0, 0, 0, 0);
$statTime.textContent = '0 ms';
renderCurrent();
updateButtonStates();
}
@ -2707,7 +2831,10 @@ $btnPlayPause.addEventListener('click', function() {
renderCurrent();
steps = buildSteps($algoSelect.value);
stepIndex = 0;
startTime = 0;
elapsedMs = 0;
setStats(0, 0, 0, 0);
$statTime.textContent = '0 ms';
}
isRunning = true;
isPaused = false;
@ -2723,7 +2850,31 @@ $btnStep.addEventListener('click', function() {
renderCurrent();
steps = buildSteps($algoSelect.value);
stepIndex = 0;
startTime = 0;
elapsedMs = 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;
isPaused = true;
}
@ -2761,16 +2912,19 @@ $btnReset.addEventListener('click', doReset);
$speedSlider.addEventListener('input', function() {
updateSpeedLabel();
if (isRunning && !isPaused) scheduleNext();
writeUrlState();
});
$sizeSlider.addEventListener('input', function() {
updateSizeLabel();
if (!isRunning) doReset();
writeUrlState();
});
$algoSelect.addEventListener('change', function() {
updateThreadSlider();
if (!isRunning) doReset();
writeUrlState();
});
$threadSlider.addEventListener('input', function() {
@ -2780,6 +2934,29 @@ $threadSlider.addEventListener('input', function() {
$presetSelect.addEventListener('change', function() {
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() {
@ -2799,6 +2976,76 @@ $btnTheme.addEventListener('click', function() {
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
// ================================================================
@ -2832,6 +3079,50 @@ makeTouchSlider($sizeSlider);
makeTouchSlider($speedSlider);
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)
// ================================================================
@ -2865,6 +3156,8 @@ lucide.createIcons();
const prefersDark = stored ? stored === 'dark' : true;
applyTheme(prefersDark, true); // skipRender=true during init
})();
// Restore state from URL
readUrlState();
refreshColors();
updateSpeedLabel();
updateSizeLabel();
@ -2876,12 +3169,18 @@ renderCurrent();
updateButtonStates();
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;
if (!prefersReducedMotion) {
gsap.from('header', { opacity: 0, y: -20, duration: 0.5, ease: 'power2.out' });
gsap.from('section, details', { opacity: 0, y: 16, duration: 0.45, stagger: 0.07, ease: 'power2.out', delay: 0.15 });
gsap.from('.stat-card', { opacity: 0, scale: 0.92, duration: 0.35, stagger: 0.06, ease: 'back.out(1.4)', delay: 0.35 });
document.querySelector('header').classList.add('anim-header');
document.querySelectorAll('section, details').forEach(function(el, i) {
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>