
commit
111eab6cf0
10 changed files with 914 additions and 0 deletions
@ -0,0 +1,16 @@
@@ -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" |
@ -0,0 +1,264 @@
@@ -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() |
||||
|
After Width: | Height: | Size: 7.3 KiB |
@ -0,0 +1,164 @@
@@ -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> |
@ -0,0 +1,244 @@
@@ -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; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,74 @@
@@ -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> |
@ -0,0 +1,147 @@
@@ -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 new issue