Compare commits

..

No commits in common. "50d4656ad7079d08fe518a1bb9c82f828fa767b8" and "d7319a0c6d5ffb94bfb37d257057f53ec5ef8d8f" have entirely different histories.

5 changed files with 68 additions and 150 deletions

View File

@ -12,7 +12,6 @@ import logging
logger = logging.getLogger('svganim.strokes') logger = logging.getLogger('svganim.strokes')
class Annotation: class Annotation:
def __init__(self, tag: str, drawing: Drawing, t_in: float, t_out: float) -> None: def __init__(self, tag: str, drawing: Drawing, t_in: float, t_out: float) -> None:
self.tag = tag self.tag = tag
@ -24,6 +23,7 @@ class Annotation:
def id(self) -> str: def id(self) -> str:
return f'{self.drawing.id}:{self.tag}:{self.t_in}:{self.t_out}' return f'{self.drawing.id}:{self.tag}:{self.t_in}:{self.t_out}'
def getAnimationSlice(self) -> AnimationSlice: def getAnimationSlice(self) -> AnimationSlice:
return self.drawing.get_animation().getSlice(self.t_in, self.t_out) return self.drawing.get_animation().getSlice(self.t_in, self.t_out)
@ -70,7 +70,7 @@ class Drawing:
if 'file' not in md['audio']: if 'file' not in md['audio']:
return None return None
return AudioSlice(filename=os.path.join(self.basedir, md['audio']['file'][1:]), offset=md['audio']['offset']*1000) return AudioSlice(filename=os.path.join(self.basedir,md['audio']['file'][1:]), offset=md['audio']['offset']*1000)
def get_animation(self) -> AnimationSlice: def get_animation(self) -> AnimationSlice:
# with open(self.eventfile, "r") as fp: # with open(self.eventfile, "r") as fp:
@ -96,11 +96,10 @@ class Drawing:
strokes.append( strokes.append(
Stroke( Stroke(
event["color"], event["color"],
[Point.fromTuple(tuple(p)) [Point.fromTuple(tuple(p)).scaled(self.get_canvas_metadata()['dimensions']) for p in event["points"]],
for p in event["points"]],
) )
) )
return AnimationSlice(strokes, audioslice=self.get_audio()) return AnimationSlice(strokes, audioslice=self.get_audio() )
def get_metadata(self): def get_metadata(self):
canvas = self.get_canvas_metadata() canvas = self.get_canvas_metadata()
@ -159,12 +158,13 @@ class AnimationSlice:
return Viewbox(min_x, min_y, max_x - min_x, max_y - min_y) return Viewbox(min_x, min_y, max_x - min_x, max_y - min_y)
def getSlice(self, t_in: float, t_out: float) -> AnimationSlice: def getSlice(self, t_in: float, t_out: float) -> AnimationSlice:
frame_in = self.getIndexForInPoint(t_in) frame_in = self.getIndexForTime(t_in)
frame_out = self.getIndexForOutPoint(t_out) frame_out = self.getIndexForTime(t_out)
strokes = self.getStrokeSlices(frame_in, frame_out) strokes = self.getStrokeSlices(frame_in, frame_out)
audio = self.audio.getSlice(t_in, t_out) if self.audio else None audio = self.audio.getSlice(t_in, t_out) if self.audio else None
return AnimationSlice(strokes, t_in, t_out, audio) return AnimationSlice(strokes, t_in, t_out, audio)
def get_as_svg_dwg(self) -> svgwrite.Drawing: def get_as_svg_dwg(self) -> svgwrite.Drawing:
box = self.get_bounding_box() box = self.get_bounding_box()
(_, fn) = tempfile.mkstemp(suffix='.svg', text=True) (_, fn) = tempfile.mkstemp(suffix='.svg', text=True)
@ -200,47 +200,11 @@ class AnimationSlice:
break break
in_i = index_in[1] if index_in[0] == i else 0 in_i = index_in[1] if index_in[0] == i else 0
out_i = index_out[1] if index_out[0] == i else len( out_i = index_out[1] if index_out[0] == i else len(stroke.points) - 1
stroke.points) - 1
slices.append(StrokeSlice(stroke, in_i, out_i)) slices.append(StrokeSlice(stroke, in_i, out_i))
return slices return slices
def getIndexForInPoint(self, ms) -> FrameIndex:
"""Get the frame index (path, point) based on the given time
The In point version (so the first index after ms)
Equal to annotations.js findPositionForTime(ms)
"""
path_i = 0
point_i = 0
for i, stroke in enumerate(self.strokes):
start_at = stroke.points[0].t
end_at = stroke.points[-1].t
if end_at < ms:
# certainly not the right point yet
continue
if start_at > ms:
path_i = i
point_i = 0
break # too far, so this is the first point after in point
else:
# our in-point is inbetween first and last of the stroke
# we are getting close, find the right point_i
path_i = i
for pi, point in enumerate(stroke.points):
point_i = pi
if point.t > ms:
break # stop when finding the next point after in point
break # done :-)
return (path_i, point_i)
def getIndexForOutPoint(self, ms) -> FrameIndex:
"""Get the frame index (path, point) based on the given time
The Out point version (so the last index before ms)
Equal to annotations.js findPositionForTime(ms)
"""
return self.getIndexForTime( ms)
def getIndexForTime(self, ms) -> FrameIndex: def getIndexForTime(self, ms) -> FrameIndex:
"""Get the frame index (path, point) based on the given time """Get the frame index (path, point) based on the given time
Equal to annotations.js findPositionForTime(ms) Equal to annotations.js findPositionForTime(ms)
@ -257,7 +221,7 @@ class AnimationSlice:
# we are getting close, find the right point_i # we are getting close, find the right point_i
path_i = i path_i = i
for pi, point in enumerate(stroke.points): for pi, point in enumerate(stroke.points):
if point.t > ms: if point.t:
break # too far break # too far
point_i = pi point_i = pi
break # done :-) break # done :-)
@ -270,11 +234,11 @@ class AnimationSlice:
class AudioSlice: class AudioSlice:
def __init__(self, filename: Filename, t_in: float = None, t_out: float = None, offset: float = None): def __init__(self, filename: Filename, t_in: float=None, t_out: float=None, offset:float = None):
self.filename = filename self.filename = filename
self.t_in = t_in # in ms self.t_in = t_in # in ms
self.t_out = t_out # in ms self.t_out = t_out # in ms
self.offset = offset # in ms self.offset = offset # in ms
def getSlice(self, t_in: float, t_out: float) -> AnimationSlice: def getSlice(self, t_in: float, t_out: float) -> AnimationSlice:
return AudioSlice(self.filename, t_in, t_out, self.offset) return AudioSlice(self.filename, t_in, t_out, self.offset)
@ -287,18 +251,15 @@ class AudioSlice:
end = self.t_out - self.offset end = self.t_out - self.offset
if start < 0 and end < 0: if start < 0 and end < 0:
extract = AudioSegment.silent( extract = AudioSegment.silent(duration=end-start, frame_rate=song.frame_rate)
duration=end-start, frame_rate=song.frame_rate)
else: else:
if start < 0: if start < 0:
preroll = AudioSegment.silent( preroll = AudioSegment.silent(duration=start * -1, frame_rate=song.frame_rate)
duration=start * -1, frame_rate=song.frame_rate)
start = 0 start = 0
else: else:
preroll = None preroll = None
if end > len(song): if end > len(song):
postroll = AudioSegment.silent( postroll = AudioSegment.silent(duration=end - len(song), frame_rate=song.frame_rate)
duration=end - len(song), frame_rate=song.frame_rate)
end = len(song) - 1 end = len(song) - 1
else: else:
postroll = None postroll = None
@ -321,8 +282,7 @@ class AnnotationIndex:
self.drawing_dir = drawing_dir self.drawing_dir = drawing_dir
self.metadata_dir = metadata_dir self.metadata_dir = metadata_dir
# disable disk cache because of glitches shelve.open(filename, writeback=True) self.shelve = {} #disable disk cache because of glitches shelve.open(filename, writeback=True)
self.shelve = {}
def refresh(self): def refresh(self):
# reset the index # reset the index
@ -345,8 +305,7 @@ class AnnotationIndex:
if 'annotations' not in meta: if 'annotations' not in meta:
continue continue
for ann in meta['annotations']: for ann in meta['annotations']:
annotation = Annotation( annotation = Annotation(ann['tag'], drawing, ann['t_in'], ann['t_out'])
ann['tag'], drawing, ann['t_in'], ann['t_out'])
self.shelve['_annotations'][annotation.id] = annotation self.shelve['_annotations'][annotation.id] = annotation
if annotation.tag not in self.shelve['_tags']: if annotation.tag not in self.shelve['_tags']:
self.shelve['_tags'][annotation.tag] = [annotation] self.shelve['_tags'][annotation.tag] = [annotation]
@ -355,6 +314,7 @@ class AnnotationIndex:
annotation annotation
) )
@property @property
def drawings(self) -> dict[str, Drawing]: def drawings(self) -> dict[str, Drawing]:
return self.shelve["_drawings"] return self.shelve["_drawings"]
@ -395,7 +355,7 @@ class AnnotationIndex:
class Point: class Point:
def __init__(self, x: float, y: float, last: bool, t: float): def __init__(self, x: float, y: float, last: bool, t: float):
self.x = float(x) self.x = float(x)
self.y = float(y) # if y == 0 it can still be integer.... odd python self.y = float(y) # if y == 0 it can still be integer.... odd python
self.last = last self.last = last
self.t = t self.t = t
@ -403,25 +363,23 @@ class Point:
def fromTuple(cls, p: tuple[float, float, int, float]): def fromTuple(cls, p: tuple[float, float, int, float]):
return cls(p[0], p[1], bool(p[2]), p[3]) return cls(p[0], p[1], bool(p[2]), p[3])
def scaledToFit(self, dimensions: dict[str, float]) -> Point: def scaled(self, dimensions: dict[str, float]) -> Point:
# TODO: change so that it actually scales to FIT dimensions return Point(self.x*dimensions['width'], self.y * dimensions['height'], self.last, self.t)
return Point(self.x, self.y, self.last, self.t)
Points = list[Point] Points = list[Point]
SvgDrawing = Union[svgwrite.container.SVG, svgwrite.container.Group] SvgDrawing = Union[svgwrite.container.SVG, svgwrite.container.Group]
class Stroke: class Stroke:
def __init__(self, color: str, points: Points) -> None: def __init__(self, color: str, points: Points) -> None:
self.color = color self.color = color
self.points = points self.points = points
def add_to_dwg(self, dwg: SvgDrawing): def add_to_dwg(self, dwg: SvgDrawing):
path = svgwrite.path.Path(d=self.get_as_d()).stroke( path = svgwrite.path.Path(d=self.get_as_d()).stroke(self.color,1).fill("none")
self.color, 1).fill("none")
dwg.add(path) dwg.add(path)
def get_bounding_box(self) -> Viewbox: def get_bounding_box(self) -> Viewbox:
min_x, max_x = float("inf"), float("-inf") min_x, max_x = float("inf"), float("-inf")
min_y, max_y = float("inf"), float("-inf") min_y, max_y = float("inf"), float("-inf")
@ -474,7 +432,7 @@ class StrokeSlice(Stroke):
@property @property
def points(self) -> Points: def points(self) -> Points:
return self.stroke.points[self.i_in: self.i_out + 1] return self.stroke.points[self.i_in : self.i_out + 1]
@property @property
def color(self) -> str: def color(self) -> str:
@ -483,23 +441,22 @@ class StrokeSlice(Stroke):
def strokes2D(strokes): def strokes2D(strokes):
# strokes to a d attribute for a path # strokes to a d attribute for a path
d = "" d = "";
last_stroke = None last_stroke = None;
cmd = "" cmd = "";
for stroke in strokes: for stroke in strokes:
if not last_stroke: if not last_stroke:
d += f"M{stroke[0]},{stroke[1]} " d += f"M{stroke[0]},{stroke[1]} "
cmd = 'M' cmd = 'M'
else: else:
if last_stroke[2] == 1: if last_stroke[2] == 1:
d += " m" d += " m"
cmd = 'm' cmd = 'm'
elif cmd != 'l': elif cmd != 'l':
d += ' l ' d+=' l '
cmd = 'l' cmd = 'l'
rel_stroke = [stroke[0] - last_stroke[0], rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
stroke[1] - last_stroke[1]]
d += f"{rel_stroke[0]},{rel_stroke[1]} " d += f"{rel_stroke[0]},{rel_stroke[1]} "
last_stroke = stroke last_stroke = stroke
return d return d

