chronodiagram/www/draw.js

400 lines
13 KiB
JavaScript

// loadDrawing = function(identifier){
// const request = new Request('/copy_and_load/'+identifier, {
// method: 'GET',
// });
// // fetch(request)
// // .then(response => response.json())
// // .then(data => {
// // const metadata_req = new Request(`/annotations/${data.file}`, {
// // method: 'GET',
// // });
// // fetch(metadata_req)
// // .then(response => response.ok ? response.json() : null)
// // .then(metadata => {
// // if (metadata !== null) {
// // metadata.annotations = metadata.annotations.map((a) => new Annotation(a.tag, a.t_in, a.t_out))
// // }
// // this.loadStrokes(data, metadata)
// // })
// // .catch(e => console.log(e));
// // // do something with the data sent in the request
// // });
// }
class Canvas {
constructor(wrapperEl, preload_id) {
this.allowDrawing = false;
this.socket = null; // don't initialise right away
this.viewbox = { "x": 0, "y": 0, "width": null, "height": null };
this.url = window.location.origin.replace('http', 'ws') + '/ws?' + window.location.search.substring(1);
// build the interface
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.fullscreenEl = document.createElement('div');
this.fullscreenEl.classList.add('button-fullscreen');
this.fullscreenEl.innerText = "Fullscreen";
this.wrapperEl.appendChild(this.fullscreenEl);
this.fullscreenEl.addEventListener('click', (e) => {
document.body.requestFullscreen();
});
document.body.addEventListener('fullscreenchange', (e) => {
if (document.fullscreenElement) {
document.body.classList.add('fullscreen');
} else {
document.body.classList.remove('fullscreen');
}
})
this.colors = ["black", "red", "blue", "green"];
this.resize();
window.addEventListener('resize', (ev) => this.requestResize());
this.paths = [];
this.viewboxes = [];
this.events = []; // all paths & viewboxes events
this.isDrawing = false;
this.hasMouseDown = false;
this.currentStrokeEl = null;
this.startTime = null;
this.isMoving = false;
document.body.addEventListener('pointermove', (ev) => {
ev.stopPropagation();
ev.preventDefault();
if (ev.pointerType == "touch" || ev.buttons & 2) { // 4: middle mouse button
this.moveCanvas(ev);
} else { // pointerType == pen or mouse
this.draw(ev);
}
});
document.body.addEventListener('pointerup', (ev) => {
ev.stopPropagation();
ev.preventDefault();
if (ev.pointerType == "touch" || ev.buttons & 2 || this.isMoving) { // buttons is 0 on pointerup
this.endMoveCanvas(ev);
this.isMoving = false;
} else { // pointerType == pen or mouse
location.hash = '#' + this.filename; // only update when drawn.
this.penup(ev);
}
});
this.svgEl.addEventListener('contextmenu', function (e) {
// do something here...
e.preventDefault();
}, false);
this.svgEl.addEventListener('pointerdown', (ev) => {
ev.stopPropagation();
ev.preventDefault();
if (ev.pointerType == "touch" || ev.buttons & 2) { // 4: middle mouse button, 2; right mouse button
this.isMoving = true;
this.startMoveCanvas(ev);
} else if (ev.buttons & 1) { // pointerType == pen or mouse
this.startStroke(ev);
}
});
this.createToolbox();
this.setColor(this.colors[0]);
this.socket = new WebSocket(this.url);
this.socket.addEventListener('open', (e) => {
this.sendDimensions();
if (preload_id) {
// signal if we want to continue from an existing drawing
this.socket.send(JSON.stringify({
'event': 'preload',
'file': preload_id,
}));
}
});
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()
}
if (msg.hasOwnProperty('preloaded_svg')) {
console.log('preloaded', msg);
if (msg.dimensions[0] != this.viewbox.width || msg.dimensions[1] != this.viewbox.height) {
alert(`Loading file with different dimensions. This can lead to odd results. Original: ${msg.dimensions[0]}x${msg.dimensions[1]} Now: ${this.viewbox.width}x${this.viewbox.height}`)
}
this.setPreloaded(msg.preloaded_svg);
// this.setFilename(msg.filename);
// this.openTheFloor()
}
});
}
setPreloaded(json_url) {
this.preloaded_resource = json_url
const request = new Request(this.preloaded_resource + '.svg', {
method: 'GET',
});
fetch(request)
.then(response => response.text())
.then(body => {
const parser = new DOMParser()
const dom = parser.parseFromString(body, "image/svg+xml");
console.log(dom, dom.getRootNode().querySelectorAll('g'))
const group = dom.getRootNode().querySelectorAll('g')[0]
this.svgEl.prepend(group);
})
}
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() {
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.viewbox.width = window.innerWidth;
this.viewbox.height = window.innerHeight;
this.applyViewBox();
this.sendDimensions();
}
sendDimensions() {
const d = {
'event': 'dimensions',
'width': this.viewbox.width,
'height': this.viewbox.height
};
if (this.socket === null) {
// ignore ...
} else if (this.socket.readyState) {
this.socket.send(JSON.stringify(d));
} else {
this.socket.addEventListener('open', (ev) => {
this.socket.send(JSON.stringify(d));
})
}
}
applyViewBox() {
const viewBox = `${this.viewbox.x} ${this.viewbox.y} ${this.viewbox.width} ${this.viewbox.height}`;
this.svgEl.setAttribute('viewBox', viewBox);
this.svgEl.setAttribute('width', this.viewbox.width + '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() {
this.resize();
// 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'] + this.viewbox.x) / box['width'];
let y = (e.y - box['top'] + this.viewbox.y) / box['height'];
return { 'x': x, 'y': y };
}
isInsideBounds(pos) {
return !(pos['x'] < 0 || pos['y'] < 0 || pos['x'] > 1 || pos['y'] > 1);
}
draw(e) {
let pos = this.getCoordinates(e);
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);
}
}
startStroke(e) {
this.hasMouseDown = true;
const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
strokeEl.style.stroke = this.currentColor;
this.svgEl.appendChild(strokeEl);
this.currentStrokeEl = strokeEl;
let path = {
'el': strokeEl,
'color': this.currentColor,
'points': []
};
this.paths.push(path);
this.events.push(path); // same ref.
if (this.startTime === null) {
// initiate timer on first stroke
this.startTime = window.performance.now();
}
}
endStroke(pos) {
if (!this.isDrawing) {
return;
}
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];
const d = {
'event': 'stroke',
'color': stroke.color,
'points': stroke.points
};
console.log('send', d);
this.socket.send(JSON.stringify(d));
}
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.viewbox.width},${stroke[1] * this.viewbox.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.viewbox.width},${rel_stroke[1] * this.viewbox.height} `;
}
last_stroke = stroke;
}
return d;
}
}