drag support in draw, new fileformat to support this
This commit is contained in:
parent
9be9f11ba2
commit
a9c71ac940
2 changed files with 145 additions and 87 deletions
71
webserver.py
71
webserver.py
|
@ -54,10 +54,9 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
# the client connected
|
# the client connected
|
||||||
def open(self, p = None):
|
def open(self, p = None):
|
||||||
self.__class__.connections.add(self)
|
self.__class__.connections.add(self)
|
||||||
self.strokes = []
|
|
||||||
self.prefix = datetime.datetime.now().strftime('%Y-%m-%d-')
|
self.prefix = datetime.datetime.now().strftime('%Y-%m-%d-')
|
||||||
self.filename = self.prefix + str(self.check_filenr()) + '-' + uuid.uuid4().hex[:6]
|
self.filename = self.prefix + str(self.check_filenr()) + '-' + uuid.uuid4().hex[:6]
|
||||||
print(self.filename)
|
logger.info(f"{self.filename=}")
|
||||||
self.write_message(json.dumps({
|
self.write_message(json.dumps({
|
||||||
"filename": self.filename
|
"filename": self.filename
|
||||||
}))
|
}))
|
||||||
|
@ -66,6 +65,18 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
files = glob.glob(os.path.join(self.config.storage, self.prefix +'*'))
|
files = glob.glob(os.path.join(self.config.storage, self.prefix +'*'))
|
||||||
return len(files) + 1
|
return len(files) + 1
|
||||||
|
|
||||||
|
def appendEvent(self, row):
|
||||||
|
# write to an appendable json format. So basically a file that should be wrapped in [] to be json-parsable
|
||||||
|
with open(os.path.join(self.config.storage,self.filename +'.json_appendable'), 'a') as fp:
|
||||||
|
if not self.hasWritten:
|
||||||
|
#metadata to first row, but only on demand
|
||||||
|
fp.write(json.dumps([datetime.datetime.now().strftime("%Y-%m-%d %T"), self.dimensions[0], self.dimensions[1]]))
|
||||||
|
# writer.writerow()
|
||||||
|
self.hasWritten = True
|
||||||
|
|
||||||
|
fp.write(',\n')
|
||||||
|
# first column is color, rest is points
|
||||||
|
fp.write(json.dumps(row))
|
||||||
|
|
||||||
# the client sent the message
|
# the client sent the message
|
||||||
def on_message(self, message):
|
def on_message(self, message):
|
||||||
|
@ -73,26 +84,15 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = json.loads(message)
|
msg = json.loads(message)
|
||||||
if msg['action'] == 'stroke':
|
if msg['event'] == 'stroke':
|
||||||
print('stroke!')
|
logger.info('stroke')
|
||||||
self.strokes.append([msg['color'], msg['points']])
|
self.appendEvent(msg)
|
||||||
|
elif msg['event'] == 'dimensions':
|
||||||
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'])]
|
self.dimensions = [int(msg['width']), int(msg['height'])]
|
||||||
logger.info(f"{self.dimensions=}")
|
logger.info(f"{self.dimensions=}")
|
||||||
|
elif msg['event'] == 'viewbox':
|
||||||
|
logger.info('move or resize')
|
||||||
|
self.appendEvent(msg)
|
||||||
else:
|
else:
|
||||||
# self.send({'alert': 'Unknown request: {}'.format(message)})
|
# self.send({'alert': 'Unknown request: {}'.format(message)})
|
||||||
logger.warn('Unknown request: {}'.format(message))
|
logger.warn('Unknown request: {}'.format(message))
|
||||||
|
@ -143,30 +143,33 @@ class AnimationHandler(tornado.web.RequestHandler):
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
# filename = self.get_argument("file", None)
|
# filename = self.get_argument("file", None)
|
||||||
if filename == '':
|
if filename == '':
|
||||||
names = sorted([f"/files/{name[:-4]}" for name in os.listdir(self.config.storage) if name not in ['.gitignore']])
|
names = sorted([f"/files/{name[:-16]}" for name in os.listdir(self.config.storage) if name.endswith('json_appendable')])
|
||||||
self.write(json.dumps(names))
|
self.write(json.dumps(names))
|
||||||
else:
|
else:
|
||||||
path = os.path.join(self.config.storage,os.path.basename(filename)+".csv")
|
path = os.path.join(self.config.storage,os.path.basename(filename)+".json_appendable")
|
||||||
drawing = {
|
drawing = {
|
||||||
"file": filename,
|
"file": filename,
|
||||||
"shape": []
|
"shape": []
|
||||||
}
|
}
|
||||||
with open(path, 'r') as fp:
|
with open(path, 'r') as fp:
|
||||||
strokes = csv.reader(fp,delimiter=';')
|
events = json.loads('['+fp.read()+']')
|
||||||
for i, stroke in enumerate(strokes):
|
# events = csv.reader(fp,delimiter=';')
|
||||||
|
for i, event in enumerate(events):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
# metadata on first line
|
# metadata on first line
|
||||||
drawing['time'] = stroke[0]
|
drawing['time'] = event[0]
|
||||||
drawing['dimensions'] = [stroke[1], stroke[2]]
|
drawing['dimensions'] = [event[1], event[2]]
|
||||||
continue
|
else:
|
||||||
color = stroke.pop(0)
|
if event['event'] == 'viewbox':
|
||||||
points = []
|
pass
|
||||||
for i in range(int(len(stroke) / 4)):
|
if event['event'] == 'stroke':
|
||||||
p = stroke[i*4:i*4+4]
|
# points = []
|
||||||
points.append([float(p[0]), float(p[1]), int(p[2]), float(p[3])])
|
# 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({
|
drawing['shape'].append({
|
||||||
'color': color,
|
'color': event['color'],
|
||||||
'points': points
|
'points': event['points']
|
||||||
})
|
})
|
||||||
self.write(json.dumps(drawing))
|
self.write(json.dumps(drawing))
|
||||||
|
|
||||||
|
|
155
www/draw.js
155
www/draw.js
|
@ -1,7 +1,10 @@
|
||||||
class Canvas {
|
class Canvas {
|
||||||
constructor(wrapperEl) {
|
constructor(wrapperEl) {
|
||||||
this.allowDrawing = false;
|
this.allowDrawing = false;
|
||||||
|
this.viewbox = { "x": 0, "y": 0, "width": null, "height": null };
|
||||||
this.url = window.location.origin.replace('http', 'ws') + '/ws?' + window.location.search.substring(1);
|
this.url = window.location.origin.replace('http', 'ws') + '/ws?' + window.location.search.substring(1);
|
||||||
|
|
||||||
|
// build the interface
|
||||||
this.wrapperEl = wrapperEl;
|
this.wrapperEl = wrapperEl;
|
||||||
this.wrapperEl.classList.add('closed');
|
this.wrapperEl.classList.add('closed');
|
||||||
|
|
||||||
|
@ -25,15 +28,38 @@ class Canvas {
|
||||||
|
|
||||||
|
|
||||||
this.paths = [];
|
this.paths = [];
|
||||||
|
this.viewboxes = [];
|
||||||
|
this.events = []; // all paths & viewboxes events
|
||||||
this.isDrawing = false;
|
this.isDrawing = false;
|
||||||
this.hasMouseDown = false;
|
this.hasMouseDown = false;
|
||||||
this.currentStrokeEl = null;
|
this.currentStrokeEl = null;
|
||||||
|
|
||||||
this.startTime = null;
|
this.startTime = null;
|
||||||
|
|
||||||
document.body.addEventListener('pointermove', this.draw.bind(this));
|
this.isMoving = false;
|
||||||
document.body.addEventListener('pointerup', this.penup.bind(this));
|
document.body.addEventListener('pointermove', (ev) => {
|
||||||
this.svgEl.addEventListener('pointerdown', this.startStroke.bind(this));
|
if (ev.pointerType == "touch" || ev.buttons & 4) { // 4: middle mouse button
|
||||||
|
this.moveCanvas(ev);
|
||||||
|
} else { // pointerType == pen or mouse
|
||||||
|
this.draw(ev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.body.addEventListener('pointerup', (ev) => {
|
||||||
|
if (ev.pointerType == "touch" || ev.buttons & 4 || this.isMoving) { // buttons is 0 on pointerup
|
||||||
|
this.endMoveCanvas(ev);
|
||||||
|
this.isMoving = false;
|
||||||
|
} else { // pointerType == pen or mouse
|
||||||
|
this.penup(ev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.svgEl.addEventListener('pointerdown', (ev) => {
|
||||||
|
if (ev.pointerType == "touch" || ev.buttons & 4) { // 4: middle mouse button
|
||||||
|
this.isMoving = true;
|
||||||
|
this.startMoveCanvas(ev);
|
||||||
|
} else { // pointerType == pen or mouse
|
||||||
|
this.startStroke(ev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.createToolbox();
|
this.createToolbox();
|
||||||
|
|
||||||
|
@ -42,12 +68,13 @@ class Canvas {
|
||||||
|
|
||||||
this.socket = new WebSocket(this.url);
|
this.socket = new WebSocket(this.url);
|
||||||
this.socket.addEventListener('open', (e) => {
|
this.socket.addEventListener('open', (e) => {
|
||||||
|
const d = {
|
||||||
this.socket.send(JSON.stringify({
|
'event': 'dimensions',
|
||||||
'action': 'dimensions',
|
'width': this.viewbox.width,
|
||||||
'width': this.width,
|
'height': this.viewbox.height
|
||||||
'height': this.height
|
};
|
||||||
}));
|
console.log('send', d);
|
||||||
|
this.socket.send(JSON.stringify(d));
|
||||||
})
|
})
|
||||||
this.socket.addEventListener('message', (e) => {
|
this.socket.addEventListener('message', (e) => {
|
||||||
let msg = JSON.parse(e.data);
|
let msg = JSON.parse(e.data);
|
||||||
|
@ -60,6 +87,40 @@ class Canvas {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startMoveCanvas(ev) {
|
||||||
|
this.moveCanvasPrevPoint = { "x": ev.x, "y": ev.y };
|
||||||
|
this.currentMoves = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
endMoveCanvas(ev) {
|
||||||
|
this.moveCanvasPrevPoint = null;
|
||||||
|
|
||||||
|
// sync viewpoints
|
||||||
|
const d = {
|
||||||
|
'event': 'viewbox',
|
||||||
|
'viewboxes': this.currentMoves
|
||||||
|
};
|
||||||
|
console.log('send', d);
|
||||||
|
this.socket.send(JSON.stringify(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
moveCanvas(ev) {
|
||||||
|
if (this.moveCanvasPrevPoint === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const diff = {
|
||||||
|
"x": ev.x - this.moveCanvasPrevPoint.x,
|
||||||
|
"y": ev.y - this.moveCanvasPrevPoint.y,
|
||||||
|
}
|
||||||
|
this.viewbox.x -= diff.x;
|
||||||
|
this.viewbox.y -= diff.y;
|
||||||
|
this.moveCanvasPrevPoint = { "x": ev.x, "y": ev.y };
|
||||||
|
this.currentMoves.push(Object.assign({'t': window.performance.now() - this.startTime}, this.viewbox));
|
||||||
|
|
||||||
|
this.applyViewBox()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
openTheFloor() {
|
openTheFloor() {
|
||||||
this.wrapperEl.classList.remove('closed');
|
this.wrapperEl.classList.remove('closed');
|
||||||
}
|
}
|
||||||
|
@ -100,12 +161,27 @@ class Canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
this.width = window.innerWidth;
|
this.viewbox.width = window.innerWidth;
|
||||||
this.height = window.innerHeight;
|
this.viewbox.height = window.innerHeight;
|
||||||
const viewBox = `0 0 ${this.width} ${this.height}`;
|
|
||||||
|
this.applyViewBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyViewBox() {
|
||||||
|
const viewBox = `${this.viewbox.x} ${this.viewbox.y} ${this.viewbox.width} ${this.viewbox.height}`;
|
||||||
this.svgEl.setAttribute('viewBox', viewBox);
|
this.svgEl.setAttribute('viewBox', viewBox);
|
||||||
this.svgEl.setAttribute('width', this.width + 'mm');
|
this.svgEl.setAttribute('width', this.viewbox.width + 'mm');
|
||||||
this.svgEl.setAttribute('height', this.height + 'mm');
|
this.svgEl.setAttribute('height', this.viewbox.height + 'mm');
|
||||||
|
|
||||||
|
// todo save drag event;
|
||||||
|
// const newViewbox = Object.assign({}, this.viewbox, {'t': window.performance.now() - this.startTime});
|
||||||
|
// const lastViewbox = this.viewboxes[this.viewboxes.length - 1];
|
||||||
|
// if(newViewbox.x == lastViewbox.x && newViewbox.y == lastViewbox.y && newViewbox.width == lastViewbox.width && newViewbox.height == lastViewbox.height){
|
||||||
|
// // do nothing, avoiding duplicate
|
||||||
|
// } else {
|
||||||
|
// this.viewboxes.push(newViewbox);
|
||||||
|
// this.events.push(newViewbox);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
requestResize() {
|
requestResize() {
|
||||||
|
@ -116,8 +192,8 @@ class Canvas {
|
||||||
getCoordinates(e) {
|
getCoordinates(e) {
|
||||||
// convert event coordinates into relative positions on x & y axis
|
// convert event coordinates into relative positions on x & y axis
|
||||||
let box = this.svgEl.getBoundingClientRect();
|
let box = this.svgEl.getBoundingClientRect();
|
||||||
let x = (e.x - box['left']) / box['width'];
|
let x = (e.x - box['left'] + this.viewbox.x) / box['width'];
|
||||||
let y = (e.y - box['top']) / box['height'];
|
let y = (e.y - box['top'] + this.viewbox.y) / box['height'];
|
||||||
return { 'x': x, 'y': y };
|
return { 'x': x, 'y': y };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,27 +201,11 @@ class Canvas {
|
||||||
return !(pos['x'] < 0 || pos['y'] < 0 || pos['x'] > 1 || pos['y'] > 1);
|
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) {
|
draw(e) {
|
||||||
let pos = this.getCoordinates(e);
|
let pos = this.getCoordinates(e);
|
||||||
if (this.hasMouseDown)
|
|
||||||
console.log(pos, e);
|
|
||||||
|
|
||||||
// if(!isInsideBounds(pos)) {
|
if (!this.isDrawing && this.hasMouseDown /*&& this.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;
|
this.isDrawing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,28 +214,22 @@ class Canvas {
|
||||||
let d = this.strokes2D(this.paths[this.paths.length - 1].points);
|
let d = this.strokes2D(this.paths[this.paths.length - 1].points);
|
||||||
this.currentStrokeEl.setAttribute('d', d);
|
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) {
|
startStroke(e) {
|
||||||
this.hasMouseDown = true;
|
this.hasMouseDown = true;
|
||||||
console.log('start');
|
|
||||||
|
|
||||||
const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||||
strokeEl.style.stroke = this.currentColor;
|
strokeEl.style.stroke = this.currentColor;
|
||||||
this.svgEl.appendChild(strokeEl);
|
this.svgEl.appendChild(strokeEl);
|
||||||
this.currentStrokeEl = strokeEl;
|
this.currentStrokeEl = strokeEl;
|
||||||
this.paths.push({
|
let path = {
|
||||||
'el': strokeEl,
|
'el': strokeEl,
|
||||||
'color': this.currentColor,
|
'color': this.currentColor,
|
||||||
'points': []
|
'points': []
|
||||||
});
|
};
|
||||||
|
this.paths.push(path);
|
||||||
|
this.events.push(path); // same ref.
|
||||||
|
|
||||||
if (this.startTime === null) {
|
if (this.startTime === null) {
|
||||||
// initiate timer on first stroke
|
// initiate timer on first stroke
|
||||||
|
@ -184,11 +238,10 @@ class Canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
endStroke(pos) {
|
endStroke(pos) {
|
||||||
console.log(this.isDrawing)
|
|
||||||
if (!this.isDrawing) {
|
if (!this.isDrawing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("send!")
|
|
||||||
this.isDrawing = false;
|
this.isDrawing = false;
|
||||||
//document.body.removeEventListener('mousemove', draw);
|
//document.body.removeEventListener('mousemove', draw);
|
||||||
|
|
||||||
|
@ -198,11 +251,13 @@ class Canvas {
|
||||||
this.paths[this.paths.length - 1].points[this.paths[this.paths.length - 1].points.length - 1][2] = 1;
|
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];
|
const stroke = this.paths[this.paths.length - 1];
|
||||||
this.socket.send(JSON.stringify({
|
const d = {
|
||||||
'action': 'stroke',
|
'event': 'stroke',
|
||||||
'color': stroke.color,
|
'color': stroke.color,
|
||||||
'points': stroke.points
|
'points': stroke.points
|
||||||
}));
|
};
|
||||||
|
console.log('send', d);
|
||||||
|
this.socket.send(JSON.stringify(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
penup(e) {
|
penup(e) {
|
||||||
|
@ -222,7 +277,7 @@ class Canvas {
|
||||||
let cmd = "";
|
let cmd = "";
|
||||||
for (let stroke of strokes) {
|
for (let stroke of strokes) {
|
||||||
if (!last_stroke) {
|
if (!last_stroke) {
|
||||||
d += `M${stroke[0] * this.width},${stroke[1] * this.height} `;
|
d += `M${stroke[0] * this.viewbox.width},${stroke[1] * this.viewbox.height} `;
|
||||||
cmd = 'M';
|
cmd = 'M';
|
||||||
} else {
|
} else {
|
||||||
if (last_stroke[2] == 1) {
|
if (last_stroke[2] == 1) {
|
||||||
|
@ -233,7 +288,7 @@ class Canvas {
|
||||||
cmd = 'l';
|
cmd = 'l';
|
||||||
}
|
}
|
||||||
let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
|
let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
|
||||||
d += `${rel_stroke[0] * this.width},${rel_stroke[1] * this.height} `;
|
d += `${rel_stroke[0] * this.viewbox.width},${rel_stroke[1] * this.viewbox.height} `;
|
||||||
}
|
}
|
||||||
last_stroke = stroke;
|
last_stroke = stroke;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue