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