Compare commits
2 commits
4d08b0b4ad
...
9a4a89c9c7
Author | SHA1 | Date | |
---|---|---|---|
|
9a4a89c9c7 | ||
|
3ed2448545 |
3 changed files with 90 additions and 39 deletions
|
@ -119,7 +119,7 @@ class Drawing:
|
|||
for p in event["points"]],
|
||||
)
|
||||
)
|
||||
return AnimationSlice([self.id, None, None], strokes, viewboxes, audioslice=self.get_audio())
|
||||
return AnimationSlice(self, [self.id, None, None], strokes, viewboxes, audioslice=self.get_audio())
|
||||
|
||||
def get_metadata(self):
|
||||
canvas = self.get_canvas_metadata()
|
||||
|
@ -159,17 +159,17 @@ class AnimationSlice:
|
|||
# either a whole drawing or the result of applying an annotation to a drawing (an excerpt)
|
||||
# TODO rename to AnimationSlice to include audio as well
|
||||
def __init__(
|
||||
self, slice_id: SliceId, strokes: list[Stroke], viewboxes: list[TimedViewbox] = [], t_in: float = 0, t_out: float = None, audioslice: AudioSlice = None
|
||||
self, drawing: Drawing, slice_id: SliceId, strokes: list[Stroke], viewboxes: list[TimedViewbox] = [], t_in: float = 0, t_out: float = None, audioslice: AudioSlice = None
|
||||
) -> None:
|
||||
self.drawing = drawing
|
||||
self.id = slice_id
|
||||
self.strokes = strokes
|
||||
self.viewboxes = viewboxes
|
||||
self.t_in = t_in
|
||||
self.t_out = t_out
|
||||
self.audio = audioslice
|
||||
# TODO: Audio
|
||||
|
||||
def asDict(self) -> dict:
|
||||
def asDict(self, include_full_drawing=False) -> dict:
|
||||
"""Can be used to json-ify the animation-slice
|
||||
"""
|
||||
|
||||
|
@ -187,8 +187,11 @@ class AnimationSlice:
|
|||
"shape": [s.asDict() for s in self.strokes],
|
||||
"viewboxes": boxes,
|
||||
"bounding_box": self.get_bounding_box().__dict__,
|
||||
"audio": self.getAudioDict() if self.audio else None
|
||||
"audio": self.getAudioDict() if self.audio else None,
|
||||
}
|
||||
if include_full_drawing:
|
||||
drawing["background"] = [s.get_as_d() for s in self.drawing.get_animation().strokes]
|
||||
drawing["background_bounding_box"] = self.drawing.get_animation().get_bounding_box().__dict__
|
||||
return drawing
|
||||
|
||||
def getAudioDict(self):
|
||||
|
@ -249,7 +252,7 @@ class AnimationSlice:
|
|||
viewboxes = self.getViewboxesSlice(t_in, t_out)
|
||||
|
||||
audio = self.audio.getSlice(t_in, t_out) if self.audio else None
|
||||
return AnimationSlice([self.id[0], t_in, t_out], strokes, viewboxes, t_in, t_out, audio)
|
||||
return AnimationSlice(self.drawing, [self.id[0], t_in, t_out], strokes, viewboxes, t_in, t_out, audio)
|
||||
|
||||
def get_as_svg_dwg(self) -> svgwrite.Drawing:
|
||||
box = self.get_bounding_box()
|
||||
|
@ -406,7 +409,7 @@ class AudioSlice:
|
|||
self.t_out = t_out # in ms
|
||||
self.offset = offset # in ms TODO: use from self.drawing metadata
|
||||
|
||||
def getSlice(self, t_in: float, t_out: float) -> AnimationSlice:
|
||||
def getSlice(self, t_in: float, t_out: float) -> AudioSlice:
|
||||
return AudioSlice(self.filename, self.drawing, t_in, t_out, self.offset)
|
||||
|
||||
def asDict(self):
|
||||
|
|
|
@ -280,7 +280,7 @@ class ExportHandler(tornado.web.RequestHandler):
|
|||
|
||||
|
||||
logger.info('write json')
|
||||
data = animation.asDict()
|
||||
data = animation.asDict(include_full_drawing=True)
|
||||
data['audio']['file'] = f'annotation-{identifier}.mp3';
|
||||
archive.writestr(f'annotation-{identifier}.json', json.dumps(data))
|
||||
|
||||
|
@ -406,7 +406,7 @@ class AnimationHandler(tornado.web.RequestHandler):
|
|||
self.write(audio.read())
|
||||
else:
|
||||
self.set_header("Content-Type", "application/json")
|
||||
self.write(json.dumps(animation.asDict()))
|
||||
self.write(json.dumps(animation.asDict(include_full_drawing=True)))
|
||||
|
||||
|
||||
class TagHandler(tornado.web.RequestHandler):
|
||||
|
|
|
@ -75,6 +75,21 @@ class StrokeGroup {
|
|||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
setPrecomputedStrokes(strokeDs) {
|
||||
const pathEls = this.g.querySelectorAll('path');
|
||||
for (let pathEl of pathEls) {
|
||||
pathEl.parentNode.removeChild(pathEl);
|
||||
}
|
||||
strokeDs.forEach((strokeD, index) => {
|
||||
let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
// pathEl.style.stroke = stroke.color;
|
||||
// pathEl.classList.add('path');
|
||||
pathEl.setAttribute('d', strokeD);
|
||||
this.g.appendChild(pathEl);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Stroke {
|
||||
|
@ -110,6 +125,18 @@ class StrokeSlice {
|
|||
}
|
||||
}
|
||||
|
||||
const CropOptions = {
|
||||
Fit_Selection: 'selection',
|
||||
Follow_Drawing: 'follow',
|
||||
Whole_Drawing: 'whole',
|
||||
};
|
||||
|
||||
const CropDescriptions = {
|
||||
selection: 'Crop to annotation',
|
||||
follow: 'Follow drawing canvas',
|
||||
whole: 'Show whole drawing',
|
||||
}
|
||||
|
||||
class Annotator extends EventTarget {
|
||||
constructor(wrapperEl, tagFile, fileurl, config) {
|
||||
fileurl = fileurl.replace("&", "&"); // little hack: tornadoweb does this automatically for some reason
|
||||
|
@ -117,7 +144,8 @@ class Annotator extends EventTarget {
|
|||
|
||||
this.config = {
|
||||
is_player: config && config.hasOwnProperty('is_player') ? config.is_player : false, // in player mode annotations are not loaded, nor is the annotator shown
|
||||
crop_to_fit: config && config.hasOwnProperty('crop_to_fit') ? config.crop_to_fit : false, // don't animate viewport, but show the whole drawing
|
||||
crop_to_fit: config && config.hasOwnProperty('crop_to_fit') ? config.crop_to_fit : false, // DEPRECATED don't animate viewport, but show the whole drawing
|
||||
crop: config && config.hasOwnProperty('crop') && Object.values(CropOptions).indexOf(config.crop) !== -1 ? config.crop : CropOptions.Fit_Selection, // don't animate viewport, but show the whole drawing
|
||||
autoplay: config && config.hasOwnProperty('autoplay') ? config.autoplay : false, // immediately start playback
|
||||
}
|
||||
|
||||
|
@ -153,7 +181,7 @@ class Annotator extends EventTarget {
|
|||
this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
this.wrapperEl.appendChild(this.svgEl);
|
||||
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.wrapperEl.classList.add("crop-" + this.config.crop);
|
||||
|
||||
|
||||
this.controlsEl = document.createElement('div');
|
||||
|
@ -219,10 +247,10 @@ class Annotator extends EventTarget {
|
|||
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);
|
||||
this.toggleCropPlayerEl = document.createElement('li');
|
||||
this.toggleCropPlayerEl.innerText = CropDescriptions[this.config.crop];
|
||||
this.toggleCropPlayerEl.addEventListener('click', () => this.toggleCrop());
|
||||
extraControlsEl.appendChild(this.toggleCropPlayerEl);
|
||||
|
||||
extraEl.appendChild(extraControlsEl);
|
||||
|
||||
|
@ -238,7 +266,7 @@ class Annotator extends EventTarget {
|
|||
this._currentTimeMs = 0;
|
||||
this.videoIsPlaying = false;
|
||||
|
||||
const groups = ['before', 'annotation', 'after']
|
||||
const groups = ['background', 'before', 'annotation', 'after']
|
||||
this.strokeGroups = {};
|
||||
groups.forEach(group => {
|
||||
let groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
|
@ -738,6 +766,8 @@ class Annotator extends EventTarget {
|
|||
|
||||
this.filename = drawing.file;
|
||||
this.strokes = drawing.shape.map(s => new Stroke(s['color'], s['points']));
|
||||
this.backgroundStrokes = drawing.hasOwnProperty('background') ? drawing.background : [];
|
||||
this.backgroundBoundingBox = drawing.hasOwnProperty('background_bounding_box') ? drawing.background_bounding_box : null;
|
||||
this.viewboxes = drawing.viewboxes;
|
||||
this.currentPathI = null;
|
||||
this.currentPointI = null;
|
||||
|
@ -761,6 +791,10 @@ class Annotator extends EventTarget {
|
|||
this.nextViewboxTimeout = null;
|
||||
this._setPausedFlag(true);
|
||||
|
||||
if(this.backgroundStrokes && this.backgroundStrokes.length){
|
||||
this.strokeGroups['background'].setPrecomputedStrokes(this.backgroundStrokes)
|
||||
}
|
||||
|
||||
return this.setupAudioConfig().then(() => {
|
||||
// this.setUpAnnotator()
|
||||
let keyEl;
|
||||
|
@ -1035,6 +1069,7 @@ class Annotator extends EventTarget {
|
|||
return slices;
|
||||
}
|
||||
|
||||
|
||||
// TODO: when drawing, have a group active & inactive.
|
||||
// active is getPathRange(currentIn, currentOut)
|
||||
// inactive is what comes before and after.
|
||||
|
@ -1098,8 +1133,10 @@ class Annotator extends EventTarget {
|
|||
}
|
||||
|
||||
updateViewbox() {
|
||||
if (this.config.crop_to_fit) {
|
||||
if (this.config.crop == CropOptions.Fit_Selection) {
|
||||
this.svgEl.setAttribute('viewBox', `${this.bounding_box.x} ${this.bounding_box.y} ${this.bounding_box.width} ${this.bounding_box.height}`);
|
||||
} else if (this.config.crop == CropOptions.Whole_Drawing && this.backgroundBoundingBox) {
|
||||
this.svgEl.setAttribute('viewBox', `${this.backgroundBoundingBox.x} ${this.backgroundBoundingBox.y} ${this.backgroundBoundingBox.width} ${this.backgroundBoundingBox.height}`);
|
||||
} else {
|
||||
let x, y, w, h;
|
||||
if (this.currentViewboxI !== null) {
|
||||
|
@ -1117,18 +1154,31 @@ 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');
|
||||
setCrop(crop_option) {
|
||||
if(Object.values(CropOptions).indexOf(crop_option) === -1) {
|
||||
console.error('invalid crop option', crop_option);
|
||||
crop_option = CropOptions.Fit_Selection;
|
||||
}
|
||||
|
||||
this.config.crop = crop_option;
|
||||
for(let option of Object.values(CropOptions)) {
|
||||
if (this.config.crop == option) {
|
||||
this.wrapperEl.classList.add('crop-' + option);
|
||||
} else {
|
||||
this.wrapperEl.classList.remove('crop-' + option);
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleCropPlayerEl.innerText = CropDescriptions[this.config.crop];
|
||||
this.updateViewbox();
|
||||
}
|
||||
|
||||
toggleCrop() {
|
||||
this.setCrop(!this.config.crop_to_fit);
|
||||
console.log(this.config.crop, Object.values(CropOptions), Object.values(CropOptions).indexOf(this.config.crop))
|
||||
const i = (Object.values(CropOptions).indexOf(this.config.crop) + 1) % Object.keys(CropOptions).length;
|
||||
const newCrop = Object.values(CropOptions)[i];
|
||||
console.log(i, newCrop);
|
||||
this.setCrop(newCrop);
|
||||
}
|
||||
|
||||
getNextPosition(path_i, point_i) {
|
||||
|
@ -1506,7 +1556,7 @@ class AnnotationPlayer extends HTMLElement {
|
|||
|
||||
const config = {
|
||||
is_player: true,
|
||||
crop_to_fit: this.hasAttribute('data-no-crop') ? false : true,
|
||||
crop: this.hasAttribute('data-crop') ? this.getAttribute('data-crop') : null,
|
||||
autoplay: true,
|
||||
}
|
||||
|
||||
|
@ -1665,6 +1715,13 @@ class AnnotationPlayer extends HTMLElement {
|
|||
opacity:0;
|
||||
}
|
||||
|
||||
.background{
|
||||
visibility: hidden
|
||||
}
|
||||
.play:not(.crop-selection) .background{
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.gray {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
|
@ -1701,11 +1758,10 @@ class AnnotationPlayer extends HTMLElement {
|
|||
/*text-decoration: line-through;*/
|
||||
font-weight:bold;
|
||||
}
|
||||
.play.cropped-to-selection details > ul li:nth-child(2){
|
||||
.play.crop-selection details > ul li:nth-child(2){
|
||||
/*text-decoration: line-through;*/
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
this.shadowRoot.appendChild(styleEl);
|
||||
|
@ -1720,26 +1776,18 @@ class AnnotationPlayer extends HTMLElement {
|
|||
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') {
|
||||
// console.log(name, oldValue, newValue);
|
||||
if (name == 'data-crop') {
|
||||
if (!this.annotator) {
|
||||
return;
|
||||
}
|
||||
this.annotator.setCrop(!this.hasAttribute('data-no-crop'));
|
||||
this.annotator.setCrop(this.hasAttribute('data-crop'));
|
||||
}
|
||||
}
|
||||
|
||||
// required for attributeChangedCallback()
|
||||
static get observedAttributes() { return ['data-no-crop']; }
|
||||
static get observedAttributes() { return ['data-crop']; }
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue