Add timecode input field
This commit is contained in:
parent
6d7a223a69
commit
438c09a48f
3 changed files with 117 additions and 28 deletions
|
@ -5,3 +5,5 @@ 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.
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 }));
|
||||||
}
|
}
|
||||||
|
@ -1064,3 +1125,4 @@ class Annotator extends EventTarget {
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue