zoom funktion hinzugefügt

This commit is contained in:
2026-09-29 09:36:20 +02:00
parent e1a5ff86f0
commit 92cb45a340
+66 -9
View File
@@ -283,7 +283,7 @@ h1 { margin:0 0 6px; font-size:28px; }
.subtitle { color:#aaa; margin-bottom:22px; } .subtitle { color:#aaa; margin-bottom:22px; }
.editor { background:#181818; border:1px solid #333; border-radius:10px; padding:20px; } .editor { background:#181818; border:1px solid #333; border-radius:10px; padding:20px; }
.wave-wrap { position:relative; width:100%; height:240px; background:#0b0b0b; border-radius:6px; overflow:hidden; user-select:none; touch-action:none; } .wave-wrap { position:relative; width:100%; height:240px; background:#0b0b0b; border-radius:6px; overflow:hidden; user-select:none; touch-action:none; }
.wave-wrap img { width:100%; height:100%; object-fit:fill; display:block; opacity:.9; } .wave-image { position:absolute; top:0; left:0; width:100%; height:100%; object-fit:fill; display:block; opacity:.9; pointer-events:none; }
.selection { position:absolute; top:0; bottom:0; background:rgba(80,150,255,.16); border-left:2px solid #fff; border-right:2px solid #fff; pointer-events:none; } .selection { position:absolute; top:0; bottom:0; background:rgba(80,150,255,.16); border-left:2px solid #fff; border-right:2px solid #fff; pointer-events:none; }
.playhead { position:absolute; top:0; bottom:0; width:2px; margin-left:-1px; background:#ffcc33; box-shadow:0 0 5px #000; z-index:4; pointer-events:none; } .playhead { position:absolute; top:0; bottom:0; width:2px; margin-left:-1px; background:#ffcc33; box-shadow:0 0 5px #000; z-index:4; pointer-events:none; }
.handle { position:absolute; top:0; bottom:0; width:18px; margin-left:-9px; cursor:ew-resize; z-index:3; touch-action:none; } .handle { position:absolute; top:0; bottom:0; width:18px; margin-left:-9px; cursor:ew-resize; z-index:3; touch-action:none; }
@@ -299,6 +299,7 @@ button.primary { background:#1769aa; }
button.danger { background:#8b0000; } button.danger { background:#8b0000; }
button:disabled { opacity:.45; cursor:not-allowed; } button:disabled { opacity:.45; cursor:not-allowed; }
.position { margin-left:auto; color:#ccc; font-variant-numeric:tabular-nums; } .position { margin-left:auto; color:#ccc; font-variant-numeric:tabular-nums; }
.zoom { color:#aaa; font-variant-numeric:tabular-nums; }
.values { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-top:20px; } .values { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-top:20px; }
.value { background:#222; border-radius:6px; padding:12px; } .value { background:#222; border-radius:6px; padding:12px; }
.value span { display:block; color:#999; font-size:13px; margin-bottom:5px; } .value span { display:block; color:#999; font-size:13px; margin-bottom:5px; }
@@ -313,6 +314,7 @@ button:disabled { opacity:.45; cursor:not-allowed; }
body { padding:12px; } body { padding:12px; }
.editor { padding:12px; } .editor { padding:12px; }
.wave-wrap { height:180px; } .wave-wrap { height:180px; }
.zoom { width:100%; margin-left:0; }
.values { grid-template-columns:1fr; } .values { grid-template-columns:1fr; }
.position { width:100%; margin-left:0; } .position { width:100%; margin-left:0; }
button, a.button { flex:1 1 auto; } button, a.button { flex:1 1 auto; }
@@ -327,15 +329,15 @@ button:disabled { opacity:.45; cursor:not-allowed; }
<div class="editor"> <div class="editor">
<div class="wave-wrap" id="wave"> <div class="wave-wrap" id="wave">
<img src="{{ waveform_url }}" alt="Wellenform"> <img class="wave-image" id="waveImage" src="{{ waveform_url }}" alt="Wellenform">
<div class="selection" id="selection"></div> <div class="selection" id="selection"></div>
<div class="playhead" id="playhead"></div> <div class="playhead" id="playhead"></div>
<div class="handle" id="startHandle" title="Anfang verschieben"></div> <div class="handle" id="startHandle" title="Anfang verschieben"></div>
<div class="handle" id="endHandle" title="Ende verschieben"></div> <div class="handle" id="endHandle" title="Ende verschieben"></div>
</div> </div>
<div class="time-row"> <div class="time-row">
<span>00:00:00</span> <span id="viewStart">00:00:00</span>
<span>{{ duration }}</span> <span id="viewEnd">{{ duration }}</span>
</div> </div>
<audio id="audio" controls preload="metadata" style="width:100%;margin-top:18px"> <audio id="audio" controls preload="metadata" style="width:100%;margin-top:18px">
@@ -351,6 +353,8 @@ button:disabled { opacity:.45; cursor:not-allowed; }
<button onclick="playFromStart()">▶ Ab Auswahlstart</button> <button onclick="playFromStart()">▶ Ab Auswahlstart</button>
<button onclick="setStartAtCurrent()">⏱ Anfang = Position</button> <button onclick="setStartAtCurrent()">⏱ Anfang = Position</button>
<button onclick="setEndAtCurrent()">⏱ Ende = Position</button> <button onclick="setEndAtCurrent()">⏱ Ende = Position</button>
<button onclick="resetZoom()">↺ Gesamtansicht</button>
<span class="zoom" id="zoomValue">Zoom: 100 %</span>
<span class="position" id="position">00:00:00</span> <span class="position" id="position">00:00:00</span>
</div> </div>
@@ -375,7 +379,14 @@ const filename = {{ filename|tojson }};
let start = 0; let start = 0;
let end = duration; let end = duration;
let dragging = null; let dragging = null;
let zoom = 1;
let viewStart = 0;
let viewEnd = duration;
const MIN_ZOOM = 1;
const MAX_ZOOM = 16;
const ZOOM_FACTOR = 1.5;
const wave = document.getElementById('wave'); const wave = document.getElementById('wave');
const waveImage = document.getElementById('waveImage');
const startHandle = document.getElementById('startHandle'); const startHandle = document.getElementById('startHandle');
const endHandle = document.getElementById('endHandle'); const endHandle = document.getElementById('endHandle');
const selection = document.getElementById('selection'); const selection = document.getElementById('selection');
@@ -391,24 +402,70 @@ function fmt(sec) {
} }
function xToTime(clientX) { function xToTime(clientX) {
const r = wave.getBoundingClientRect(); const r = wave.getBoundingClientRect();
return Math.max(0, Math.min(duration, ((clientX-r.left)/r.width)*duration)); const ratio = Math.max(0, Math.min(1, (clientX-r.left)/r.width));
return viewStart + ratio * (viewEnd-viewStart);
}
function timeToPercent(t) {
const span = viewEnd-viewStart;
if (span <= 0) return 0;
return ((t-viewStart)/span)*100;
} }
function render() { function render() {
const a = (start/duration)*100; const a = timeToPercent(start);
const b = (end/duration)*100; const b = timeToPercent(end);
startHandle.style.left = a+'%'; startHandle.style.left = a+'%';
endHandle.style.left = b+'%'; endHandle.style.left = b+'%';
selection.style.left = a+'%'; selection.style.left = a+'%';
selection.style.width = (b-a)+'%'; selection.style.width = Math.max(0, b-a)+'%';
const imageWidth = zoom * 100;
waveImage.style.width = imageWidth+'%';
waveImage.style.left = (-(viewStart/duration)*imageWidth)+'%';
document.getElementById('startValue').textContent = fmt(start); document.getElementById('startValue').textContent = fmt(start);
document.getElementById('endValue').textContent = fmt(end); document.getElementById('endValue').textContent = fmt(end);
document.getElementById('keepValue').textContent = fmt(end-start); document.getElementById('keepValue').textContent = fmt(end-start);
document.getElementById('viewStart').textContent = fmt(viewStart);
document.getElementById('viewEnd').textContent = fmt(viewEnd);
document.getElementById('zoomValue').textContent = 'Zoom: '+Math.round(zoom*100)+' %';
renderPlayhead(); renderPlayhead();
} }
function renderPlayhead() { function renderPlayhead() {
const t = Math.max(0, Math.min(duration, audio.currentTime || 0)); const t = Math.max(0, Math.min(duration, audio.currentTime || 0));
playhead.style.left = ((t/duration)*100)+'%'; playhead.style.left = timeToPercent(t)+'%';
} }
function setZoom(newZoom, clientX) {
newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom));
if (newZoom === zoom) return;
const r = wave.getBoundingClientRect();
let focusRatio = 0.5;
if (typeof clientX === 'number' && r.width > 0) {
focusRatio = Math.max(0, Math.min(1, (clientX-r.left)/r.width));
}
const oldSpan = viewEnd-viewStart;
const focusTime = viewStart + focusRatio*oldSpan;
const newSpan = duration/newZoom;
let newStart = focusTime-focusRatio*newSpan;
newStart = Math.max(0, Math.min(duration-newSpan, newStart));
zoom = newZoom;
viewStart = newStart;
viewEnd = newStart+newSpan;
render();
}
function resetZoom() {
zoom = 1;
viewStart = 0;
viewEnd = duration;
render();
}
wave.addEventListener('wheel', e => {
e.preventDefault();
const factor = e.deltaY < 0 ? ZOOM_FACTOR : 1/ZOOM_FACTOR;
setZoom(zoom*factor, e.clientX);
}, {passive:false});
function beginDrag(which, e) { function beginDrag(which, e) {
e.preventDefault(); e.preventDefault();
dragging = which; dragging = which;