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; }
-

+
- 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;