2019-03-01 13:09:39 +00:00
|
|
|
const width = 800;
|
|
|
|
const height = 600;
|
2019-02-27 20:22:38 +00:00
|
|
|
const radius = 25;
|
|
|
|
|
|
|
|
const settings = {
|
|
|
|
physics: true,
|
|
|
|
speed: 2,
|
2019-03-01 13:09:39 +00:00
|
|
|
snapToPadding: 6,
|
|
|
|
hitTargetPadding: 6,
|
|
|
|
colors: {
|
|
|
|
default: '#000',
|
|
|
|
active: '#00f',
|
|
|
|
getColor: (object) => selectedObject === object ? settings.colors.active : settings.colors.default
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const canvas = document.getElementById('canvas');
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
canvas.width = width;
|
|
|
|
canvas.height = height;
|
|
|
|
|
|
|
|
const states = [];
|
|
|
|
const connections = [];
|
2019-03-04 09:29:32 +00:00
|
|
|
const animations = [];
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
let isPaused = false;
|
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
function convertLatexShortcuts(text) {
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
// TODO: Replace with more general way
|
|
|
|
text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));
|
|
|
|
}
|
|
|
|
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
let caretTimer,
|
2019-03-04 09:29:32 +00:00
|
|
|
caretVisible = true,
|
|
|
|
caretPos = 0;
|
2019-03-01 13:09:39 +00:00
|
|
|
|
|
|
|
function resetCaret() {
|
|
|
|
clearInterval(caretTimer);
|
|
|
|
caretTimer = setInterval(() => caretVisible = !caretVisible, 500);
|
|
|
|
caretVisible = true;
|
|
|
|
}
|
|
|
|
|
2019-02-27 20:22:38 +00:00
|
|
|
function tick() {
|
|
|
|
states.forEach(stateA => {
|
|
|
|
states.forEach(stateB => {
|
|
|
|
if (stateA !== stateB && stateA.intersects(stateB)) {
|
|
|
|
const inter = stateA.intersection(stateB);
|
|
|
|
const angle1 = stateA.directionTo(stateB);
|
|
|
|
const angle2 = angle1 + Math.PI;
|
|
|
|
|
|
|
|
const x1 = Math.cos(angle1) * inter + stateA.x;
|
|
|
|
const y1 = Math.sin(angle1) * inter + stateA.y;
|
|
|
|
const x2 = Math.cos(angle2) * inter + stateB.x;
|
|
|
|
const y2 = Math.sin(angle2) * inter + stateB.y;
|
|
|
|
|
|
|
|
stateA.moveTo(x1, y1);
|
|
|
|
stateB.moveTo(x2, y2);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
states.forEach(state => {
|
|
|
|
if (state.v) {
|
|
|
|
state.moveToStep();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
if (!isPaused)
|
|
|
|
setTimeout(tick, 1000 / 30);
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function draw() {
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
2019-03-01 13:09:39 +00:00
|
|
|
ctx.save();
|
|
|
|
ctx.translate(0.5, 0.5);
|
|
|
|
|
|
|
|
states.forEach(state => state.draw());
|
2019-02-27 20:22:38 +00:00
|
|
|
|
|
|
|
connections.forEach(connection => connection.draw());
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
if (!!currentConnection) {
|
|
|
|
currentConnection.draw();
|
|
|
|
}
|
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
animations.forEach(animation => animation.draw());
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
ctx.restore();
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
if (!isPaused)
|
|
|
|
setTimeout(draw, 1000 / 60);
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function selectObject(x, y) {
|
|
|
|
for (let state of states) {
|
2019-03-01 13:09:39 +00:00
|
|
|
if (state.containsPoint(x, y)) {
|
2019-02-27 20:22:38 +00:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
for (let connection of connections) {
|
|
|
|
if (connection.containsPoint(x, y)) {
|
|
|
|
return connection;
|
|
|
|
}
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
return null;
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
let cursorVisible = true,
|
2019-02-27 20:22:38 +00:00
|
|
|
selectedObject = null,
|
|
|
|
currentConnection = null,
|
2019-03-01 13:09:39 +00:00
|
|
|
movingObject = false,
|
|
|
|
originalClick = null,
|
|
|
|
deltaMouseX = 0,
|
|
|
|
deltaMouseY = 0,
|
|
|
|
shiftPressed = false;
|
2019-02-27 20:22:38 +00:00
|
|
|
|
|
|
|
canvas.addEventListener('mousedown', (event) => {
|
2019-03-01 13:09:39 +00:00
|
|
|
const mouse = cbRelMousePos(event);
|
|
|
|
selectedObject = selectObject(mouse.x, mouse.y);
|
|
|
|
movingObject = false;
|
|
|
|
originalClick = mouse;
|
2019-02-27 20:22:38 +00:00
|
|
|
|
|
|
|
if (!!selectedObject) {
|
|
|
|
if (shiftPressed && selectedObject instanceof State) {
|
2019-03-01 13:09:39 +00:00
|
|
|
currentConnection = new SelfConnection(selectedObject, mouse);
|
2019-02-27 20:22:38 +00:00
|
|
|
} else {
|
|
|
|
movingObject = true;
|
2019-03-01 13:09:39 +00:00
|
|
|
deltaMouseX = deltaMouseY = 0;
|
|
|
|
if (selectedObject.setMouseStart) {
|
|
|
|
selectedObject.setMouseStart(mouse.x, mouse.y);
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
2019-03-01 13:09:39 +00:00
|
|
|
resetCaret();
|
|
|
|
} else if (shiftPressed) {
|
|
|
|
currentConnection = new TemporaryConnection(mouse, mouse);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (canvasHasFocus()) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
resetCaret();
|
|
|
|
return true;
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
canvas.addEventListener('mousemove', (event) => {
|
2019-03-01 13:09:39 +00:00
|
|
|
const mouse = cbRelMousePos(event);
|
2019-02-27 20:22:38 +00:00
|
|
|
|
|
|
|
if (!!currentConnection) {
|
2019-03-01 13:09:39 +00:00
|
|
|
let targetNode = selectObject(mouse.x, mouse.y);
|
|
|
|
if (!(targetNode instanceof State)) {
|
|
|
|
targetNode = null;
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
if (!selectedObject) {
|
|
|
|
if (!!targetNode) {
|
|
|
|
currentConnection = new StartConnection(targetNode, originalClick);
|
2019-02-27 20:22:38 +00:00
|
|
|
} else {
|
2019-03-01 13:09:39 +00:00
|
|
|
currentConnection = new TemporaryConnection(originalClick, mouse);
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-03-01 13:09:39 +00:00
|
|
|
if (targetNode === selectedObject) {
|
|
|
|
currentConnection = new SelfConnection(selectedObject, mouse);
|
|
|
|
} else if (!!targetNode) {
|
|
|
|
currentConnection = new Connection(selectedObject, targetNode);
|
|
|
|
} else {
|
|
|
|
currentConnection = new TemporaryConnection(selectedObject.closestPointOnCircle(mouse.x, mouse.y), mouse)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
if (movingObject) {
|
|
|
|
selectedObject.setAnchorPoint(mouse.x, mouse.y);
|
|
|
|
if (selectedObject instanceof State) {
|
|
|
|
snapNode(selectedObject);
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
canvas.addEventListener('mouseup', (event) => {
|
2019-03-01 13:09:39 +00:00
|
|
|
movingObject = false;
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
if (!!currentConnection) {
|
|
|
|
if (!(currentConnection instanceof TemporaryConnection)) {
|
|
|
|
selectedObject = currentConnection;
|
|
|
|
connections.push(currentConnection);
|
|
|
|
resetCaret();
|
|
|
|
}
|
|
|
|
currentConnection = null;
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
2019-03-01 13:09:39 +00:00
|
|
|
});
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
canvas.addEventListener('dblclick', (event) => {
|
|
|
|
const mouse = cbRelMousePos(event);
|
|
|
|
selectedObject = selectObject(mouse.x, mouse.y);
|
|
|
|
|
|
|
|
if (!selectedObject) {
|
|
|
|
selectedObject = new State(mouse.x, mouse.y);
|
|
|
|
states.push(selectedObject);
|
|
|
|
resetCaret();
|
|
|
|
} else if (selectedObject instanceof State) {
|
|
|
|
selectedObject.isAcceptState = !selectedObject.isAcceptState;
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
});
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
document.addEventListener('keydown', (event) => {
|
|
|
|
const key = crossBrowserKey(event);
|
|
|
|
|
|
|
|
if (!canvasHasFocus()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
console.log(key);
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
switch (key) {
|
|
|
|
case 16: // Shift
|
|
|
|
shiftPressed = true;
|
|
|
|
break;
|
|
|
|
case 8: // Backspace
|
|
|
|
if (!!selectedObject) {
|
2019-03-04 09:29:32 +00:00
|
|
|
selectedObject.text = selectedObject.text.substr(0, caretPos - 1) + selectedObject.text.substr(caretPos, selectedObject.text.length);
|
|
|
|
caretPos--;
|
|
|
|
caretPos = Math.max(caretPos, 0);
|
2019-03-01 13:09:39 +00:00
|
|
|
resetCaret();
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
return false;
|
|
|
|
case 37: // Left Arrow
|
|
|
|
caretPos--;
|
|
|
|
caretPos = Math.max(caretPos, 0);
|
|
|
|
resetCaret();
|
|
|
|
|
|
|
|
return false;
|
|
|
|
case 39: // Right Arrow
|
|
|
|
caretPos++;
|
|
|
|
caretPos = Math.min(caretPos, selectedObject.text.length);
|
|
|
|
resetCaret();
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
return false;
|
|
|
|
case 46: // Delete
|
|
|
|
if (!!selectedObject) {
|
2019-03-04 09:29:32 +00:00
|
|
|
if (selectedObject instanceof State) {
|
2019-03-01 13:09:39 +00:00
|
|
|
states.splice(states.findIndex(state => state === selectedObject), 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < connections.length; i++) {
|
|
|
|
const con = connections[i];
|
|
|
|
if (con === selectedObject || con.state === selectedObject || con.stateA === selectedObject || con.stateB === selectedObject) {
|
|
|
|
connections.splice(i--, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
selectedObject = null;
|
|
|
|
}
|
|
|
|
break;
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
2019-03-01 13:09:39 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener('keyup', (event) => {
|
|
|
|
const key = crossBrowserKey(event);
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
if (key === 16) {
|
|
|
|
shiftPressed = false;
|
2019-02-27 20:22:38 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-03-01 13:09:39 +00:00
|
|
|
document.addEventListener('keypress', (event) => {
|
|
|
|
const key = crossBrowserKey(event);
|
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
if (!canvasHasFocus()) {
|
2019-03-01 13:09:39 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
if (!!selectedObject && 'text' in selectedObject) {
|
|
|
|
const text = selectedObject.text;
|
|
|
|
|
|
|
|
console.log(key, event);
|
|
|
|
|
|
|
|
if (!event.metaKey && !event.altKey && !event.ctrlKey) {
|
|
|
|
if (key >= 0x20 && key <= 0x7E) {
|
|
|
|
selectedObject.text = text.substring(0, caretPos) + String.fromCharCode(key) + text.substring(caretPos, text.length);
|
|
|
|
caretPos++;
|
|
|
|
resetCaret();
|
2019-03-01 13:09:39 +00:00
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(caretPos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (key === 8) {
|
2019-03-01 13:09:39 +00:00
|
|
|
return false;
|
|
|
|
}
|
2019-03-04 09:29:32 +00:00
|
|
|
});
|
2019-03-01 13:09:39 +00:00
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
window.addEventListener('keydown', (event) => {
|
|
|
|
if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') {
|
|
|
|
exportToFile();
|
|
|
|
|
|
|
|
event.preventDefault();
|
2019-03-01 13:09:39 +00:00
|
|
|
return false;
|
|
|
|
}
|
2019-02-27 20:22:38 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
CanvasRenderingContext2D.prototype.drawArrow = function (x, y, angle) {
|
|
|
|
const dx = Math.cos(angle);
|
|
|
|
const dy = Math.sin(angle);
|
|
|
|
|
|
|
|
this.beginPath();
|
|
|
|
|
|
|
|
this.moveTo(x, y);
|
|
|
|
this.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);
|
|
|
|
this.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);
|
|
|
|
|
|
|
|
this.fill();
|
|
|
|
};
|
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
CanvasRenderingContext2D.prototype.drawText = function (originalText, x, y, angleOrNull, isSelected) {
|
|
|
|
const text = convertLatexShortcuts(originalText);
|
2019-03-01 13:09:39 +00:00
|
|
|
this.font = '20px Roboto';
|
|
|
|
const width = this.measureText(text).width;
|
|
|
|
|
|
|
|
x -= width / 2; // Centers the text
|
|
|
|
|
2019-03-04 09:29:32 +00:00
|
|
|
if (!!angleOrNull) {
|
2019-03-01 13:09:39 +00:00
|
|
|
const dx = Math.cos(angleOrNull),
|
|
|
|
dy = Math.sin(angleOrNull),
|
|
|
|
cornerPointX = (width / 2 + 5) * (dx > 0 ? 1 : -1),
|
|
|
|
cornerPointY = (10 + 5) * (dy > 0 ? 1 : -1),
|
|
|
|
slide = dy * (Math.abs(dy) ** 40) * cornerPointX - dx * (Math.abs(dx) ** 10) * cornerPointY;
|
|
|
|
x += cornerPointX - dy * slide;
|
|
|
|
y += cornerPointY + dx * slide;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw text and caret
|
2019-03-04 09:29:32 +00:00
|
|
|
if ('advancedFillText' in this) {
|
2019-03-01 13:09:39 +00:00
|
|
|
// this.advancedFillText()
|
|
|
|
} else {
|
|
|
|
x = Math.round(x);
|
|
|
|
y = Math.round(y);
|
|
|
|
this.fillText(text, x, y + 6);
|
2019-03-04 09:29:32 +00:00
|
|
|
if (isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
|
|
|
|
x += this.measureText(text.substring(0, caretPos)).width;
|
2019-03-01 13:09:39 +00:00
|
|
|
|
|
|
|
this.beginPath();
|
|
|
|
|
|
|
|
this.moveTo(x, y - 10);
|
|
|
|
this.lineTo(x, y + 10);
|
|
|
|
|
|
|
|
this.stroke();
|
|
|
|
this.closePath();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function snapNode(targetState) {
|
|
|
|
for (let state of states) {
|
|
|
|
if (state === targetState)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (Math.abs(targetState.x - state.x) < settings.snapToPadding) {
|
|
|
|
targetState.x = state.x;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Math.abs(targetState.y - state.y) < settings.snapToPadding) {
|
|
|
|
targetState.y = state.y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function canvasHasFocus() {
|
|
|
|
return (document.activeElement || document.body) === document.body;
|
|
|
|
}
|
|
|
|
|
|
|
|
function crossBrowserKey(event) {
|
|
|
|
event = event || window.event;
|
|
|
|
return event.which || event.keyCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
function cbElementPos(event) {
|
|
|
|
event = event || window.event;
|
|
|
|
let obj = event.target || event.srcElement,
|
|
|
|
x = 0,
|
|
|
|
y = 0;
|
|
|
|
while (obj.offsetParent) {
|
|
|
|
x += obj.offsetLeft;
|
|
|
|
y += obj.offsetTop;
|
|
|
|
obj = obj.offsetParent;
|
|
|
|
}
|
|
|
|
return {x, y};
|
|
|
|
}
|
|
|
|
|
|
|
|
function cbMousePos(event) {
|
|
|
|
event = event || window.event;
|
|
|
|
return {
|
|
|
|
x: event.pageX || event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft,
|
|
|
|
y: event.pageY || event.clientY + document.body.scrollTop + document.documentElement.scrollTop,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function cbRelMousePos(event) {
|
|
|
|
const elem = cbElementPos(event),
|
|
|
|
mouse = cbMousePos(event);
|
|
|
|
return {
|
|
|
|
x: mouse.x - elem.x,
|
|
|
|
y: mouse.y - elem.y,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-02-27 20:22:38 +00:00
|
|
|
tick();
|
|
|
|
draw();
|
2019-03-04 09:29:32 +00:00
|
|
|
|
|
|
|
function guid() {
|
|
|
|
function s4() {
|
|
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
|
|
.toString(16)
|
|
|
|
.substring(1);
|
|
|
|
}
|
|
|
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
|
|
|
s4() + '-' + s4() + s4() + s4();
|
|
|
|
}
|