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 -->
|
||||
<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 = ' ';
|
||||
$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 = ' ';
|
||||
$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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue