Rough draft for drawing
This commit is contained in:
commit
111eab6cf0
10 changed files with 914 additions and 0 deletions
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.9.2
|
2
files/.gitignore
vendored
Normal file
2
files/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
2
log/.gitignore
vendored
Normal file
2
log/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "svganim"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Draw an animated vector image"
|
||||||
|
authors = ["Ruben van de Ven <git@rubenvandeven.com>"]
|
||||||
|
|
||||||
|
[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"
|
264
webserver.py
Normal file
264
webserver.py
Normal file
|
@ -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()
|
||||||
|
|
BIN
www/cursor.png
Normal file
BIN
www/cursor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
164
www/draw.html
Normal file
164
www/draw.html
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Draw a line animation</title>
|
||||||
|
<style media="screen">
|
||||||
|
#sample, svg{
|
||||||
|
position:absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right:0;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
z-index:2;
|
||||||
|
cursor: url('/cursor.png') 6 6, auto;
|
||||||
|
}
|
||||||
|
img{
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
bottom:0;
|
||||||
|
right:0;
|
||||||
|
left:0;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
z-index:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: none;
|
||||||
|
stroke: red;
|
||||||
|
stroke-width: 1mm;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper {
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
right:0;
|
||||||
|
bottom:0;
|
||||||
|
left:0;
|
||||||
|
background:none;
|
||||||
|
}
|
||||||
|
.gray{
|
||||||
|
position:absolute;
|
||||||
|
background:rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
html, body{
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin:0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
#interface{
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: calc({HEIGHT}/{WIDTH} * 100%);
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 15px;
|
||||||
|
width: 600px;
|
||||||
|
left: calc(50% - 250px);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
.buttons{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#submit{
|
||||||
|
background: lightblue;
|
||||||
|
border: solid 1px blue;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 110%;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.toolbox{
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50px;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: white;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
background-color:#ccc;
|
||||||
|
}
|
||||||
|
.toolbox > ul{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.toolbox > ul > li{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox .colors > li.selected {
|
||||||
|
border-width: 3px;
|
||||||
|
border-color: gray;
|
||||||
|
}
|
||||||
|
.toolbox .colors > li {
|
||||||
|
display: block;
|
||||||
|
border: solid 3px white;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
color:gray;
|
||||||
|
z-index: -1;
|
||||||
|
font-size:8pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closed{
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
.closed svg{
|
||||||
|
cursor:wait;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='interface'>
|
||||||
|
</div>
|
||||||
|
<!-- <div id='wrapper'>
|
||||||
|
<svg id="canvas" preserveAspectRatio="none">
|
||||||
|
<path d="" id="stroke" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id='info'>
|
||||||
|
<div class='buttons'>
|
||||||
|
|
||||||
|
<button id='submit'>Submit</button>
|
||||||
|
<form method='post' action='' id='finishedForm'>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toolbox">
|
||||||
|
<ul class="colors">
|
||||||
|
<li class="red"></li>
|
||||||
|
<li class="blue"></li>
|
||||||
|
<li class="green"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<script src="draw.js"></script>
|
||||||
|
<script type='text/javascript'>
|
||||||
|
const canvas = new Canvas( document.getElementById("interface"));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
244
www/draw.js
Normal file
244
www/draw.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
74
www/play.html
Normal file
74
www/play.html
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Play a line animation</title>
|
||||||
|
<style media="screen">
|
||||||
|
#sample, svg{
|
||||||
|
position:absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right:0;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
z-index:2;
|
||||||
|
cursor: url('/cursor.png') 6 6, auto;
|
||||||
|
}
|
||||||
|
img{
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
bottom:0;
|
||||||
|
right:0;
|
||||||
|
left:0;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
z-index:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: none;
|
||||||
|
stroke: gray;
|
||||||
|
stroke-width: 1mm;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper {
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
right:0;
|
||||||
|
bottom:0;
|
||||||
|
left:0;
|
||||||
|
background:none;
|
||||||
|
}
|
||||||
|
.gray{
|
||||||
|
position:absolute;
|
||||||
|
background:rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
html, body{
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin:0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist{
|
||||||
|
position: absolute;
|
||||||
|
left:0;
|
||||||
|
top:0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='interface'>
|
||||||
|
</div>
|
||||||
|
<script src="play.js"></script>
|
||||||
|
<script type='text/javascript'>
|
||||||
|
const player = new Player( document.getElementById("interface"));
|
||||||
|
player.playlist('/files/');
|
||||||
|
// player.play('/files/2021-11-22-3-75b0f0');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
147
www/play.js
Normal file
147
www/play.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue