From 438c09a48f522ff8e4c3de76d2005b08228bac78 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Tue, 19 Apr 2022 13:29:26 +0200 Subject: [PATCH] Add timecode input field --- README.md | 4 +- app/www/annotate.html | 27 +++++++++- app/www/annotate.js | 114 ++++++++++++++++++++++++++++++++---------- 3 files changed, 117 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 00ddcae..ab67a4e 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,6 @@ Create a hand drawn vector animation. ```bash poetry run python webserver.py -``` \ No newline at end of file +``` + +`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. \ No newline at end of file diff --git a/app/www/annotate.html b/app/www/annotate.html index 045f46d..bf3558e 100644 --- a/app/www/annotate.html +++ b/app/www/annotate.html @@ -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. */ }); diff --git a/app/www/annotate.js b/app/www/annotate.js index 0c1c208..5f9a213 100644 --- a/app/www/annotate.js +++ b/app/www/annotate.js @@ -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 { } -} \ No newline at end of file +} +