View File

@ -47,7 +47,6 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.strokes = [] self.strokes = []
self.hasWritten = False self.hasWritten = False
self.prev_file = None self.prev_file = None
self.prev_file_duration = 0
self.dimensions = [None, None] self.dimensions = [None, None]
# def check_origin(self, origin): # def check_origin(self, origin):
@ -127,42 +126,15 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.prev_file = prev_file self.prev_file = prev_file
metadata = self.getFileMetadata(self.prev_file) with open(self.prev_file, "r") as fp:
self.prev_file_duration = self.getLastTimestampInFile(self.prev_file)
logger.info(
"Previous file set. {self.prev_file} {metadata=} time: {self.prev_file_duration}")
self.write_message(json.dumps(
{"preloaded_svg": f"/drawing/{file}", "dimensions": [metadata[1], metadata[2]], "time": self.prev_file_duration}))
def getFileMetadata(self, filename):
with open(filename, "r") as fp:
first_line = fp.readline().strip() first_line = fp.readline().strip()
if first_line.endswith(","): if first_line.endswith(","):
first_line = first_line[:-1] first_line = first_line[:-1]
metadata = json.loads(first_line) metadata = json.loads(first_line)
return metadata self.write_message(json.dumps(
{"preloaded_svg": f"/drawing/{file}", "dimensions": [metadata[1], metadata[2]]}))
def getLastTimestampInFile(self, filename):
with open(filename, "r") as fp:
for line in fp:
pass # loop until the end
last_line = line.strip()
if last_line.endswith(","):
last_line = last_line[:-1]
data = json.loads(last_line)
if type(data) is list:
raise Exception("Oddly, the file ends with merely metadata")
if data['event'] == 'stroke':
return data['points'][-1][3]
elif data['event'] == 'viewbox':
return data['viewboxes'][-1]['t']
else:
raise Exception("Unknown last event")
# the client sent the message # the client sent the message
def on_message(self, message): def on_message(self, message):
@ -172,20 +144,13 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
msg = json.loads(message) msg = json.loads(message)
if msg["event"] == "stroke": if msg["event"] == "stroke":
logger.info("stroke") logger.info("stroke")
for i in range(len(msg['points'])):
msg['points'][i][3] += self.prev_file_duration
self.appendEvent(msg) self.appendEvent(msg)
elif msg["event"] == "dimensions": elif msg["event"] == "dimensions":
self.dimensions = [int(msg["width"]), int(msg["height"])] self.dimensions = [int(msg["width"]), int(msg["height"])]
logger.info(f"{self.dimensions=}") logger.info(f"{self.dimensions=}")
elif msg["event"] == "viewbox": elif msg["event"] == "viewbox":
logger.info("move or resize") logger.info("move or resize")
if len(msg['viewboxes']) == 0: self.appendEvent(msg)
logger.warn("Empty viewbox array")
else:
for i in range(len(msg['viewboxes'])):
msg['viewboxes'][i]['t'] += self.prev_file_duration
self.appendEvent(msg)
elif msg["event"] == "preload": elif msg["event"] == "preload":
self.preloadFile(msg["file"]) self.preloadFile(msg["file"])
else: else:
@ -288,9 +253,6 @@ class AnimationHandler(tornado.web.RequestHandler):
drawing["time"] = event[0] drawing["time"] = event[0]
drawing["dimensions"] = [event[1], event[2]] drawing["dimensions"] = [event[1], event[2]]
else: else:
if type(event) is list:
# ignore double metadatas, which appear when continuaing an existing drawing
continue
if event["event"] == "viewbox": if event["event"] == "viewbox":
pass pass
if event["event"] == "stroke": if event["event"] == "stroke":

