Compare commits
No commits in common. "6fbe49473d9eee3f828252b031d78fa038a6cd3f" and "55475451cf68afadb5b56fe172270b2ed99acbcb" have entirely different histories.
6fbe49473d
...
55475451cf
4 changed files with 82 additions and 434 deletions
|
@ -168,16 +168,29 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#annotations .svganim_player,
|
#annotations .svganim_player {
|
||||||
#annotations annotation-player {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#annotations .svganim_player svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#annotations .svganim_player.play:not(.loading) .controls {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#annotations .svganim_player:hover .controls {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="assets/nouislider-15.5.0.js"></script>
|
<script src="assets/nouislider-15.5.0.js"></script>
|
||||||
<script src="assets/wNumb-1.2.0.min.js"></script>
|
<script src="assets/wNumb-1.2.0.min.js"></script>
|
||||||
|
|
|
@ -2,9 +2,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
from zipfile import ZipFile
|
|
||||||
import tornado.ioloop
|
import tornado.ioloop
|
||||||
import tornado.web
|
import tornado.web
|
||||||
import tornado.websocket
|
import tornado.websocket
|
||||||
|
@ -242,73 +240,6 @@ class AudioListingHandler(tornado.web.RequestHandler):
|
||||||
print(names)
|
print(names)
|
||||||
self.write(json.dumps(names))
|
self.write(json.dumps(names))
|
||||||
|
|
||||||
class ExportHandler(tornado.web.RequestHandler):
|
|
||||||
"""
|
|
||||||
Export a player to a zip file
|
|
||||||
"""
|
|
||||||
def initialize(self, config, index: svganim.strokes.AnnotationIndex):
|
|
||||||
self.config = config
|
|
||||||
self.index = index
|
|
||||||
|
|
||||||
async def get(self, filename):
|
|
||||||
logger.info(f"file {filename=}")
|
|
||||||
if filename not in self.index.drawings:
|
|
||||||
raise tornado.web.HTTPError(404)
|
|
||||||
|
|
||||||
t_in = self.get_argument('t_in', None)
|
|
||||||
t_out = self.get_argument('t_out', None)
|
|
||||||
|
|
||||||
animation = self.index.drawings[filename].get_animation()
|
|
||||||
|
|
||||||
if t_in is not None and t_out is not None:
|
|
||||||
animation = animation.getSlice(float(t_in), float(t_out))
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tdir:
|
|
||||||
with ZipFile(tdir + '/annotation.zip', 'w') as archive:
|
|
||||||
logger.info('write svg')
|
|
||||||
svgstring = animation.get_as_svg()
|
|
||||||
archive.writestr('drawing.svg', svgstring)
|
|
||||||
|
|
||||||
logger.info('write png')
|
|
||||||
archive.writestr('drawing.png', cairosvg.svg2png(bytestring=svgstring))
|
|
||||||
|
|
||||||
logger.info('write mp3')
|
|
||||||
audio = await animation.audio.export(format="mp3")
|
|
||||||
archive.writestr('drawing.mp3', audio.read())
|
|
||||||
|
|
||||||
|
|
||||||
logger.info('write json')
|
|
||||||
data = animation.asDict()
|
|
||||||
data['audio']['file'] = 'drawing.mp3';
|
|
||||||
archive.writestr('annotation.json', json.dumps(data))
|
|
||||||
|
|
||||||
|
|
||||||
logger.info('write js')
|
|
||||||
with open('www/annotate.js', 'r') as fp:
|
|
||||||
archive.writestr('annotate.js', fp.read())
|
|
||||||
with open('www/assets/wNumb-1.2.0.min.js', 'r') as fp:
|
|
||||||
archive.writestr('wNumb-1.2.0.min.js', fp.read())
|
|
||||||
|
|
||||||
logger.info('write html')
|
|
||||||
html = """
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script src="wNumb-1.2.0.min.js"></script>
|
|
||||||
<script src="annotate.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<annotation-player data-poster-url="drawing.svg" data-annotation-url="annotation.json">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
archive.writestr('drawing.html', html)
|
|
||||||
|
|
||||||
with open(tdir + '/annotation.zip', 'rb') as fp:
|
|
||||||
self.set_header("Content-Type", "application/zip")
|
|
||||||
self.write(fp.read())
|
|
||||||
logger.info('done')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AnimationHandler(tornado.web.RequestHandler):
|
class AnimationHandler(tornado.web.RequestHandler):
|
||||||
def initialize(self, config, index: svganim.strokes.AnnotationIndex):
|
def initialize(self, config, index: svganim.strokes.AnnotationIndex):
|
||||||
|
@ -738,7 +669,6 @@ class Server:
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(r"/files/(.*)", AnimationHandler, {"config": self.config, "index": self.index}),
|
(r"/files/(.*)", AnimationHandler, {"config": self.config, "index": self.index}),
|
||||||
(r"/export/(.*)", ExportHandler, {"config": self.config, "index": self.index}),
|
|
||||||
(
|
(
|
||||||
r"/audio/(.+)",
|
r"/audio/(.+)",
|
||||||
tornado.web.StaticFileHandler,
|
tornado.web.StaticFileHandler,
|
||||||
|
|
|
@ -130,11 +130,10 @@ class Annotator extends EventTarget {
|
||||||
time *= -1;
|
time *= -1;
|
||||||
}
|
}
|
||||||
const s = Math.floor(time / 1000);
|
const s = Math.floor(time / 1000);
|
||||||
const minutes = String(Math.floor(s / 60)).padStart(2, '0');
|
const minutes = Math.floor(s / 60);
|
||||||
const seconds = String(s - minutes * 60).padStart(2, '0');
|
const seconds = s - minutes * 60;
|
||||||
// show miliseconds only in annotator
|
const ms = Math.floor((time / 1000 - s) * 1000);
|
||||||
const ms = !this.config.is_player ? "." + String(Math.floor((time / 1000 - s) * 1000)).padStart(3, '0') : "";
|
return `${neg}${minutes}:${seconds}.${ms}`;
|
||||||
return `${neg}${minutes}:${seconds}${ms}`;
|
|
||||||
},
|
},
|
||||||
undo: (tc) => {
|
undo: (tc) => {
|
||||||
let [rest, ms] = tc.split(/[\.\,]/);
|
let [rest, ms] = tc.split(/[\.\,]/);
|
||||||
|
@ -144,7 +143,7 @@ class Annotator extends EventTarget {
|
||||||
ms += v * factor;
|
ms += v * factor;
|
||||||
factor *= 60;
|
factor *= 60;
|
||||||
});
|
});
|
||||||
return `${ms} `;
|
return `${ms}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -153,7 +152,6 @@ class Annotator extends EventTarget {
|
||||||
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);
|
||||||
this.wrapperEl.classList.add(this.config.is_player ? "svganim_player" : "svganim_annotator");
|
this.wrapperEl.classList.add(this.config.is_player ? "svganim_player" : "svganim_annotator");
|
||||||
this.wrapperEl.classList.add(this.config.crop_to_fit ? "cropped-to-selection" : "follow-drawing");
|
|
||||||
|
|
||||||
|
|
||||||
this.controlsEl = document.createElement('div');
|
this.controlsEl = document.createElement('div');
|
||||||
|
@ -194,40 +192,15 @@ class Annotator extends EventTarget {
|
||||||
ev.preventDefault(); // we don't want to spacebar, as this is captured in the overall keydown event
|
ev.preventDefault(); // we don't want to spacebar, as this is captured in the overall keydown event
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!this.config.is_player) {
|
|
||||||
this.scrubberEl = document.createElement('div');
|
this.scrubberEl = document.createElement('div');
|
||||||
this.scrubberEl.classList.add('scrubber')
|
this.scrubberEl.classList.add('scrubber')
|
||||||
this.controlsEl.appendChild(this.scrubberEl);
|
this.controlsEl.appendChild(this.scrubberEl);
|
||||||
|
|
||||||
|
|
||||||
|
if(!this.config.is_player){
|
||||||
this.annotationsEl = document.createElement('div');
|
this.annotationsEl = document.createElement('div');
|
||||||
this.annotationsEl.classList.add('annotations')
|
this.annotationsEl.classList.add('annotations')
|
||||||
this.controlsEl.appendChild(this.annotationsEl);
|
this.controlsEl.appendChild(this.annotationsEl);
|
||||||
} else {
|
|
||||||
const extraEl = document.createElement('details');
|
|
||||||
extraEl.classList.add('controls--extra');
|
|
||||||
|
|
||||||
const summaryEl = document.createElement('summary');
|
|
||||||
summaryEl.innerHTML = "…";
|
|
||||||
extraEl.appendChild(summaryEl);
|
|
||||||
|
|
||||||
const extraControlsEl = document.createElement('ul');
|
|
||||||
|
|
||||||
// TODO: add handlers
|
|
||||||
const toggleFutureEl = document.createElement('li');
|
|
||||||
toggleFutureEl.innerText = "Show preview"
|
|
||||||
toggleFutureEl.addEventListener('click', () => this.wrapperEl.classList.toggle('hide-drawing-preview'));
|
|
||||||
extraControlsEl.appendChild(toggleFutureEl);
|
|
||||||
|
|
||||||
const toggleCropPlayerEl = document.createElement('li');
|
|
||||||
toggleCropPlayerEl.innerText = "Crop to selection";
|
|
||||||
toggleCropPlayerEl.addEventListener('click', () => this.toggleCrop());
|
|
||||||
extraControlsEl.appendChild(toggleCropPlayerEl);
|
|
||||||
|
|
||||||
extraEl.appendChild(extraControlsEl);
|
|
||||||
|
|
||||||
this.playbackControlsEl.appendChild(extraEl);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -366,7 +339,7 @@ class Annotator extends EventTarget {
|
||||||
if (this.selectedAnnotationI == annotation_i) {
|
if (this.selectedAnnotationI == annotation_i) {
|
||||||
this.annotationEl.classList.add('selected');
|
this.annotationEl.classList.add('selected');
|
||||||
}
|
}
|
||||||
this.annotationEl.title = `[${annotation.tag}] ${annotation.comment} `;
|
this.annotationEl.title = `[${annotation.tag}] ${annotation.comment}`;
|
||||||
|
|
||||||
this.annotationEl.addEventListener('mouseover', (e) => {
|
this.annotationEl.addEventListener('mouseover', (e) => {
|
||||||
|
|
||||||
|
@ -452,7 +425,7 @@ class Annotator extends EventTarget {
|
||||||
// draw full stroke of annotation
|
// draw full stroke of annotation
|
||||||
console.debug('setInOut');
|
console.debug('setInOut');
|
||||||
this.drawStrokePosition(this.inPointPosition, this.outPointPosition);
|
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]);
|
this.slider.set([this.inPointTimeMs, this.outPointTimeMs]);
|
||||||
|
|
||||||
// console.debug(this.selectedAnnotation);
|
// console.debug(this.selectedAnnotation);
|
||||||
|
@ -487,7 +460,7 @@ class Annotator extends EventTarget {
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!this.config.is_player) {
|
if (!this.config.is_player) {
|
||||||
|
|
||||||
const metadata_req = new Request(`/ annotations / ${data.file} `, {
|
const metadata_req = new Request(`/annotations/${data.file}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
return fetch(metadata_req)
|
return fetch(metadata_req)
|
||||||
|
@ -505,7 +478,7 @@ class Annotator extends EventTarget {
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// play on click for player
|
// play on click for player
|
||||||
if (this.config.is_player) {
|
if(this.config.is_player) {
|
||||||
this.svgEl.addEventListener('click', (ev) => {
|
this.svgEl.addEventListener('click', (ev) => {
|
||||||
console.debug('clicked for play/pause');
|
console.debug('clicked for play/pause');
|
||||||
this.playPause();
|
this.playPause();
|
||||||
|
@ -513,9 +486,9 @@ class Annotator extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// autoplay if necessary
|
// autoplay if necessary
|
||||||
if (this.config.autoplay) {
|
if(this.config.autoplay){
|
||||||
this.play(); // play should remove loading
|
this.play(); // play should remove loading
|
||||||
} else {
|
} else{
|
||||||
this.wrapperEl.classList.remove('loading');
|
this.wrapperEl.classList.remove('loading');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -872,7 +845,7 @@ class Annotator extends EventTarget {
|
||||||
titleEl.title = this.title ?? "[click to add title for this diagram]"
|
titleEl.title = this.title ?? "[click to add title for this diagram]"
|
||||||
titleEl.addEventListener('click', (ev) => {
|
titleEl.addEventListener('click', (ev) => {
|
||||||
const title = prompt("Change the title for the drawing", this.title ?? "");
|
const title = prompt("Change the title for the drawing", this.title ?? "");
|
||||||
if (title === null) return; //cancel
|
if(title === null) return; //cancel
|
||||||
titleEl.innerText = title.length ? title : "[add title]";
|
titleEl.innerText = title.length ? title : "[add title]";
|
||||||
this.title = title.length ? title : null;
|
this.title = title.length ? title : null;
|
||||||
this.updateState();
|
this.updateState();
|
||||||
|
@ -1013,7 +986,7 @@ class Annotator extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFinalFrameTime() {
|
getFinalFrameTime() {
|
||||||
if (this.strokes.length == 0) return null; // when no strokes are loaded (eg. for annotation)
|
if(this.strokes.length == 0) return null; // when no strokes are loaded (eg. for annotation)
|
||||||
const points = this.strokes[this.strokes.length - 1].points;
|
const points = this.strokes[this.strokes.length - 1].points;
|
||||||
return points[points.length - 1][3];
|
return points[points.length - 1][3];
|
||||||
}
|
}
|
||||||
|
@ -1059,7 +1032,7 @@ class Annotator extends EventTarget {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// for (let index = 0; index < inPath_i; index++) {
|
// for (let index = 0; index < inPath_i; index++) {
|
||||||
// const pathEl = this.svgEl.querySelector(`.path${ index } `);
|
// const pathEl = this.svgEl.querySelector(`.path${index}`);
|
||||||
// if (pathEl) {
|
// if (pathEl) {
|
||||||
// pathEl.classList.add('before_in');
|
// pathEl.classList.add('before_in');
|
||||||
// }
|
// }
|
||||||
|
@ -1071,7 +1044,7 @@ class Annotator extends EventTarget {
|
||||||
|
|
||||||
// const path = this.strokes[path_i];
|
// const path = this.strokes[path_i];
|
||||||
// // console.log(path);
|
// // console.log(path);
|
||||||
// let pathEl = this.svgEl.querySelector(`.path${ path_i } `);
|
// let pathEl = this.svgEl.querySelector(`.path${path_i}`);
|
||||||
// if (!pathEl) {
|
// if (!pathEl) {
|
||||||
// pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
// pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||||
// pathEl.style.stroke = path.color;
|
// pathEl.style.stroke = path.color;
|
||||||
|
@ -1099,10 +1072,10 @@ class Annotator extends EventTarget {
|
||||||
|
|
||||||
updateViewbox() {
|
updateViewbox() {
|
||||||
if (this.config.crop_to_fit) {
|
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 {
|
} else {
|
||||||
let x, y, w, h;
|
let x,y,w,h;
|
||||||
if (this.currentViewboxI !== null) {
|
if(this.currentViewboxI !== null) {
|
||||||
x = this.viewboxes[this.currentViewboxI].x,
|
x = this.viewboxes[this.currentViewboxI].x,
|
||||||
y = this.viewboxes[this.currentViewboxI].y,
|
y = this.viewboxes[this.currentViewboxI].y,
|
||||||
w = this.dimensions[0],
|
w = this.dimensions[0],
|
||||||
|
@ -1113,24 +1086,15 @@ class Annotator extends EventTarget {
|
||||||
w = this.dimensions[0],
|
w = this.dimensions[0],
|
||||||
h = this.dimensions[1];
|
h = this.dimensions[1];
|
||||||
}
|
}
|
||||||
this.svgEl.setAttribute('viewBox', `${x} ${y} ${w} ${h} `);
|
this.svgEl.setAttribute('viewBox', `${x} ${y} ${w} ${h}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCrop(crop_to_fit) {
|
toggleCrop(){
|
||||||
this.config.crop_to_fit = Boolean(crop_to_fit);
|
this.config.crop_to_fit = !this.config.crop_to_fit;
|
||||||
if (this.config.crop_to_fit) {
|
|
||||||
this.wrapperEl.classList.add('cropped-to-selection');
|
|
||||||
} else {
|
|
||||||
this.wrapperEl.classList.remove('cropped-to-selection');
|
|
||||||
}
|
|
||||||
this.updateViewbox();
|
this.updateViewbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCrop() {
|
|
||||||
this.setCrop(!this.config.crop_to_fit);
|
|
||||||
}
|
|
||||||
|
|
||||||
getNextPosition(path_i, point_i) {
|
getNextPosition(path_i, point_i) {
|
||||||
const path = this.strokes[path_i];
|
const path = this.strokes[path_i];
|
||||||
let next_path, next_point;
|
let next_path, next_point;
|
||||||
|
@ -1482,265 +1446,3 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-drawing-preview g.after path, .hide-drawing-preview path.before_in{
|
|
||||||
opacity:0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gray {
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
details{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary{
|
|
||||||
list-style: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
details > ul{
|
|
||||||
position: absolute;
|
|
||||||
bottom: 30px;
|
|
||||||
background: rgba(0,0,0, .5);
|
|
||||||
border-radius: 3px;
|
|
||||||
right: 0;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 0;
|
|
||||||
list-style: none;
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
details > ul li:hover{
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play:not(.hide-drawing-preview) details > ul li:first-child{
|
|
||||||
/*text-decoration: line-through;*/
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
.play.cropped-to-selection details > ul li:nth-child(2){
|
|
||||||
/*text-decoration: line-through;*/
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
|
@ -141,12 +141,14 @@ class AnnotationManager {
|
||||||
const ulEl = document.createElement('ul');
|
const ulEl = document.createElement('ul');
|
||||||
this.annotations.forEach((annotation, idx) => {
|
this.annotations.forEach((annotation, idx) => {
|
||||||
const liEl = document.createElement('li');
|
const liEl = document.createElement('li');
|
||||||
const playerEl = new AnnotationPlayer(); //document.createElement('annotation-player');
|
const imgEl = document.createElement('img');
|
||||||
playerEl.setAnnotation(annotation);
|
const playerEl = document.createElement('div');
|
||||||
|
const infoEl = document.createElement('span');
|
||||||
|
infoEl.classList.add('annotation-info');
|
||||||
|
|
||||||
const selectEl = document.createElement('input');
|
const selectEl = document.createElement('input');
|
||||||
selectEl.type = 'checkbox';
|
selectEl.type = 'checkbox';
|
||||||
selectEl.id = 'select-' + annotation.id_hash;
|
selectEl.id = 'select-'+annotation.id_hash;
|
||||||
selectEl.addEventListener('change', (ev) => {
|
selectEl.addEventListener('change', (ev) => {
|
||||||
if (ev.target.checked) {
|
if (ev.target.checked) {
|
||||||
this.addAnnotationToSelection(annotation);
|
this.addAnnotationToSelection(annotation);
|
||||||
|
@ -155,20 +157,25 @@ class AnnotationManager {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const tag = this.rootTag.find_by_id(annotation.tag);
|
const tag = this.rootTag.find_by_id(annotation.tag);
|
||||||
console.log(tag)
|
console.log(tag)
|
||||||
|
|
||||||
const infoEl = document.createElement('span');
|
|
||||||
infoEl.classList.add('annotation-info');
|
|
||||||
infoEl.innerText = `[${tag.get_name()}] ${annotation.comment}`;
|
infoEl.innerText = `[${tag.get_name()}] ${annotation.comment}`;
|
||||||
|
|
||||||
const downloadEl = document.createElement('a');
|
imgEl.src = `/annotation/${annotation.id}.svg`;
|
||||||
downloadEl.href = annotation.url.replace('files', 'export');
|
imgEl.addEventListener('click', () => {
|
||||||
downloadEl.innerHTML = '↓';
|
imgEl.style.display = 'none';
|
||||||
|
new Annotator(
|
||||||
|
playerEl,
|
||||||
|
"tags.json",
|
||||||
|
annotation.url,
|
||||||
|
{ is_player: true, crop_to_fit: true, autoplay: true }
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
infoEl.appendChild(downloadEl);
|
playerEl.classList.add('play');
|
||||||
|
|
||||||
|
liEl.appendChild(imgEl);
|
||||||
liEl.appendChild(playerEl);
|
liEl.appendChild(playerEl);
|
||||||
liEl.appendChild(selectEl);
|
liEl.appendChild(selectEl);
|
||||||
liEl.appendChild(infoEl);
|
liEl.appendChild(infoEl);
|
||||||
|
@ -188,26 +195,22 @@ class AnnotationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
addAnnotationToSelection(annotation) {
|
addAnnotationToSelection(annotation) {
|
||||||
if (this.selectedAnnotations.indexOf(annotation) === -1) {
|
if (this.selectedAnnotations.indexOf(annotation) === -1){
|
||||||
this.selectedAnnotations.push(annotation);
|
this.selectedAnnotations.push(annotation);
|
||||||
}
|
}
|
||||||
this.annotationsEl.querySelector('#select-' + annotation.id_hash).checked = true;
|
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true;
|
||||||
this.buildAnnotationActions()
|
this.buildAnnotationActions()
|
||||||
}
|
}
|
||||||
removeAnnotationFromSelection(annotation) {
|
removeAnnotationFromSelection(annotation) {
|
||||||
if (this.selectedAnnotations.indexOf(annotation) !== -1) {
|
if (this.selectedAnnotations.indexOf(annotation) !== -1){
|
||||||
this.selectedAnnotations.splice(this.selectedAnnotations.indexOf(annotation), 1)
|
this.selectedAnnotations.splice(this.selectedAnnotations.indexOf(annotation), 1)
|
||||||
}
|
}
|
||||||
this.annotationsEl.querySelector('#select-' + annotation.id_hash).checked = false;
|
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = false;
|
||||||
this.buildAnnotationActions()
|
this.buildAnnotationActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the form items to select & move the annotations
|
|
||||||
* @returns undefined
|
|
||||||
*/
|
|
||||||
buildAnnotationActions() {
|
buildAnnotationActions() {
|
||||||
if (!this.actionsEl || !this.annotations.length) return
|
if(!this.actionsEl || !this.annotations.length) return
|
||||||
this.actionsEl.innerHTML = "";
|
this.actionsEl.innerHTML = "";
|
||||||
|
|
||||||
const selectAllLabelEl = document.createElement('label');
|
const selectAllLabelEl = document.createElement('label');
|
||||||
|
@ -217,7 +220,7 @@ class AnnotationManager {
|
||||||
selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length;
|
selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length;
|
||||||
// selectAllEl.innerText = `Select all ${this.annotations.length} items`;
|
// selectAllEl.innerText = `Select all ${this.annotations.length} items`;
|
||||||
selectAllEl.addEventListener('change', (ev) => {
|
selectAllEl.addEventListener('change', (ev) => {
|
||||||
if (ev.target.checked) {
|
if(ev.target.checked) {
|
||||||
this.annotations.forEach((a) => this.addAnnotationToSelection(a));
|
this.annotations.forEach((a) => this.addAnnotationToSelection(a));
|
||||||
} else {
|
} else {
|
||||||
this.resetSelectedAnnotations();
|
this.resetSelectedAnnotations();
|
||||||
|
@ -228,7 +231,7 @@ class AnnotationManager {
|
||||||
|
|
||||||
this.actionsEl.appendChild(selectAllLabelEl)
|
this.actionsEl.appendChild(selectAllLabelEl)
|
||||||
|
|
||||||
if (!this.selectedAnnotations.length) return;
|
if(!this.selectedAnnotations.length) return;
|
||||||
|
|
||||||
const moveLabelEl = document.createElement('label');
|
const moveLabelEl = document.createElement('label');
|
||||||
moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items`
|
moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items`
|
||||||
|
@ -236,7 +239,7 @@ class AnnotationManager {
|
||||||
this.rootTag.descendants().forEach((tag, i) => {
|
this.rootTag.descendants().forEach((tag, i) => {
|
||||||
const tagEl = document.createElement('option');
|
const tagEl = document.createElement('option');
|
||||||
tagEl.value = tag.id;
|
tagEl.value = tag.id;
|
||||||
if (tag.id == this.selectedTag.id) {
|
if(tag.id == this.selectedTag.id){
|
||||||
tagEl.selected = true;
|
tagEl.selected = true;
|
||||||
}
|
}
|
||||||
tagEl.innerHTML = tag.get_indented_name();
|
tagEl.innerHTML = tag.get_indented_name();
|
||||||
|
@ -305,7 +308,7 @@ class AnnotationManager {
|
||||||
await Promise.all(this.selectedAnnotations.map(async (annotation) => {
|
await Promise.all(this.selectedAnnotations.map(async (annotation) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('tag_id', tag.id)
|
formData.append('tag_id', tag.id)
|
||||||
return await fetch('/annotation/' + annotation.id, {
|
return await fetch('/annotation/'+annotation.id, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
// headers: {
|
// headers: {
|
||||||
// 'Content-Type': 'application/json'
|
// 'Content-Type': 'application/json'
|
||||||
|
|
Loading…
Reference in a new issue