Add timecode input field

This commit is contained in:
Ruben van de Ven 2022-04-19 13:29:26 +02:00
parent 6d7a223a69
commit 438c09a48f
3 changed files with 117 additions and 28 deletions

View file

@ -4,4 +4,6 @@ Create a hand drawn vector animation.
```bash ```bash
poetry run python webserver.py poetry run python webserver.py
``` ```
`parse_offsets.py` can be used to pad the diagram in order to sync it with the audio. This is necessary eg. after a network failure. It works by adding a line with the required offset to the `.json_appendable`-file.

View file

@ -72,14 +72,25 @@
} }
.controls--playback{
/* display:flex; */
}
input[type='range'] { .timecode{
position: absolute;
right: 100%;
width: 5%;
font-size:8px;
}
.controls--playback input[type='range'] {
/* position: absolute; /* position: absolute;
z-index: 100; z-index: 100;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; */ right: 0; */
width: 100%; width: 100%;
} }
.controls button.paused, .controls button.playing{ .controls button.paused, .controls button.playing{
@ -234,6 +245,11 @@
.noUi-horizontal .noUi-touch-area { .noUi-horizontal .noUi-touch-area {
cursor: ew-resize; cursor: ew-resize;
} }
#interface .noUi-horizontal .noUi-tooltip{
/* tooltips go below the buttons */
bottom:auto;
top:110%;
}
.audioconfig{ .audioconfig{
z-index: 9; z-index: 9;
@ -286,6 +302,15 @@
} else { } else {
const playlist = new Playlist(document.getElementById("interface"), '/files/'); const playlist = new Playlist(document.getElementById("interface"), '/files/');
} }
// Hack to disable hardware media keys starting/stopping the audio playback
navigator.mediaSession.setActionHandler('play', function() { /* Code excerpted. */ });
navigator.mediaSession.setActionHandler('pause', function() { /* Code excerpted. */ });
navigator.mediaSession.setActionHandler('seekbackward', function() { /* Code excerpted. */ });
navigator.mediaSession.setActionHandler('seekforward', function() { /* Code excerpted. */ });
navigator.mediaSession.setActionHandler('previoustrack', function() { /* Code excerpted. */ });
navigator.mediaSession.setActionHandler('nexttrack', function() { /* Code excerpted. */ });
</script> </script>
</body> </body>

View file

@ -113,6 +113,33 @@ class Annotator extends EventTarget {
constructor(wrapperEl, tags, fileurl) { constructor(wrapperEl, tags, fileurl) {
super(); super();
this.formatter = wNumb({
decimals: 2,
edit: (time) => {
let neg = "";
if (time < 0) {
neg = "-";
time *= -1;
}
const s = Math.floor(time / 1000);
const minutes = Math.floor(s / 60);
const seconds = s - minutes * 60;
const ms = Math.floor((time / 1000 - s) * 1000);
return `${neg}${minutes}:${seconds}.${ms}`;
},
undo: (tc) => {
let [rest, ms] = tc.split(/[\.\,]/);
ms = parseFloat(typeof ms === "undefined" ? 0 : ms);
let factor = 1000;
rest.split(':').reverse().forEach((v, i) => {
ms += v * factor;
factor *= 60;
});
return `${ms}`;
}
});
this.wrapperEl = wrapperEl; this.wrapperEl = wrapperEl;
this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.wrapperEl.appendChild(this.svgEl); this.wrapperEl.appendChild(this.svgEl);
@ -122,19 +149,29 @@ class Annotator extends EventTarget {
this.controlsEl.classList.add('controls') this.controlsEl.classList.add('controls')
this.wrapperEl.appendChild(this.controlsEl); this.wrapperEl.appendChild(this.controlsEl);
this.playbackControlsEl = document.createElement('div');
this.playbackControlsEl.classList.add('controls--playback')
this.controlsEl.appendChild(this.playbackControlsEl);
this.playheadEl = document.createElement('input'); this.playheadEl = document.createElement('input');
this.playheadEl.type = "range"; this.playheadEl.type = "range";
this.playheadEl.min = 0; this.playheadEl.min = 0;
this.playheadEl.step = 0.01; this.playheadEl.step = 0.01;
this.controlsEl.appendChild(this.playheadEl); this.playbackControlsEl.appendChild(this.playheadEl);
this.playheadEl.addEventListener("input", (ev) => { this.playheadEl.addEventListener("input", (ev) => {
this.scrubTo(ev.target.value); this.scrubTo(ev.target.value);
}) })
this.timeCodeEl = document.createElement('input');
this.timeCodeEl.type = 'numeric';
this.timeCodeEl.classList.add('timecode');
this.timeCodeEl.disabled = true;
this.playbackControlsEl.appendChild(this.timeCodeEl);
this.playPauseEl = document.createElement('button'); this.playPauseEl = document.createElement('button');
this.playPauseEl.classList.add('paused'); this.playPauseEl.classList.add('paused');
this.controlsEl.appendChild(this.playPauseEl); this.playbackControlsEl.appendChild(this.playPauseEl);
this.playPauseEl.addEventListener("click", (ev) => { this.playPauseEl.addEventListener("click", (ev) => {
this.playPause() this.playPause()
@ -397,7 +434,7 @@ class Annotator extends EventTarget {
this.updateAnnotations(true); this.updateAnnotations(true);
this._currentTimeMs = t_out; this._currentTimeMs = t_out;
this.playheadEl.value = this._currentTimeMs; this._updatePlayhead();
this.setUpAnnotator(); this.setUpAnnotator();
} }
} }
@ -406,7 +443,7 @@ class Annotator extends EventTarget {
setUpAnnotator() { setUpAnnotator() {
this.playheadEl.min = this.audioOffset < 0 ? this.audioOffset * 1000 : 0; this.playheadEl.min = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
this.playheadEl.max = this.getEndTimeMs(); this.playheadEl.max = this.getEndTimeMs();
this.playheadEl.value = this._currentTimeMs; this._updatePlayhead();
this.inPointPosition = this.findPositionForTime(this.currentTime); this.inPointPosition = this.findPositionForTime(this.currentTime);
this.inPointTimeMs = this._currentTimeMs; this.inPointTimeMs = this._currentTimeMs;
@ -418,13 +455,16 @@ class Annotator extends EventTarget {
} }
// console.log(this._currentTimeMs, ) // console.log(this._currentTimeMs, )
const sliderMin = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
const sliderMax = this.getEndTimeMs();
this.slider = noUiSlider.create(this.scrubberEl, { this.slider = noUiSlider.create(this.scrubberEl, {
start: [this._currentTimeMs, this.getEndTimeMs()], start: [this._currentTimeMs, this.getEndTimeMs()],
connect: true, connect: true,
range: { range: {
'min': this.audioOffset < 0 ? this.audioOffset * 1000 : 0, 'min': sliderMin,
'max': this.getEndTimeMs(), 'max': sliderMax,
}, },
keyboardDefaultStep: (sliderMax-sliderMin) / 1000,
tooltips: [ tooltips: [
this.formatter, this.formatter,
this.formatter this.formatter
@ -460,6 +500,36 @@ class Annotator extends EventTarget {
// this.playAudioSegment(values[0], values[1]); // this.playAudioSegment(values[0], values[1]);
}); });
this.slider.getTooltips().forEach((ttEl, i) => {
// console.log(ttEl, i);
ttEl.addEventListener('click', (e) => {
let ttInputEl = document.createElement('input');
ttInputEl.value = ttEl.innerHTML
ttEl.innerHTML = "";
ttEl.appendChild(ttInputEl);
ttInputEl.focus();
const submit = () => {
console.log(ttInputEl.value);
const tcMs = this.formatter.from(ttInputEl.value);
let points = this.slider.get();
points[i] = tcMs;
console.log(points);
this.slider.set(points);
};
ttInputEl.addEventListener('keydown', (keyE) => {
keyE.stopPropagation(); //prevent movement of tooltip
if(keyE.key == "Enter") {
submit();
}
})
ttInputEl.addEventListener('click', (clickE) => {
clickE.stopPropagation(); //prevent retrigger on selectino
})
ttInputEl.addEventListener('blur', submit);
});
})
this.drawStrokePosition(this.inPointPosition, this.outPointPosition); this.drawStrokePosition(this.inPointPosition, this.outPointPosition);
} }
@ -470,7 +540,7 @@ class Annotator extends EventTarget {
this.audioFile = metadata.hasOwnProperty('audio') ? metadata.audio.file : null; this.audioFile = metadata.hasOwnProperty('audio') ? metadata.audio.file : null;
this.audioOffset = metadata.hasOwnProperty('audio') ? Number.parseFloat(metadata.audio.offset) : 0; this.audioOffset = metadata.hasOwnProperty('audio') ? Number.parseFloat(metadata.audio.offset) : 0;
this._currentTimeMs = this.audioOffset < 0 ? this.audioOffset * 1000 : 0; this._currentTimeMs = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
this.playheadEl.value = this._currentTimeMs; this._updatePlayhead();
// //
// load any saved metadata // load any saved metadata
} }
@ -498,21 +568,6 @@ class Annotator extends EventTarget {
this.nextViewboxTimeout = null; this.nextViewboxTimeout = null;
this._setPausedFlag(true); this._setPausedFlag(true);
this.formatter = wNumb({
decimals: 2,
edit: (time) => {
let neg = "";
if (time < 0) {
neg = "-";
time *= -1;
}
const s = Math.floor(time / 1000);
const minutes = Math.floor(s / 60);
const seconds = s - minutes * 60;
const ms = Math.floor((time / 1000 - s) * 1000);
return `${neg}${minutes}:${seconds}:${ms}`;
}
});
this.setupAudioConfig().then(() => { this.setupAudioConfig().then(() => {
@ -910,6 +965,12 @@ class Annotator extends EventTarget {
return this._paused; return this._paused;
} }
_updatePlayhead() {
this.playheadEl.value = this._currentTimeMs;
this.timeCodeEl.value = this.formatter.to(this._currentTimeMs);
}
_animationFrame(timestamp) { _animationFrame(timestamp) {
// TODO, move time at end of playStrokePosition to here // TODO, move time at end of playStrokePosition to here
const nextTime = window.performance.now() - this.startTimeMs; const nextTime = window.performance.now() - this.startTimeMs;
@ -921,7 +982,7 @@ class Annotator extends EventTarget {
} else { } else {
this._currentTimeMs = nextTime; this._currentTimeMs = nextTime;
} }
this.playheadEl.value = this._currentTimeMs; this._updatePlayhead();
if (!interrupt && (this.videoIsPlaying || this.audioIsPlaying)) { if (!interrupt && (this.videoIsPlaying || this.audioIsPlaying)) {
window.requestAnimationFrame((timestamp) => this._animationFrame(timestamp)); window.requestAnimationFrame((timestamp) => this._animationFrame(timestamp));
} else { } else {
@ -959,7 +1020,7 @@ class Annotator extends EventTarget {
this.dispatchEvent(new CustomEvent('seeking', {})); this.dispatchEvent(new CustomEvent('seeking', {}));
this._currentTimeMs = this.strokes[point[0]].points[point[1]][2]; this._currentTimeMs = this.strokes[point[0]].points[point[1]][2];
[this.currentPathI, this.currentPointI] = point; [this.currentPathI, this.currentPointI] = point;
this.playheadEl.value = this._currentTimeMs; this._updatePlayhead();
this._updateFrame(); this._updateFrame();
// TODO set audio, wait for promise to finish // TODO set audio, wait for promise to finish
this.dispatchEvent(new CustomEvent('seeked', {})); this.dispatchEvent(new CustomEvent('seeked', {}));
@ -973,7 +1034,7 @@ class Annotator extends EventTarget {
this._currentTimeMs = Number.parseFloat(time) * 1000; this._currentTimeMs = Number.parseFloat(time) * 1000;
[this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs); [this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs);
this.playheadEl.value = this._currentTimeMs; this._updatePlayhead();
this._updateFrame(); this._updateFrame();
this.dispatchEvent(new CustomEvent('seeked', { detail: this.currentTime })); this.dispatchEvent(new CustomEvent('seeked', { detail: this.currentTime }));
} }
@ -1063,4 +1124,5 @@ class Annotator extends EventTarget {
} }
} }