View File

@ -56,7 +56,7 @@ class StrokeGroup {
let cmd = ""; let cmd = "";
for (let stroke of strokes) { for (let stroke of strokes) {
if (!last_stroke) { if (!last_stroke) {
d += `M${stroke[0]},${stroke[1]} `; d += `M${stroke[0] * this.player.dimensions[0]},${stroke[1] * this.player.dimensions[1]} `;
cmd = 'M'; cmd = 'M';
} else { } else {
if (last_stroke[2] == 1) { if (last_stroke[2] == 1) {
@ -67,7 +67,7 @@ class StrokeGroup {
cmd = 'l'; cmd = 'l';
} }
let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]]; let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += `${rel_stroke[0]},${rel_stroke[1]} `; d += `${rel_stroke[0] * this.player.dimensions[0]},${rel_stroke[1] * this.player.dimensions[1]} `;
} }
last_stroke = stroke; last_stroke = stroke;

View File

@ -61,7 +61,7 @@ class Canvas {
}) })
this.colors = ["black", "#cc1414", "blue", "green"]; this.colors = ["black", "red", "blue", "green"];
this.resize(); this.resize();
@ -292,15 +292,14 @@ class Canvas {
getCoordinates(e) { getCoordinates(e) {
// convert event coordinates into relative positions on x & y axis // convert event coordinates into relative positions on x & y axis
let box = this.svgEl.getBoundingClientRect(); let box = this.svgEl.getBoundingClientRect();
let x = (e.x - box['left'] + this.viewbox.x); let x = (e.x - box['left'] + this.viewbox.x) / box['width'];
let y = (e.y - box['top'] + this.viewbox.y); let y = (e.y - box['top'] + this.viewbox.y) / box['height'];
return { 'x': x, 'y': y }; return { 'x': x, 'y': y };
} }
// isInsideBounds(pos) { isInsideBounds(pos) {
// let box = this.svgEl.getBoundingClientRect(); return !(pos['x'] < 0 || pos['y'] < 0 || pos['x'] > 1 || pos['y'] > 1);
// return !(pos['x'] < 0 || pos['y'] < 0 || pos['x'] > box['width'] || pos['y'] > box['height']); }
// }
draw(e) { draw(e) {
@ -378,7 +377,7 @@ class Canvas {
let cmd = ""; let cmd = "";
for (let stroke of strokes) { for (let stroke of strokes) {
if (!last_stroke) { if (!last_stroke) {
d += `M${stroke[0]},${stroke[1]} `; d += `M${stroke[0] * this.viewbox.width},${stroke[1] * this.viewbox.height} `;
cmd = 'M'; cmd = 'M';
} else { } else {
if (last_stroke[2] == 1) { if (last_stroke[2] == 1) {
@ -389,7 +388,7 @@ class Canvas {
cmd = 'l'; cmd = 'l';
} }
let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]]; let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += `${rel_stroke[0]},${rel_stroke[1]} `; d += `${rel_stroke[0] * this.viewbox.width},${rel_stroke[1] * this.viewbox.height} `;
} }
last_stroke = stroke; last_stroke = stroke;

View File

@ -74,13 +74,13 @@ class Player {
if ( if (
this.currentPathI < this.inPointPosition[0] || this.currentPathI < this.inPointPosition[0] ||
this.currentPointI < this.inPointPosition[1]) { this.currentPointI < this.inPointPosition[1]) {
this.drawStrokePosition( this.drawStrokePosition(
// this.inPointPosition[0], // this.inPointPosition[0],
// this.inPointPosition[1], // this.inPointPosition[1],
// always draw at out position, as to see the whole shape of the range // always draw at out position, as to see the whole shape of the range
this.outPointPosition[0], this.outPointPosition[0],
this.outPointPosition[1], this.outPointPosition[1],
); );
} }
} }
if (handle === 1) { if (handle === 1) {
@ -122,7 +122,7 @@ class Player {
// inactive is what comes before and after. // inactive is what comes before and after.
// then, playing the video is just running pathRanghe(0, playhead) // then, playing the video is just running pathRanghe(0, playhead)
drawStrokePosition(path_i, point_i, show_all) { drawStrokePosition(path_i, point_i, show_all) {
if (typeof show_all === 'undefined') if(typeof show_all === 'undefined')
show_all = false; show_all = false;
// check if anything is placed that is in the future from the current playhead // check if anything is placed that is in the future from the current playhead
@ -194,7 +194,7 @@ class Player {
} }
// when an outpoint is set, stop playing there // when an outpoint is set, stop playing there
if (this.outPointPosition && (next_path > this.outPointPosition[0] || next_point > this.outPointPosition[1])) { if(next_path > this.outPointPosition[0] || next_point > this.outPointPosition[1]){
return [null, null]; return [null, null];
} }
@ -202,12 +202,12 @@ class Player {
} }
playStrokePosition(path_i, point_i, allow_interrupt) { playStrokePosition(path_i, point_i, allow_interrupt) {
if (allow_interrupt) { if(allow_interrupt) {
if (!this.isPlaying) { if(!this.isPlaying) {
console.log('not playing because of interrupt'); console.log('not playing because of interrupt');
return; return;
} }
} else { } else{
this.isPlaying = true; this.isPlaying = true;
} }
this.drawStrokePosition(path_i, point_i); this.drawStrokePosition(path_i, point_i);
@ -281,7 +281,7 @@ class Player {
let cmd = ""; let cmd = "";
for (let stroke of strokes) { for (let stroke of strokes) {
if (!last_stroke) { if (!last_stroke) {
d += `M${stroke[0]},${stroke[1]} `; d += `M${stroke[0] * this.dimensions[0]},${stroke[1] * this.dimensions[1]} `;
cmd = 'M'; cmd = 'M';
} else { } else {
if (last_stroke[2] == 1) { if (last_stroke[2] == 1) {
@ -292,7 +292,7 @@ class Player {
cmd = 'l'; cmd = 'l';
} }
let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]]; let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += `${rel_stroke[0]},${rel_stroke[1]} `; d += `${rel_stroke[0] * this.dimensions[0]},${rel_stroke[1] * this.dimensions[1]} `;
} }
last_stroke = stroke; last_stroke = stroke;