From 111eab6cf029b1e55e3ee105d6cadbf07cbdf2f9 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Mon, 22 Nov 2021 20:54:04 +0100 Subject: [PATCH] Rough draft for drawing --- .python-version | 1 + files/.gitignore | 2 + log/.gitignore | 2 + pyproject.toml | 16 +++ webserver.py | 264 +++++++++++++++++++++++++++++++++++++++++++++++ www/cursor.png | Bin 0 -> 7434 bytes www/draw.html | 164 +++++++++++++++++++++++++++++ www/draw.js | 244 +++++++++++++++++++++++++++++++++++++++++++ www/play.html | 74 +++++++++++++ www/play.js | 147 ++++++++++++++++++++++++++ 10 files changed, 914 insertions(+) create mode 100644 .python-version create mode 100644 files/.gitignore create mode 100644 log/.gitignore create mode 100644 pyproject.toml create mode 100644 webserver.py create mode 100644 www/cursor.png create mode 100644 www/draw.html create mode 100644 www/draw.js create mode 100644 www/play.html create mode 100644 www/play.js diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2009c7d --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.2 diff --git a/files/.gitignore b/files/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/files/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/log/.gitignore b/log/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d364da2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "svganim" +version = "0.1.0" +description = "Draw an animated vector image" +authors = ["Ruben van de Ven "] + +[tool.poetry.dependencies] +python = "^3.9" +tornado = "^6.1" +coloredlogs = "^15.0.1" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/webserver.py b/webserver.py new file mode 100644 index 0000000..8a43ea5 --- /dev/null +++ b/webserver.py @@ -0,0 +1,264 @@ +import json +import logging +import os +import tornado.ioloop +import tornado.web +import tornado.websocket +from urllib.parse import urlparse +import uuid +import datetime +import html +import argparse +import coloredlogs +import glob +import csv + + + + +class DateTimeEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, datetime.datetime): + return o.isoformat(timespec='milliseconds') + + return super().default(self, o) + +class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler): + def set_extra_headers(self, path): + """For subclass to add extra headers to the response""" + if path[-5:] == '.html': + self.set_header("Access-Control-Allow-Origin", "*") + if path[-4:] == '.svg': + self.set_header("Content-Type", "image/svg+xml") + + +class WebSocketHandler(tornado.websocket.WebSocketHandler): + """ + Websocket from the workers + """ + CORS_ORIGINS = ['localhost'] + connections = set() + + def initialize(self, config): + self.config = config + self.strokes = [] + self.hasWritten = False + self.dimensions = [None, None] + + def check_origin(self, origin): + parsed_origin = urlparse(origin) + # parsed_origin.netloc.lower() gives localhost:3333 + valid = any([parsed_origin.hostname.endswith(origin) for origin in self.CORS_ORIGINS]) + return valid + + # the client connected + def open(self, p = None): + self.__class__.connections.add(self) + self.strokes = [] + self.prefix = datetime.datetime.now().strftime('%Y-%m-%d-') + self.filename = self.prefix + str(self.check_filenr()) + '-' + uuid.uuid4().hex[:6] + print(self.filename) + self.write_message(json.dumps({ + "filename": self.filename + })) + + def check_filenr(self): + files = glob.glob(os.path.join(self.config.storage, self.prefix +'*')) + return len(files) + 1 + + + # the client sent the message + def on_message(self, message): + logger.info(f"recieve: {message}") + + try: + msg = json.loads(message) + if msg['action'] == 'stroke': + print('stroke!') + self.strokes.append([msg['color'], msg['points']]) + + with open(os.path.join(self.config.storage,self.filename +'.csv'), 'a') as fp: + writer = csv.writer(fp, delimiter=';') + if not self.hasWritten: + #metadata to first row, but only on demand + writer.writerow([datetime.datetime.now().strftime("%Y-%m-%d %T"), self.dimensions[0], self.dimensions[1]]) + self.hasWritten = True + + # first column is color, rest is points + writer.writerow([msg['color']] +[coordinate for point in msg['points'] for coordinate in point[:4]]) + + + elif msg['action'] == 'dimensions': + self.dimensions = [int(msg['width']), int(msg['height'])] + logger.info(f"{self.dimensions=}") + + + else: + # self.send({'alert': 'Unknown request: {}'.format(message)}) + logger.warn('Unknown request: {}'.format(message)) + + except Exception as e: + # self.send({'alert': 'Invalid request: {}'.format(e)}) + logger.exception(e) + + # client disconnected + def on_close(self): + self.__class__.rmConnection(self) + + logger.info(f"Client disconnected: {self.request.remote_ip}") + + + @classmethod + def rmConnection(cls, client): + if client not in cls.connections: + return + cls.connections.remove(client) + + @classmethod + def hasConnection(cls, client): + return client in cls.connections + + +class AnimationHandler(tornado.web.RequestHandler): + def initialize(self, config): + self.config = config + + def get(self, filename): + self.set_header("Content-Type", "application/json") + # filename = self.get_argument("file", None) + if filename == '': + names = [f"/files/{name[:-4]}" for name in os.listdir(self.config.storage) if name not in ['.gitignore']] + self.write(json.dumps(names)) + else: + path = os.path.join(self.config.storage,os.path.basename(filename)+".csv") + drawing = { + "shape": [] + } + with open(path, 'r') as fp: + strokes = csv.reader(fp,delimiter=';') + for i, stroke in enumerate(strokes): + if i == 0: + # metadata on first line + drawing['time'] = stroke[0] + drawing['dimensions'] = [stroke[1], stroke[2]] + continue + color = stroke.pop(0) + points = [] + for i in range(int(len(stroke) / 4)): + p = stroke[i*4:i*4+4] + points.append([float(p[0]), float(p[1]), int(p[2]), float(p[3])]) + drawing['shape'].append({ + 'color': color, + 'points': points + }) + self.write(json.dumps(drawing)) + +def strokes2D(strokes): + # strokes to a d attribute for a path + d = ""; + last_stroke = None; + cmd = ""; + for stroke in strokes: + if not last_stroke: + d += f"M{stroke[0]},{stroke[1]} " + cmd = 'M' + else: + if last_stroke[2] == 1: + d += " m" + cmd = 'm' + elif cmd != 'l': + d+=' l ' + cmd = 'l' + + 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; + +class Server: + """ + Server for HIT -> plotter events + As well as for the Status interface + """ + loop = None + + def __init__(self, config, logger): + self.config = config + self.logger = logger + + #self.config['server']['port'] + self.web_root = os.path.join('www') + + + def start(self): + application = tornado.web.Application([ + (r"/ws(.*)", WebSocketHandler, { + 'config': self.config, + }), + + (r"/files/(.*)", AnimationHandler, + {'config': self.config}), + (r"/(.*)", StaticFileWithHeaderHandler, + {"path": self.web_root}), + ], debug=True, autoreload=True) + application.listen(self.config.port) + tornado.ioloop.IOLoop.current().start() + + +if __name__ == "__main__": + argParser = argparse.ArgumentParser( + description='Start up the vector animation server') + # argParser.add_argument( + # '--config', + # '-c', + # required=True, + # type=str, + # help='The yaml config file to load' + # ) + argParser.add_argument( + '--port', + type=int, + default=7890, + help='Port' + ) + argParser.add_argument( + '--storage', + type=str, + default='files', + help='directory name for output files' + ) + argParser.add_argument( + '--verbose', + '-v', + action='count', default=0 + ) + + args = argParser.parse_args() + + loglevel = logging.NOTSET if args.verbose > 1 else logging.DEBUG if args.verbose > 0 else logging.INFO + + coloredlogs.install( + level=loglevel, +# default: "%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s" + fmt="%(asctime)s %(hostname)s %(name)s[%(process)d,%(threadName)s] %(levelname)s %(message)s" + ) + + # File logging + formatter = logging.Formatter(fmt='%(asctime)s %(module)s:%(lineno)d %(levelname)8s | %(message)s', + datefmt='%Y/%m/%d %H:%M:%S') # %I:%M:%S %p AM|PM format + logFileHandler = logging.handlers.RotatingFileHandler( + 'log/draw_log.log', + maxBytes=1024*512, + backupCount=5 + ) + logFileHandler.setFormatter(formatter) + + logger = logging.getLogger("sorteerhoed") + logger.addHandler( + logFileHandler + ) + logger.info("Start server") + + server = Server(args, logger) + server.start() + diff --git a/www/cursor.png b/www/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..ad600de337902dfd18b178d6463c5b83eaf11c6f GIT binary patch literal 7434 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 IM6N<$g7qFs5C8xG literal 0 HcmV?d00001 diff --git a/www/draw.html b/www/draw.html new file mode 100644 index 0000000..c16773b --- /dev/null +++ b/www/draw.html @@ -0,0 +1,164 @@ + + + + + Draw a line animation + + + +
+
+ + + + + diff --git a/www/draw.js b/www/draw.js new file mode 100644 index 0000000..28b7a74 --- /dev/null +++ b/www/draw.js @@ -0,0 +1,244 @@ +class Canvas { + constructor(wrapperEl) { + this.allowDrawing = false; + this.url = window.location.origin.replace('http', 'ws') + '/ws?' + window.location.search.substring(1); + this.wrapperEl = wrapperEl; + this.wrapperEl.classList.add('closed'); + + this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.wrapperEl.appendChild(this.svgEl); + + this.toolboxEl = document.createElement('div'); + this.toolboxEl.classList.add('toolbox') + this.wrapperEl.appendChild(this.toolboxEl); + + this.filenameEl = document.createElement('div'); + this.filenameEl.classList.add('filename') + this.wrapperEl.appendChild(this.filenameEl); + + + this.colors = ["red", "blue", "green"]; + + this.resize(); + + window.addEventListener('resize', this.requestResize); + + + this.paths = []; + this.isDrawing = false; + this.hasMouseDown = false; + this.currentStrokeEl = null; + + this.startTime = null; + + document.body.addEventListener('mousemove', this.draw.bind(this)); + document.body.addEventListener('mouseup', this.penup.bind(this)); + this.svgEl.addEventListener('mousedown', this.startStroke.bind(this)); + + this.createToolbox(); + + this.setColor(this.colors[0]); + + + this.socket = new WebSocket(this.url); + this.socket.addEventListener('open', (e) => { + + this.socket.send(JSON.stringify({ + 'action': 'dimensions', + 'width': this.width, + 'height': this.height + })); + }) + this.socket.addEventListener('message', (e) => { + let msg = JSON.parse(e.data); + console.log('receive', msg); + if (msg.hasOwnProperty('filename')) { + console.log('filename', msg.filename); + this.setFilename(msg.filename); + this.openTheFloor() + } + }); + } + + openTheFloor() { + this.wrapperEl.classList.remove('closed'); + } + + setFilename(filename) { + this.filename = filename; + this.filenameEl.innerText = filename; + } + + createToolbox() { + const colorsEl = document.createElement('ul'); + colorsEl.classList.add('colors'); + for (let color of this.colors) { + const colorEl = document.createElement('li'); + colorEl.style.background = color; + colorEl.addEventListener('click', (e) => { + console.log('set color', color) + this.setColor(color); + + }) + + colorsEl.appendChild(colorEl); + } + this.toolboxEl.appendChild(colorsEl); + } + + setColor(color) { + this.currentColor = color; + const colorEls = this.toolboxEl.querySelectorAll('.colors li'); + for (let colorEl of colorEls) { + if (colorEl.style.backgroundColor == color) { + colorEl.classList.add('selected'); + } + else { + colorEl.classList.remove('selected'); + } + } + } + + resize() { + this.width = window.innerWidth; + this.height = window.innerHeight; + const viewBox = `0 0 ${this.width} ${this.height}`; + this.svgEl.setAttribute('viewBox', viewBox); + this.svgEl.setAttribute('width', this.width + 'mm'); + this.svgEl.setAttribute('height', this.height + 'mm'); + } + + requestResize() { + alert('Resize not implemented yet. Please reloade the page'); + } + + + getCoordinates(e) { + // convert event coordinates into relative positions on x & y axis + let box = this.svgEl.getBoundingClientRect(); + let x = (e.x - box['left']) / box['width']; + let y = (e.y - box['top']) / box['height']; + return { 'x': x, 'y': y }; + } + + isInsideBounds(pos) { + return !(pos['x'] < 0 || pos['y'] < 0 || pos['x'] > 1 || pos['y'] > 1); + } + + // isInsideDrawingBounds (pos) { + // if(pos['x'] > xPadding && pos['x'] < (xPadding+drawWidthFactor) && pos['y'] > yPadding && pos['y'] < yPadding+drawHeightFactor) { + // return true; + // } + // return false; + // } + + draw(e) { + let pos = this.getCoordinates(e); + if (this.hasMouseDown) + console.log(pos, e); + + // if(!isInsideBounds(pos)) { + // // outside of bounds + // return; + // } + + // if(!e.buttons || (this.isDrawing && !this.isInsideBounds(pos))){ + // this.endStroke(pos); + // } + if (!this.isDrawing && this.hasMouseDown && this.isInsideBounds(pos)) { + this.isDrawing = true; + } + + if (this.isDrawing) { + this.paths[this.paths.length - 1].points.push([pos['x'], pos['y'], 0, window.performance.now() - this.startTime]); + let d = this.strokes2D(this.paths[this.paths.length - 1].points); + this.currentStrokeEl.setAttribute('d', d); + } + + //console.log([pos['x'], pos['y']], isDrawing); + // socket.send(JSON.stringify({ + // 'action': 'move', + // 'direction': [pos['x'], pos['y']], + // 'mouse': isDrawing, + // })); + } + + startStroke(e) { + this.hasMouseDown = true; + console.log('start'); + + const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + strokeEl.style.stroke = this.currentColor; + this.svgEl.appendChild(strokeEl); + this.currentStrokeEl = strokeEl; + this.paths.push({ + 'el': strokeEl, + 'color': this.currentColor, + 'points': [] + }); + + if (this.startTime === null) { + // initiate timer on first stroke + this.startTime = window.performance.now(); + } + } + + endStroke(pos) { + console.log(this.isDrawing) + if (!this.isDrawing) { + return; + } + console.log("send!") + this.isDrawing = false; + //document.body.removeEventListener('mousemove', draw); + + + if (this.paths[this.paths.length - 1].points.length > 0) { + // mark point as last of stroke + this.paths[this.paths.length - 1].points[this.paths[this.paths.length - 1].points.length - 1][2] = 1; + } + const stroke = this.paths[this.paths.length - 1]; + this.socket.send(JSON.stringify({ + 'action': 'stroke', + 'color': stroke.color, + 'points': stroke.points + })); + } + + penup(e) { + if (!this.hasMouseDown) { + return; + } + this.hasMouseDown = false; + + let pos = this.getCoordinates(e); + this.endStroke(pos); + } + + strokes2D(strokes) { + // strokes to a d attribute for a path + let d = ""; + let last_stroke = undefined; + let cmd = ""; + for (let stroke of strokes) { + if (!last_stroke) { + d += `M${stroke[0] * this.width},${stroke[1] * this.height} `; + cmd = 'M'; + } else { + if (last_stroke[2] == 1) { + d += " m"; + cmd = 'm'; + } else if (cmd != 'l') { + d += ' l '; + cmd = 'l'; + } + let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]]; + d += `${rel_stroke[0] * this.width},${rel_stroke[1] * this.height} `; + } + last_stroke = stroke; + + } + return d; + } + +} diff --git a/www/play.html b/www/play.html new file mode 100644 index 0000000..1ea23c6 --- /dev/null +++ b/www/play.html @@ -0,0 +1,74 @@ + + + + + Play a line animation + + + +
+
+ + + + diff --git a/www/play.js b/www/play.js new file mode 100644 index 0000000..acfd029 --- /dev/null +++ b/www/play.js @@ -0,0 +1,147 @@ + + +class Player { + constructor(wrapperEl) { + this.wrapperEl = wrapperEl; + this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.wrapperEl.appendChild(this.svgEl); + + this.resize(); + } + + playlist(url) { + const request = new Request(url, { + method: 'GET', + }); + + + fetch(request) + .then(response => response.json()) + .then(data => { + let playlist = this.wrapperEl.querySelector('.playlist'); + if(!playlist) { + playlist = document.createElement('nav'); + playlist.classList.add('playlist'); + this.wrapperEl.appendChild(playlist) + } + else{ + playlist.innerHTML = ""; + } + + const listEl = document.createElement("ul"); + for(let fileUrl of data) { + const liEl = document.createElement("li"); + liEl.innerText = fileUrl + liEl.addEventListener('click', (e) => { + this.play(fileUrl); + + }); + listEl.appendChild(liEl); + } + playlist.appendChild(listEl); + // do something with the data sent in the request + }); + } + + play(file) { + const request = new Request(file, { + method: 'GET', + }); + + + fetch(request) + .then(response => response.json()) + .then(data => { + this.playStrokes(data) + // do something with the data sent in the request + }); + + } + + playStrokes(drawing){ + this.strokes = drawing.shape; + this.currentPath = null; + this.dimensions = drawing.dimensions; + this.svgEl.setAttributeNS('http://www.w3.org/2000/svg', 'viewBox', `0 0 ${this.dimensions.width} ${this.dimensions.height}`) + this.startTime = window.performance.now() - this.strokes[0].points[0][3]; + this.playStroke(0,1); + } + + playStroke(path_i, point_i){ + const path = this.strokes[path_i]; + // console.log(path); + let pathEl = this.svgEl.querySelector(`.path${path_i}`); + if(!pathEl){ + pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + pathEl.style.stroke = path.color; + pathEl.classList.add('path'+path_i) + this.svgEl.appendChild(pathEl) + } + + const d = this.strokes2D(path.points.slice(0, point_i)); + pathEl.setAttribute('d', d); + + let next_path, next_point,t; + if(path.points.length > point_i + 1){ + next_path = path_i; + next_point = point_i + 1; + t = path.points[next_point][3];// - path.points[point_i][3]; + // setTimeout(() => this.playStroke(next_path, next_point), dt); + } else if(this.strokes.length > path_i + 1) { + next_path = path_i + 1; + next_point = 1; + t = this.strokes[next_path].points[next_point][3];// - path.points[point_i][3]; + // use starttime instead of diff, to prevent floating + } else { + console.log('done'); + return + } + + const dt = t - (window.performance.now() - this.startTime); + setTimeout(() => this.playStroke(next_path, next_point), dt); + } + + playUntil(path_i){ + // for scrubber + } + + resize() { + this.width = window.innerWidth; + this.height = window.innerHeight; + const viewBox = `0 0 ${this.width} ${this.height}`; + this.svgEl.setAttribute('viewBox', viewBox); + this.svgEl.setAttribute('width', this.width + 'mm'); + this.svgEl.setAttribute('height', this.height + 'mm'); + } + + requestResize() { + alert('Resize not implemented yet. Please reloade the page'); + } + + + strokes2D(strokes) { + // strokes to a d attribute for a path + let d = ""; + let last_stroke = undefined; + let cmd = ""; + for (let stroke of strokes) { + if (!last_stroke) { + d += `M${stroke[0] * this.width},${stroke[1] * this.height} `; + cmd = 'M'; + } else { + if (last_stroke[2] == 1) { + d += " m"; + cmd = 'm'; + } else if (cmd != 'l') { + d += ' l '; + cmd = 'l'; + } + let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]]; + d += `${rel_stroke[0] * this.width},${rel_stroke[1] * this.height} `; + } + last_stroke = stroke; + + } + return d; + } +} \ No newline at end of file