diff --git a/editor.py b/editor.py index 72af350..d7dd325 100644 --- a/editor.py +++ b/editor.py @@ -283,7 +283,7 @@ h1 { margin:0 0 6px; font-size:28px; } .subtitle { color:#aaa; margin-bottom:22px; } .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 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; } .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; } @@ -299,6 +299,7 @@ button.primary { background:#1769aa; } button.danger { background:#8b0000; } button:disabled { opacity:.45; cursor:not-allowed; } .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; } .value { background:#222; border-radius:6px; padding:12px; } .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; } .editor { padding:12px; } .wave-wrap { height:180px; } + .zoom { width:100%; margin-left:0; } .values { grid-template-columns:1fr; } .position { width:100%; margin-left:0; } button, a.button { flex:1 1 auto; } @@ -327,15 +329,15 @@ button:disabled { opacity:.45; cursor:not-allowed; }
- Wellenform + Wellenform
- 00:00:00 - {{ duration }} + 00:00:00 + {{ duration }}
@@ -375,7 +379,14 @@ const filename = {{ filename|tojson }}; let start = 0; let end = duration; 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 waveImage = document.getElementById('waveImage'); const startHandle = document.getElementById('startHandle'); const endHandle = document.getElementById('endHandle'); const selection = document.getElementById('selection'); @@ -391,24 +402,70 @@ function fmt(sec) { } function xToTime(clientX) { 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() { - const a = (start/duration)*100; - const b = (end/duration)*100; + const a = timeToPercent(start); + const b = timeToPercent(end); startHandle.style.left = a+'%'; endHandle.style.left = b+'%'; 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('endValue').textContent = fmt(end); 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(); } function renderPlayhead() { 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) { e.preventDefault(); dragging = which;