diff --git a/app/www/annotate.js b/app/www/annotate.js index 3ec0e98..499bd34 100644 --- a/app/www/annotate.js +++ b/app/www/annotate.js @@ -130,10 +130,11 @@ class Annotator extends EventTarget { 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}`; + const minutes = String(Math.floor(s / 60)).padStart(2, '0'); + const seconds = String(s - minutes * 60).padStart(2, '0'); + // show miliseconds only in annotator + const ms = !this.config.is_player ? "." + String(Math.floor((time / 1000 - s) * 1000)).padStart(3, '0') : ""; + return `${neg}${minutes}:${seconds}${ms}`; }, undo: (tc) => { let [rest, ms] = tc.split(/[\.\,]/); @@ -143,7 +144,7 @@ class Annotator extends EventTarget { ms += v * factor; factor *= 60; }); - return `${ms}`; + return `${ ms } `; } }); @@ -192,12 +193,12 @@ class Annotator extends EventTarget { ev.preventDefault(); // we don't want to spacebar, as this is captured in the overall keydown event }) - this.scrubberEl = document.createElement('div'); - this.scrubberEl.classList.add('scrubber') - this.controlsEl.appendChild(this.scrubberEl); - - if(!this.config.is_player){ + this.scrubberEl = document.createElement('div'); + this.scrubberEl.classList.add('scrubber') + this.controlsEl.appendChild(this.scrubberEl); + + this.annotationsEl = document.createElement('div'); this.annotationsEl.classList.add('annotations') this.controlsEl.appendChild(this.annotationsEl); @@ -339,7 +340,7 @@ class Annotator extends EventTarget { if (this.selectedAnnotationI == annotation_i) { this.annotationEl.classList.add('selected'); } - this.annotationEl.title = `[${annotation.tag}] ${annotation.comment}`; + this.annotationEl.title = `[${ annotation.tag }] ${ annotation.comment } `; this.annotationEl.addEventListener('mouseover', (e) => { @@ -425,7 +426,7 @@ class Annotator extends EventTarget { // draw full stroke of annotation console.debug('setInOut'); this.drawStrokePosition(this.inPointPosition, this.outPointPosition); - console.debug([`${this.inPointTimeMs}`, `${this.outPointTimeMs}`]) + console.debug([`${ this.inPointTimeMs } `, `${ this.outPointTimeMs } `]) this.slider.set([this.inPointTimeMs, this.outPointTimeMs]); // console.debug(this.selectedAnnotation); @@ -460,7 +461,7 @@ class Annotator extends EventTarget { .then(data => { if (!this.config.is_player) { - const metadata_req = new Request(`/annotations/${data.file}`, { + const metadata_req = new Request(`/ annotations / ${ data.file } `, { method: 'GET', }); return fetch(metadata_req) @@ -1032,7 +1033,7 @@ class Annotator extends EventTarget { // } // for (let index = 0; index < inPath_i; index++) { - // const pathEl = this.svgEl.querySelector(`.path${index}`); + // const pathEl = this.svgEl.querySelector(`.path${ index } `); // if (pathEl) { // pathEl.classList.add('before_in'); // } @@ -1044,7 +1045,7 @@ class Annotator extends EventTarget { // const path = this.strokes[path_i]; // // console.log(path); - // let pathEl = this.svgEl.querySelector(`.path${path_i}`); + // let pathEl = this.svgEl.querySelector(`.path${ path_i } `); // if (!pathEl) { // pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // pathEl.style.stroke = path.color; @@ -1072,7 +1073,7 @@ class Annotator extends EventTarget { updateViewbox() { if (this.config.crop_to_fit) { - this.svgEl.setAttribute('viewBox', `${this.bounding_box.x} ${this.bounding_box.y} ${this.bounding_box.width} ${this.bounding_box.height}`); + this.svgEl.setAttribute('viewBox', `${ this.bounding_box.x } ${ this.bounding_box.y } ${ this.bounding_box.width } ${ this.bounding_box.height } `); } else { let x,y,w,h; if(this.currentViewboxI !== null) { @@ -1086,15 +1087,19 @@ class Annotator extends EventTarget { w = this.dimensions[0], h = this.dimensions[1]; } - this.svgEl.setAttribute('viewBox', `${x} ${y} ${w} ${h}`); + this.svgEl.setAttribute('viewBox', `${ x } ${ y } ${ w } ${ h } `); } } - toggleCrop(){ - this.config.crop_to_fit = !this.config.crop_to_fit; + setCrop(crop_to_fit) { + this.config.crop_to_fit = Boolean(crop_to_fit); this.updateViewbox(); } + toggleCrop(){ + this.setCrop(!this.config.crop_to_fit); + } + getNextPosition(path_i, point_i) { const path = this.strokes[path_i]; let next_path, next_point; @@ -1446,3 +1451,226 @@ class Annotator extends EventTarget { } + +class AnnotationPlayer extends HTMLElement { + constructor() { + super(); + // We don't use constructor() because an element's attributes + // are unavailable until connected to the DOM. + + // attributes: + // - data-no-crop + // - autoplay + // - preload + // - data-poster-src + // - data-annotation-url + } + + connectedCallback() { + // Create a shadow root + this.attachShadow({ mode: "open" }); + + const imgEl = document.createElement('img'); + const playerEl = document.createElement('div'); + + const config = { + is_player: true, + crop_to_fit: this.hasAttribute('data-no-crop') ? false : true, + autoplay: true, + } + + + imgEl.src = this.getAttribute('data-poster-url'); + imgEl.addEventListener('click', () => { + imgEl.style.display = 'none'; + this.annotator = new Annotator( + playerEl, + null, //"tags.json", + this.getAttribute('data-annotation-url'), + config + ); + }) + + playerEl.classList.add('play'); + + const styleEl = document.createElement('style'); + styleEl.textContent = ` + :host{ + overflow: hidden; + padding: 10px; + + background: white; + } + + svg, img { + width: 100%; + height: 100%; + } + + .play:not(.loading) .controls { + visibility: hidden; + } + + :host(:hover) .controls { + visibility: visible !important; + } + + .controls--playback { + display:flex; + background: rgba(0,0,0,.5); + border-radius: 3px; + } + + .timecode { + width: 30px; + font-size: 8px; + background: none; + border: none; + color: white; + } + + + .controls--playback input[type='range'] { + flex-grow: 1; + -webkit-appearance: none; + background: none; + + } + + input[type="range"]::-webkit-slider-runnable-track, + input[type="range"]::-moz-range-track { + background: lightgray; + height: 5px; + border-radius: 3px; + } + + input[type="range"]::-moz-range-progress { + background-color: white; + height: 5px; + border-radius: 3px 0 0 3px; + } + + + input[type="range"]::-webkit-slider-thumb, + input[type="range"]::-moz-range-thumb { + -webkit-appearance: none; + height: 15px; + width: 15px; + background: white; + margin-top: -5px; + border-radius: 50%; + border:none; + } + + + + .controls button.paused, + .controls button.playing { + order: -1; + width: 30px; + height: 30px; + border: none; + background: none; + color: white; + line-height: 1; + } + + .controls button.paused::before { + content: '⏵'; + } + + .controls button.playing::before { + content: '⏸'; + } + + + .loading .controls button:is(.playing, .paused)::before { + content: '↺'; + display: inline-block; + animation: rotate 1s infinite; + } + + @keyframes rotate { + 0% { + transform: rotate(359deg) + } + + 100% { + transform: rotate(0deg) + } + } + + .controls { + position: absolute !important; + z-index: 100; + bottom: 10px; + left: 5%; + right: 0; + width: 90%; + } + svg .background { + fill: white + } + + path { + fill: none; + stroke: gray; + stroke-width: 1mm; + stroke-linecap: round; + } + + g.before path { + opacity: 0.5; + stroke: gray !important; + } + + g.after path, + path.before_in { + opacity: .1; + stroke: gray !important; + } + + .gray { + position: absolute; + background: rgba(255, 255, 255, 0.7); + } + + + `; + + this.shadowRoot.appendChild(styleEl); + this.shadowRoot.appendChild(imgEl); + this.shadowRoot.appendChild(playerEl); + + } + + setAnnotation(annotation) { + // this.annotation = annotation; + this.setAttribute('data-annotation-url', annotation.url) + this.setAttribute('data-poster-url', `/annotation/${annotation.id}.svg`) + } + + toggleCrop() { + if (this.hasAttribute('data-no-crop')) { + this.removeAttribute('data-no-crop'); + } else { + this.setAttribute('data-no-crop', true); + } + } + + attributeChangedCallback(name, oldValue, newValue) { + console.log(name, oldValue, newValue); + if(name == 'data-no-crop'){ + if(!this.annotator) { + return; + } + this.annotator.setCrop(!this.hasAttribute('data-no-crop')); + } + } + + // required for attributeChangedCallback() + static get observedAttributes() { return ['data-no-crop']; } + +} + +window.customElements.define('annotation-player', AnnotationPlayer); diff --git a/app/www/annotations.js b/app/www/annotations.js index f09b3de..a0ab6ae 100644 --- a/app/www/annotations.js +++ b/app/www/annotations.js @@ -1,161 +1,3 @@ -class AnnotationPlayer extends HTMLElement { - constructor() { - super(); - // We don't use constructor() because an element's attributes - // are unavailable until connected to the DOM. - } - - connectedCallback() { - // Create a shadow root - this.attachShadow({ mode: "open" }); - - const imgEl = document.createElement('img'); - const playerEl = document.createElement('div'); - - - imgEl.src = `/annotation/${this.annotation.id}.svg`; - imgEl.addEventListener('click', () => { - imgEl.style.display = 'none'; - new Annotator( - playerEl, - "tags.json", - this.annotation.url, - { is_player: true, crop_to_fit: true, autoplay: true } - ); - }) - - playerEl.classList.add('play'); - - const styleEl = document.createElement('style'); - styleEl.textContent = ` - :host{ - overflow: hidden; - padding: 10px; - - background: white; - } - - svg, img { - width: 100%; - height: 100%; - } - - .play:not(.loading) .controls { - visibility: hidden; - } - - :host(:hover) .controls { - visibility: visible !important; - } - - .controls--playback { - /* display:flex; */ - position: relative; - } - - .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 { - position: absolute; - left: 100%; - width: 30px; - height: 30px; - } - - .controls button.paused::before { - content: '⏵'; - } - - .controls button.playing::before { - content: '⏸'; - } - - - .loading .controls button:is(.playing, .paused)::before { - content: '↺'; - display: inline-block; - animation: rotate 1s infinite; - } - - @keyframes rotate { - 0% { - transform: rotate(359deg) - } - - 100% { - transform: rotate(0deg) - } - } - - .controls { - position: absolute !important; - z-index: 100; - bottom: 10px; - left: 5%; - right: 0; - width: 90%; - } - svg .background { - fill: white - } - - path { - fill: none; - stroke: gray; - stroke-width: 1mm; - stroke-linecap: round; - } - - g.before path { - opacity: 0.5; - stroke: gray !important; - } - - g.after path, - path.before_in { - opacity: .1; - stroke: gray !important; - } - - .gray { - position: absolute; - background: rgba(255, 255, 255, 0.7); - } - - - `; - - this.shadowRoot.appendChild(styleEl); - this.shadowRoot.appendChild(imgEl); - this.shadowRoot.appendChild(playerEl); - - } - - setAnnotation(annotation) { - this.annotation = annotation; - } -} - -window.customElements.define('annotation-player', AnnotationPlayer); - - class AnnotationManager { constructor(rootEl, tagsUrl) { this.rootEl = rootEl;