Compare commits

..

No commits in common. "6fbe49473d9eee3f828252b031d78fa038a6cd3f" and "55475451cf68afadb5b56fe172270b2ed99acbcb" have entirely different histories.

4 changed files with 82 additions and 434 deletions

View file

@ -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>

View file

@ -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,

View file

@ -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(/[\.\,]/);
@ -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 = "&mldr;";
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);
} }
@ -1117,18 +1090,9 @@ class Annotator extends EventTarget {
} }
} }
setCrop(crop_to_fit) {
this.config.crop_to_fit = Boolean(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();
}
toggleCrop(){ toggleCrop(){
this.setCrop(!this.config.crop_to_fit); this.config.crop_to_fit = !this.config.crop_to_fit;
this.updateViewbox();
} }
getNextPosition(path_i, point_i) { getNextPosition(path_i, point_i) {
@ -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);

View file

@ -141,8 +141,10 @@ 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';
@ -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 = '&darr;'; 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);
@ -202,10 +209,6 @@ class AnnotationManager {
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 = "";