Fix: timsort snap params, passive resize, drawPath optimization, aria-live, readUrlState labels

This commit is contained in:
Dieter Schlüter 2026-04-06 18:43:43 +02:00
commit 113b8ff758

View file

@ -197,7 +197,7 @@
<div class="flex-1"></div>
<div class="text-center">
<h1 class="title-text font-bold tracking-tight" style="color: var(--c-text); text-shadow: 0 0 40px rgba(74,124,255,0.3);">
Sortier-Algorithmen <span style="font-size: 0.45em; font-weight: 400; opacity: 0.5; vertical-align: middle;">v0.2.5</span>
Sortier-Algorithmen <span style="font-size: 0.45em; font-weight: 400; opacity: 0.5; vertical-align: middle;">v0.2.6</span>
</h1>
<p class="text-muted text-sm mt-0.5">Interaktive Visualisierung mit schrittweiser Animation</p>
</div>
@ -359,8 +359,8 @@
<!-- Phase Label + Step Explanation (fixed height to prevent layout shift) -->
<div class="bg-surface border border-border rounded-xl px-4 py-2 space-y-0.5" style="min-height: 52px;">
<div id="phaseLabel" class="text-xs font-semibold text-accent truncate" style="min-height: 20px;">&nbsp;</div>
<div id="stepExplanation" class="text-xs text-muted truncate font-mono" style="min-height: 18px;">&nbsp;</div>
<div id="phaseLabel" aria-live="polite" class="text-xs font-semibold text-accent truncate" style="min-height: 20px;">&nbsp;</div>
<div id="stepExplanation" aria-live="polite" class="text-xs text-muted truncate font-mono" style="min-height: 18px;">&nbsp;</div>
</div>
<!-- Canvas -->
@ -381,7 +381,7 @@
<div class="progress-track flex-1">
<div id="progressFill" class="progress-fill" style="width: 0%;"></div>
</div>
<span id="stepCounter" class="text-xs text-muted font-mono whitespace-nowrap">0 / 0</span>
<span id="stepCounter" aria-live="polite" class="text-xs text-muted font-mono whitespace-nowrap">0 / 0</span>
</div>
<!-- Statistics -->
@ -1687,6 +1687,8 @@ function buildSteps(algoName) {
case 'timsort': {
const n = arr.length;
const RUN = 32;
const runMeta = { runs: [] };
for (let r = 0; r < n; r += RUN) runMeta.runs.push(r);
// Phase 1: Insertion Sort auf Runs der Größe RUN
for (let start = 0; start < n; start += RUN) {
const end = Math.min(start + RUN, n);
@ -1695,11 +1697,11 @@ function buildSteps(algoName) {
let j = i - 1;
while (j >= start) {
compares++;
snap('compare', [j, j + 1]);
snap('compare', [j, j + 1], null, runMeta);
if (arr[j] > key) {
arr[j + 1] = arr[j];
moves++;
snap('move', [j + 1]);
snap('move', [j + 1], null, runMeta);
j--;
} else {
break;
@ -1708,7 +1710,7 @@ function buildSteps(algoName) {
if (arr[j + 1] !== key) {
arr[j + 1] = key;
moves++;
snap('move', [j + 1]);
snap('move', [j + 1], null, runMeta);
}
}
}
@ -2109,6 +2111,24 @@ function drawBars(arr, highlights, allSorted, meta, connectedPair) {
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
// Reusable rounded-rect path helper (defined once, called per bar)
function drawRoundedRectPath(ctx, px, py, pw, ph, pr) {
if (pr > 0 && ph > pr * 2) {
ctx.beginPath();
ctx.moveTo(px + pr, py);
ctx.lineTo(px + pw - pr, py);
ctx.quadraticCurveTo(px + pw, py, px + pw, py + pr);
ctx.lineTo(px + pw, py + ph);
ctx.lineTo(px, py + ph);
ctx.lineTo(px, py + pr);
ctx.quadraticCurveTo(px, py, px + pr, py);
ctx.closePath();
} else {
ctx.beginPath();
ctx.rect(px, py, pw, ph);
}
}
for (let i = 0; i < n; i++) {
const val = arr[i];
const barH = Math.max(2, (val / maxVal) * areaH);
@ -2126,41 +2146,24 @@ function drawBars(arr, highlights, allSorted, meta, connectedPair) {
else if (t === 'move') color = COLORS.move;
}
// Rounded-top bar path generator
const r = Math.min(3, barW / 4);
function drawPath() {
if (r > 0 && barH > r * 2) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + barW - r, y);
ctx.quadraticCurveTo(x + barW, y, x + barW, y + r);
ctx.lineTo(x + barW, y + barH);
ctx.lineTo(x, y + barH);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
} else {
ctx.beginPath();
ctx.rect(x, y, barW, barH);
}
}
// FIX: Glow-Effekt VOR dem normalen Balken zeichnen
// Glow-Effekt
if (highlights[i] && !allSorted) {
ctx.save();
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 14;
ctx.globalAlpha = 0.35;
drawPath();
drawRoundedRectPath(ctx, x, y, barW, barH, r);
ctx.fill();
ctx.restore();
}
// Haupt-Balken zeichnen
// Haupt-Balken
ctx.fillStyle = color;
ctx.globalAlpha = 1;
drawPath();
drawRoundedRectPath(ctx, x, y, barW, barH, r);
ctx.fill();
// Value label
@ -2929,6 +2932,8 @@ function readUrlState() {
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));
updateSizeLabel();
updateSpeedLabel();
}
function writeUrlState() {
@ -2961,7 +2966,7 @@ function handleResize() {
}
// Use window resize to avoid feedback loops (ResizeObserver on canvas parent causes blowout)
window.addEventListener('resize', handleResize);
window.addEventListener('resize', handleResize, { passive: true });
// ================================================================
// Initialization