Add timecode input field
This commit is contained in:
parent
6d7a223a69
commit
438c09a48f
3 changed files with 117 additions and 28 deletions
|
@ -4,4 +4,6 @@ Create a hand drawn vector animation.
|
|||
|
||||
```bash
|
||||
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.
|
|
@ -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;
|
||||
z-index: 100;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0; */
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.controls button.paused, .controls button.playing{
|
||||
|
@ -234,6 +245,11 @@
|
|||
.noUi-horizontal .noUi-touch-area {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
#interface .noUi-horizontal .noUi-tooltip{
|
||||
/* tooltips go below the buttons */
|
||||
bottom:auto;
|
||||
top:110%;
|
||||
}
|
||||
|
||||
.audioconfig{
|
||||
z-index: 9;
|
||||
|
@ -286,6 +302,15 @@
|
|||
} else {
|
||||
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>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -113,6 +113,33 @@ class Annotator extends EventTarget {
|
|||
constructor(wrapperEl, tags, fileurl) {
|
||||
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.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
this.wrapperEl.appendChild(this.svgEl);
|
||||
|
@ -122,19 +149,29 @@ class Annotator extends EventTarget {
|
|||
this.controlsEl.classList.add('controls')
|
||||
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.type = "range";
|
||||
this.playheadEl.min = 0;
|
||||
this.playheadEl.step = 0.01;
|
||||
this.controlsEl.appendChild(this.playheadEl);
|
||||
this.playbackControlsEl.appendChild(this.playheadEl);
|
||||
|
||||
this.playheadEl.addEventListener("input", (ev) => {
|
||||
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.classList.add('paused');
|
||||
this.controlsEl.appendChild(this.playPauseEl);
|
||||
this.playbackControlsEl.appendChild(this.playPauseEl);
|
||||
|
||||
this.playPauseEl.addEventListener("click", (ev) => {
|
||||
this.playPause()
|
||||
|
@ -397,7 +434,7 @@ class Annotator extends EventTarget {
|
|||
this.updateAnnotations(true);
|
||||
|
||||
this._currentTimeMs = t_out;
|
||||
this.playheadEl.value = this._currentTimeMs;
|
||||
this._updatePlayhead();
|
||||
this.setUpAnnotator();
|
||||
}
|
||||
}
|
||||
|
@ -406,7 +443,7 @@ class Annotator extends EventTarget {
|
|||
setUpAnnotator() {
|
||||
this.playheadEl.min = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
|
||||
this.playheadEl.max = this.getEndTimeMs();
|
||||
this.playheadEl.value = this._currentTimeMs;
|
||||
this._updatePlayhead();
|
||||
|
||||
this.inPointPosition = this.findPositionForTime(this.currentTime);
|
||||
this.inPointTimeMs = this._currentTimeMs;
|
||||
|
@ -418,13 +455,16 @@ class Annotator extends EventTarget {
|
|||
}
|
||||
|
||||
// console.log(this._currentTimeMs, )
|
||||
const sliderMin = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
|
||||
const sliderMax = this.getEndTimeMs();
|
||||
this.slider = noUiSlider.create(this.scrubberEl, {
|
||||
start: [this._currentTimeMs, this.getEndTimeMs()],
|
||||
connect: true,
|
||||
range: {
|
||||
'min': this.audioOffset < 0 ? this.audioOffset * 1000 : 0,
|
||||
'max': this.getEndTimeMs(),
|
||||
'min': sliderMin,
|
||||
'max': sliderMax,
|
||||
},
|
||||
keyboardDefaultStep: (sliderMax-sliderMin) / 1000,
|
||||
tooltips: [
|
||||
this.formatter,
|
||||
this.formatter
|
||||
|
@ -460,6 +500,36 @@ class Annotator extends EventTarget {
|
|||
// 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);
|
||||
}
|
||||
|
||||
|
@ -470,7 +540,7 @@ class Annotator extends EventTarget {
|
|||
this.audioFile = metadata.hasOwnProperty('audio') ? metadata.audio.file : null;
|
||||
this.audioOffset = metadata.hasOwnProperty('audio') ? Number.parseFloat(metadata.audio.offset) : 0;
|
||||
this._currentTimeMs = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
|
||||
this.playheadEl.value = this._currentTimeMs;
|
||||
this._updatePlayhead();
|
||||
//
|
||||
// load any saved metadata
|
||||
}
|
||||
|
@ -498,21 +568,6 @@ class Annotator extends EventTarget {
|
|||
this.nextViewboxTimeout = null;
|
||||
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(() => {
|
||||
|
@ -910,6 +965,12 @@ class Annotator extends EventTarget {
|
|||
return this._paused;
|
||||
}
|
||||
|
||||
_updatePlayhead() {
|
||||
this.playheadEl.value = this._currentTimeMs;
|
||||
this.timeCodeEl.value = this.formatter.to(this._currentTimeMs);
|
||||
}
|
||||
|
||||
|
||||
_animationFrame(timestamp) {
|
||||
// TODO, move time at end of playStrokePosition to here
|
||||
const nextTime = window.performance.now() - this.startTimeMs;
|
||||
|
@ -921,7 +982,7 @@ class Annotator extends EventTarget {
|
|||
} else {
|
||||
this._currentTimeMs = nextTime;
|
||||
}
|
||||
this.playheadEl.value = this._currentTimeMs;
|
||||
this._updatePlayhead();
|
||||
if (!interrupt && (this.videoIsPlaying || this.audioIsPlaying)) {
|
||||
window.requestAnimationFrame((timestamp) => this._animationFrame(timestamp));
|
||||
} else {
|
||||
|
@ -959,7 +1020,7 @@ class Annotator extends EventTarget {
|
|||
this.dispatchEvent(new CustomEvent('seeking', {}));
|
||||
this._currentTimeMs = this.strokes[point[0]].points[point[1]][2];
|
||||
[this.currentPathI, this.currentPointI] = point;
|
||||
this.playheadEl.value = this._currentTimeMs;
|
||||
this._updatePlayhead();
|
||||
this._updateFrame();
|
||||
// TODO set audio, wait for promise to finish
|
||||
this.dispatchEvent(new CustomEvent('seeked', {}));
|
||||
|
@ -973,7 +1034,7 @@ class Annotator extends EventTarget {
|
|||
this._currentTimeMs = Number.parseFloat(time) * 1000;
|
||||
[this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs);
|
||||
|
||||
this.playheadEl.value = this._currentTimeMs;
|
||||
this._updatePlayhead();
|
||||
this._updateFrame();
|
||||
this.dispatchEvent(new CustomEvent('seeked', { detail: this.currentTime }));
|
||||
}
|
||||
|
@ -1063,4 +1124,5 @@ class Annotator extends EventTarget {
|
|||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue