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')
class Annotation:
def __init__(self, tag: str, drawing: Drawing, t_in: float, t_out: float) -> None:
self.tag = tag
@ -24,6 +23,7 @@ class Annotation:
def id(self) -> str:
return f'{self.drawing.id}:{self.tag}:{self.t_in}:{self.t_out}'
def getAnimationSlice(self) -> AnimationSlice:
return self.drawing.get_animation().getSlice(self.t_in, self.t_out)
@ -96,8 +96,7 @@ class Drawing:
strokes.append(
Stroke(
event["color"],
[Point.fromTuple(tuple(p))
for p in event["points"]],
[Point.fromTuple(tuple(p)).scaled(self.get_canvas_metadata()['dimensions']) for p in event["points"]],
)
)
return AnimationSlice(strokes, audioslice=self.get_audio() )
@ -159,12 +158,13 @@ class AnimationSlice:
return Viewbox(min_x, min_y, max_x - min_x, max_y - min_y)
def getSlice(self, t_in: float, t_out: float) -> AnimationSlice:
frame_in = self.getIndexForInPoint(t_in)
frame_out = self.getIndexForOutPoint(t_out)
frame_in = self.getIndexForTime(t_in)
frame_out = self.getIndexForTime(t_out)
strokes = self.getStrokeSlices(frame_in, frame_out)
audio = self.audio.getSlice(t_in, t_out) if self.audio else None
return AnimationSlice(strokes, t_in, t_out, audio)
def get_as_svg_dwg(self) -> svgwrite.Drawing:
box = self.get_bounding_box()
(_, fn) = tempfile.mkstemp(suffix='.svg', text=True)
@ -200,47 +200,11 @@ class AnimationSlice:
break
in_i = index_in[1] if index_in[0] == i else 0
out_i = index_out[1] if index_out[0] == i else len(
stroke.points) - 1
out_i = index_out[1] if index_out[0] == i else len(stroke.points) - 1
slices.append(StrokeSlice(stroke, in_i, out_i))
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:
"""Get the frame index (path, point) based on the given time
Equal to annotations.js findPositionForTime(ms)
@ -257,7 +221,7 @@ class AnimationSlice:
# we are getting close, find the right point_i
path_i = i
for pi, point in enumerate(stroke.points):
if point.t > ms:
if point.t:
break # too far
point_i = pi
break # done :-)
@ -287,18 +251,15 @@ class AudioSlice:
end = self.t_out - self.offset
if start < 0 and end < 0:
extract = AudioSegment.silent(
duration=end-start, frame_rate=song.frame_rate)
extract = AudioSegment.silent(duration=end-start, frame_rate=song.frame_rate)
else:
if start < 0:
preroll = AudioSegment.silent(
duration=start * -1, frame_rate=song.frame_rate)
preroll = AudioSegment.silent(duration=start * -1, frame_rate=song.frame_rate)
start = 0
else:
preroll = None
if end > len(song):
postroll = AudioSegment.silent(
duration=end - len(song), frame_rate=song.frame_rate)
postroll = AudioSegment.silent(duration=end - len(song), frame_rate=song.frame_rate)
end = len(song) - 1
else:
postroll = None
@ -321,8 +282,7 @@ class AnnotationIndex:
self.drawing_dir = drawing_dir
self.metadata_dir = metadata_dir
# disable disk cache because of glitches shelve.open(filename, writeback=True)
self.shelve = {}
self.shelve = {} #disable disk cache because of glitches shelve.open(filename, writeback=True)
def refresh(self):
# reset the index
@ -345,8 +305,7 @@ class AnnotationIndex:
if 'annotations' not in meta:
continue
for ann in meta['annotations']:
annotation = Annotation(
ann['tag'], drawing, ann['t_in'], ann['t_out'])
annotation = Annotation(ann['tag'], drawing, ann['t_in'], ann['t_out'])
self.shelve['_annotations'][annotation.id] = annotation
if annotation.tag not in self.shelve['_tags']:
self.shelve['_tags'][annotation.tag] = [annotation]
@ -355,6 +314,7 @@ class AnnotationIndex:
annotation
)
@property
def drawings(self) -> dict[str, Drawing]:
return self.shelve["_drawings"]
@ -403,25 +363,23 @@ class Point:
def fromTuple(cls, p: tuple[float, float, int, float]):
return cls(p[0], p[1], bool(p[2]), p[3])
def scaledToFit(self, dimensions: dict[str, float]) -> Point:
# TODO: change so that it actually scales to FIT dimensions
return Point(self.x, self.y, self.last, self.t)
def scaled(self, dimensions: dict[str, float]) -> Point:
return Point(self.x*dimensions['width'], self.y * dimensions['height'], self.last, self.t)
Points = list[Point]
SvgDrawing = Union[svgwrite.container.SVG, svgwrite.container.Group]
class Stroke:
def __init__(self, color: str, points: Points) -> None:
self.color = color
self.points = points
def add_to_dwg(self, dwg: SvgDrawing):
path = svgwrite.path.Path(d=self.get_as_d()).stroke(
self.color, 1).fill("none")
path = svgwrite.path.Path(d=self.get_as_d()).stroke(self.color,1).fill("none")
dwg.add(path)
def get_bounding_box(self) -> Viewbox:
min_x, max_x = float("inf"), float("-inf")
min_y, max_y = float("inf"), float("-inf")
@ -483,9 +441,9 @@ class StrokeSlice(Stroke):
def strokes2D(strokes):
# strokes to a d attribute for a path
d = ""
last_stroke = None
cmd = ""
d = "";
last_stroke = None;
cmd = "";
for stroke in strokes:
if not last_stroke:
d += f"M{stroke[0]},{stroke[1]} "
@ -498,8 +456,7 @@ def strokes2D(strokes):
d+=' l '
cmd = 'l'
rel_stroke = [stroke[0] - last_stroke[0],
stroke[1] - last_stroke[1]]
rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += f"{rel_stroke[0]},{rel_stroke[1]} "
last_stroke = stroke
return d

View File

@ -47,7 +47,6 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.strokes = []
self.hasWritten = False
self.prev_file = None
self.prev_file_duration = 0
self.dimensions = [None, None]
# def check_origin(self, origin):
@ -127,42 +126,15 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.prev_file = prev_file
metadata = self.getFileMetadata(self.prev_file)
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:
with open(self.prev_file, "r") as fp:
first_line = fp.readline().strip()
if first_line.endswith(","):
first_line = first_line[:-1]
metadata = json.loads(first_line)
return metadata
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")
self.write_message(json.dumps(
{"preloaded_svg": f"/drawing/{file}", "dimensions": [metadata[1], metadata[2]]}))
# the client sent the message
def on_message(self, message):
@ -172,19 +144,12 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
msg = json.loads(message)
if msg["event"] == "stroke":
logger.info("stroke")
for i in range(len(msg['points'])):
msg['points'][i][3] += self.prev_file_duration
self.appendEvent(msg)
elif msg["event"] == "dimensions":
self.dimensions = [int(msg["width"]), int(msg["height"])]
logger.info(f"{self.dimensions=}")
elif msg["event"] == "viewbox":
logger.info("move or resize")
if len(msg['viewboxes']) == 0:
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":
self.preloadFile(msg["file"])
@ -288,9 +253,6 @@ class AnimationHandler(tornado.web.RequestHandler):
drawing["time"] = event[0]
drawing["dimensions"] = [event[1], event[2]]
else:
if type(event) is list:
# ignore double metadatas, which appear when continuaing an existing drawing
continue
if event["event"] == "viewbox":
pass
if event["event"] == "stroke":

View File

@ -56,7 +56,7 @@ class StrokeGroup {
let cmd = "";
for (let stroke of strokes) {
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';
} else {
if (last_stroke[2] == 1) {
@ -67,7 +67,7 @@ class StrokeGroup {
cmd = 'l';
}
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;

View File

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

View File

@ -194,7 +194,7 @@ class Player {
}
// 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];
}
@ -281,7 +281,7 @@ class Player {
let cmd = "";
for (let stroke of strokes) {
if (!last_stroke) {
d += `M${stroke[0]},${stroke[1]} `;
d += `M${stroke[0] * this.dimensions[0]},${stroke[1] * this.dimensions[1]} `;
cmd = 'M';
} else {
if (last_stroke[2] == 1) {
@ -292,7 +292,7 @@ class Player {
cmd = 'l';
}
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;