Playlist: scrub/seek playback by clicking-dragging the waveform
Pointer on the waveform now seeks: scrub the playhead on the playing track (new 'seek' output message setting currentTime) or start a stopped track from the clicked point (playIndex(i, fromTime)). Grabbing within ~10px of the green/ red handles still adjusts the trim in/out instead of seeking. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
882dd4643a
commit
26a5000b04
3 files changed
+52
-17
No files matched your search
+1
-1
@@ -80,7 +80,7 @@
|
|||||||
<input type="file" id="tracks-input" accept="audio/*,video/*" multiple hidden />
|
<input type="file" id="tracks-input" accept="audio/*,video/*" multiple hidden />
|
||||||
<input type="file" id="scene-img-input" accept="image/*" hidden />
|
<input type="file" id="scene-img-input" accept="image/*" hidden />
|
||||||
<ol id="playlist" class="playlist"></ol>
|
<ol id="playlist" class="playlist"></ol>
|
||||||
<div class="hint-mini">Clic sul <b>nome</b> = avvia/riavvia · ▶/⏸ pausa · ⠿ riordina · ⌨ tasto rapido · 🎬 <b>scena</b> (effetto/testo/immagine, ognuno col proprio tempo) · ✨ <b>Intermezzo</b> = voce solo-effetti (niente audio) tra un brano e l'altro</div>
|
<div class="hint-mini">Clic sul <b>nome</b> = avvia/riavvia · ▶/⏸ pausa · ⠿ riordina · ⌨ tasto rapido · 🎬 <b>scena</b> · ✨ <b>Intermezzo</b> = solo-effetti tra i brani · sulla <b>forma d'onda</b>: clic/trascina = sposta la riproduzione, maniglie 🟢🔴 = inizio/fine</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="span2">
|
<section class="span2">
|
||||||
|
|||||||
+43
-16
@@ -276,31 +276,56 @@ function wireTrim(li, canvas, tr, i) {
|
|||||||
li.querySelector('.trim-here-e').addEventListener('click', () => { if (i === currentIndex) { tr.end = Math.max(0.5, playCur); commitTrim(tr, i, true); } });
|
li.querySelector('.trim-here-e').addEventListener('click', () => { if (i === currentIndex) { tr.end = Math.max(0.5, playCur); commitTrim(tr, i, true); } });
|
||||||
li.querySelector('.trim-reset').addEventListener('click', () => { tr.start = 0; tr.end = 0; commitTrim(tr, i, true); });
|
li.querySelector('.trim-reset').addEventListener('click', () => { tr.start = 0; tr.end = 0; commitTrim(tr, i, true); });
|
||||||
|
|
||||||
let dragH = null;
|
// Pointer on the waveform: grab a trim handle if near one, otherwise SEEK —
|
||||||
|
// scrub the playhead on the current track, or start a stopped track from there.
|
||||||
|
const HANDLE_PX = 10;
|
||||||
|
let mode = null;
|
||||||
const dur = () => durations[tr.path] || (i === currentIndex ? playDur : 0) || 0;
|
const dur = () => durations[tr.path] || (i === currentIndex ? playDur : 0) || 0;
|
||||||
const timeAt = (e) => {
|
const xToTime = (e) => {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const f = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
return Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) * dur();
|
||||||
return f * dur();
|
};
|
||||||
|
const scrub = (t) => {
|
||||||
|
if (i !== currentIndex) return; // preview seek only for the playing track
|
||||||
|
playCur = t;
|
||||||
|
send({ type: 'seek', time: t });
|
||||||
|
drawWave(canvas, tr, true);
|
||||||
};
|
};
|
||||||
canvas.addEventListener('pointerdown', (e) => {
|
canvas.addEventListener('pointerdown', (e) => {
|
||||||
if (!dur()) return;
|
const d = dur();
|
||||||
const t = timeAt(e), s = tr.start || 0, en = tr.end > 0 ? tr.end : dur();
|
const rect = canvas.getBoundingClientRect();
|
||||||
dragH = Math.abs(t - s) <= Math.abs(t - en) ? 'start' : 'end';
|
const x = e.clientX - rect.left;
|
||||||
|
const sx = d ? (tr.start || 0) / d * rect.width : 0;
|
||||||
|
const ex = d ? (tr.end > 0 ? tr.end : d) / d * rect.width : rect.width;
|
||||||
|
if (d && Math.abs(x - sx) <= HANDLE_PX) mode = 'start';
|
||||||
|
else if (d && Math.abs(x - ex) <= HANDLE_PX) mode = 'end';
|
||||||
|
else mode = 'seek';
|
||||||
canvas.setPointerCapture(e.pointerId); e.preventDefault();
|
canvas.setPointerCapture(e.pointerId); e.preventDefault();
|
||||||
|
if (mode === 'seek') scrub(xToTime(e));
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointermove', (e) => {
|
canvas.addEventListener('pointermove', (e) => {
|
||||||
if (!dragH) return;
|
if (!mode) return;
|
||||||
const t = timeAt(e);
|
const t = xToTime(e);
|
||||||
if (dragH === 'start') tr.start = Math.max(0, Math.min(t, (tr.end > 0 ? tr.end : dur()) - 0.5));
|
if (mode === 'start') { tr.start = Math.max(0, Math.min(t, (tr.end > 0 ? tr.end : dur()) - 0.5)); }
|
||||||
else tr.end = Math.min(dur(), Math.max(t, (tr.start || 0) + 0.5));
|
else if (mode === 'end') { tr.end = Math.min(dur(), Math.max(t, (tr.start || 0) + 0.5)); }
|
||||||
|
else { scrub(t); return; }
|
||||||
drawWave(canvas, tr, i === currentIndex);
|
drawWave(canvas, tr, i === currentIndex);
|
||||||
sIn.value = fmtTime(Math.floor(tr.start || 0));
|
sIn.value = fmtTime(Math.floor(tr.start || 0));
|
||||||
eIn.value = tr.end > 0 ? fmtTime(Math.floor(tr.end)) : '';
|
eIn.value = tr.end > 0 ? fmtTime(Math.floor(tr.end)) : '';
|
||||||
});
|
});
|
||||||
const endDrag = () => { if (dragH) { dragH = null; commitTrim(tr, i, true); } };
|
const endDrag = (e) => {
|
||||||
|
if (!mode) return;
|
||||||
|
if (mode === 'seek') {
|
||||||
|
const t = xToTime(e);
|
||||||
|
if (i === currentIndex) scrub(t);
|
||||||
|
else playIndex(i, t); // start a stopped track from the clicked point
|
||||||
|
} else {
|
||||||
|
commitTrim(tr, i, true);
|
||||||
|
}
|
||||||
|
mode = null;
|
||||||
|
};
|
||||||
canvas.addEventListener('pointerup', endDrag);
|
canvas.addEventListener('pointerup', endDrag);
|
||||||
canvas.addEventListener('pointercancel', endDrag);
|
canvas.addEventListener('pointercancel', () => { mode = null; });
|
||||||
}
|
}
|
||||||
let padCrossfadeMs = 0; // crossfade duration for pad triggers
|
let padCrossfadeMs = 0; // crossfade duration for pad triggers
|
||||||
let capturingFor = -1; // index of the track awaiting a hotkey, or -1
|
let capturingFor = -1; // index of the track awaiting a hotkey, or -1
|
||||||
@@ -477,7 +502,7 @@ function addInterlude() {
|
|||||||
savePlaylistState();
|
savePlaylistState();
|
||||||
}
|
}
|
||||||
|
|
||||||
function playIndex(i) {
|
function playIndex(i, fromTime) {
|
||||||
if (i < 0 || i >= playlist.length) return;
|
if (i < 0 || i >= playlist.length) return;
|
||||||
clearGap();
|
clearGap();
|
||||||
const tr = playlist[i];
|
const tr = playlist[i];
|
||||||
@@ -492,7 +517,9 @@ function playIndex(i) {
|
|||||||
playInterlude(tr);
|
playInterlude(tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
send({ type: tr.isVideo ? 'playVideoTrack' : 'playTrack', path: tr.path, start: tr.start || 0, end: tr.end || 0 });
|
// fromTime (set by clicking the waveform) overrides the trim start for this run.
|
||||||
|
const startAt = (fromTime != null) ? Math.max(0, fromTime) : (tr.start || 0);
|
||||||
|
send({ type: tr.isVideo ? 'playVideoTrack' : 'playTrack', path: tr.path, start: startAt, end: tr.end || 0 });
|
||||||
startCues(tr);
|
startCues(tr);
|
||||||
renderPlaylist();
|
renderPlaylist();
|
||||||
}
|
}
|
||||||
@@ -783,7 +810,7 @@ function renderPlaylist() {
|
|||||||
'<span class="tt-tot" title="Durata totale">⏱ ' + fmtTime(Math.floor(total)) + '</span>' +
|
'<span class="tt-tot" title="Durata totale">⏱ ' + fmtTime(Math.floor(total)) + '</span>' +
|
||||||
'<span class="tt-rem" title="Rimasto">⧗ -' + fmtTime(Math.ceil(remaining)) + '</span>' +
|
'<span class="tt-rem" title="Rimasto">⧗ -' + fmtTime(Math.ceil(remaining)) + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<canvas class="wave" width="' + WAVE_BUCKETS + '" height="46"></canvas>' +
|
'<canvas class="wave" width="' + WAVE_BUCKETS + '" height="46" title="Clic/trascina = sposta il punto di riproduzione · maniglie verde/rossa = inizio/fine"></canvas>' +
|
||||||
'<div class="track-trim">' +
|
'<div class="track-trim">' +
|
||||||
'<span class="trim-lbl trim-lbl-s">Inizio</span>' +
|
'<span class="trim-lbl trim-lbl-s">Inizio</span>' +
|
||||||
'<input class="trim-start" type="text" value="' + fmtTime(Math.floor(tStart)) + '" title="Punto di inizio (mm:ss)" />' +
|
'<input class="trim-start" type="text" value="' + fmtTime(Math.floor(tStart)) + '" title="Punto di inizio (mm:ss)" />' +
|
||||||
|
|||||||
@@ -359,6 +359,14 @@ djv.onControl(async (m) => {
|
|||||||
} catch (e) { djv.report({ type: 'error', message: e.message }); }
|
} catch (e) { djv.report({ type: 'error', message: e.message }); }
|
||||||
break;
|
break;
|
||||||
case 'setTrim': audio.setTrim(m.start || 0, m.end || 0); break;
|
case 'setTrim': audio.setTrim(m.start || 0, m.end || 0); break;
|
||||||
|
case 'seek': {
|
||||||
|
const el = audio.mediaEl;
|
||||||
|
if (el && isFinite(el.duration) && el.duration > 0) {
|
||||||
|
el.currentTime = Math.max(0, Math.min(el.duration - 0.05, m.time || 0));
|
||||||
|
audio._trimFired = false; // re-arm the trim-end check after seeking
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'probeDurations': probeDurations(m.paths || []); break;
|
case 'probeDurations': probeDurations(m.paths || []); break;
|
||||||
case 'togglePlay': {
|
case 'togglePlay': {
|
||||||
const playing = audio.togglePlay();
|
const playing = audio.togglePlay();
|
||||||
|
|||||||
Reference in new issue
Block a user