const width = 800; const height = 600; const radius = 25; const settings = { physics: true, speed: 2, snapToPadding: 6, hitTargetPadding: 6, colors: { default: '#000', active: '#00f', getColor: (object) => selectedObject === object ? settings.colors.active : settings.colors.default } }; const elements = { documents: document.getElementById('document-tabs'), }; const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = width; canvas.height = height; const documents = []; let activeDocument = null; function switchDocument(doc) { if (doc instanceof FSMDocument) { doc = documents.findIndex(document => document.id === doc.id); } if(doc < 0) { activeDocument = null; } if (typeof doc === 'number' && doc >= 0 && doc < documents.length) { activeDocument = doc; const docElements = document.getElementsByClassName('document-tab'); for (let el of docElements) { el.classList.remove('active'); } docElements[doc].classList.add('active'); resetCaret(); selectedObject = null; currentConnection = null; movingObject = false; originalClick = null; deltaMouseX = 0; deltaMouseY = 0; shiftPressed = false; } } function addDocument(doc) { documents.push(doc); const elem = document.createElement('li'); elem.classList.add('document-tab'); const label = elem.addChild('span', 'document-name', doc.name || 'Unbenannt'); const btn = elem.addChild('button', ['btn', 'btn-close'], 'X'); elem.addEventListener('click', (e) => { if(e.target === elem || e.target === label) switchDocument(doc); }); btn.addEventListener('click', () => { closeDocument(doc); }); doc.element = elem; elements.documents.appendChild(elem); } function closeDocument(doc) { if (doc instanceof FSMDocument) { doc = documents.findIndex(document => document.id === doc.id); } if (typeof doc === 'number') { documents.splice(doc, 1); const docElements = document.getElementsByClassName('document-tab'); docElements[doc].remove(); if(doc < activeDocument) { activeDocument--; } else if(doc === activeDocument) { if(activeDocument >= documents.length) { activeDocument = documents.length - 1; } } switchDocument(activeDocument); } } const animations = []; let isPaused = false; 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; } let caretTimer, caretVisible = true, caretPos = 0; function resetCaret() { clearInterval(caretTimer); caretTimer = setInterval(() => caretVisible = !caretVisible, 500); caretVisible = true; } function tick() { if (activeDocument !== null) { documents[activeDocument].tick(); } if (!isPaused) setTimeout(tick, 1000 / 30); } function draw() { ctx.clearRect(0, 0, width, height); ctx.save(); ctx.translate(0.5, 0.5); if (activeDocument !== null) { documents[activeDocument].draw(); if (!!currentConnection) { currentConnection.draw(); } animations.forEach(animation => animation.draw()); } else { ctx.fillStyle = '#eee'; ctx.fillRect(0, 0, width, height); ctx.fillStyle = '#000'; ctx.drawText('Bitte öffne ein Dokument oder erstelle ein neues...', width / 2, height / 2, null, false); } ctx.restore(); if (!isPaused) setTimeout(draw, 1000 / 60); } function selectObject(x, y) { if (activeDocument === null) { return null; } const doc = documents[activeDocument]; for (let state of doc.states) { if (state.containsPoint(x, y)) { return state; } } for (let connection of doc.connections) { if (connection.containsPoint(x, y)) { return connection; } } caretPos = 0; return null; } let cursorVisible = true, selectedObject = null, currentConnection = null, movingObject = false, originalClick = null, deltaMouseX = 0, deltaMouseY = 0, shiftPressed = false; canvas.addEventListener('mousedown', (event) => { const mouse = cbRelMousePos(event); selectedObject = selectObject(mouse.x, mouse.y); movingObject = false; originalClick = mouse; if (!!selectedObject) { if (shiftPressed && selectedObject instanceof State) { currentConnection = new SelfConnection(selectedObject, mouse); } else { movingObject = true; deltaMouseX = deltaMouseY = 0; if (selectedObject.setMouseStart) { selectedObject.setMouseStart(mouse.x, mouse.y); } } resetCaret(); } else if (shiftPressed) { currentConnection = new TemporaryConnection(mouse, mouse); } if (canvasHasFocus()) { return false; } else { resetCaret(); return true; } }); canvas.addEventListener('mousemove', (event) => { const mouse = cbRelMousePos(event); if (!!currentConnection) { let targetNode = selectObject(mouse.x, mouse.y); if (!(targetNode instanceof State)) { targetNode = null; } if (!selectedObject) { if (!!targetNode) { currentConnection = new StartConnection(targetNode, originalClick); } else { currentConnection = new TemporaryConnection(originalClick, mouse); } } else { 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) } } } if (movingObject) { selectedObject.setAnchorPoint(mouse.x, mouse.y); if (selectedObject instanceof State) { snapNode(selectedObject); } } }); canvas.addEventListener('mouseup', () => { movingObject = false; if (!!currentConnection && activeDocument !== null) { if (!(currentConnection instanceof TemporaryConnection)) { selectedObject = currentConnection; documents[activeDocument].connections.push(currentConnection); resetCaret(); } currentConnection = null; } }); canvas.addEventListener('dblclick', (event) => { const mouse = cbRelMousePos(event); if (activeDocument === null) return; selectedObject = selectObject(mouse.x, mouse.y); documents[activeDocument].onDblClick(mouse.x, mouse.y); }); canvas.addEventListener('contextmenu', (event) => { const mouse = cbRelMousePos(event); event.preventDefault(); if(activeDocument === null) return; selectedObject = selectObject(mouse.x, mouse.y); documents[activeDocument].onRightClick(mouse.x, mouse.y); }); document.addEventListener('keydown', (event) => { const key = crossBrowserKey(event); if (!canvasHasFocus()) { return true; } switch (key) { case 16: // Shift shiftPressed = true; break; case 8: // Backspace if (!!selectedObject) { selectedObject.text = selectedObject.text.substr(0, caretPos - 1) + selectedObject.text.substr(caretPos, selectedObject.text.length); caretPos--; caretPos = Math.max(caretPos, 0); resetCaret(); } event.preventDefault(); return false; case 37: // Left Arrow if (!(event.ctrlKey || event.metaKey) && !!selectedObject) { caretPos--; caretPos = Math.max(caretPos, 0); resetCaret(); return false; } break; case 39: // Right Arrow if (!(event.ctrlKey || event.metaKey) && !!selectedObject) { caretPos++; caretPos = Math.min(caretPos, selectedObject.text.length); resetCaret(); return false; } break; case 36: // Home if(!(event.ctrlKey || event.metaKey) && !!selectedObject) { caretPos = 0; resetCaret(); return false; } break; case 35: // End if(!(event.ctrlKey || event.metaKey) && !!selectedObject) { caretPos = selectedObject.text.length; return false; } break; case 46: // Delete if (activeDocument !== null) { documents[activeDocument].deleteCurrentObject(); } break; } }); document.addEventListener('keyup', (event) => { const key = crossBrowserKey(event); if (key === 16) { shiftPressed = false; } }); document.addEventListener('keypress', (event) => { const key = crossBrowserKey(event); if (!canvasHasFocus()) { return true; } if (!!selectedObject && 'text' in selectedObject) { const text = selectedObject.text; 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(); return false; } } } if (key === 8) { return false; } }); window.addEventListener('keydown', (event) => { const keyCode = crossBrowserKey(event); const key = String.fromCharCode(keyCode).toLowerCase(); if ((event.ctrlKey || event.metaKey) && key === 's') { // Ctrl + S if (event.altKey || event.shiftKey) { // Ctrl + Alt + S modalExport.open(); } else { saveToLocalStorage(); } event.preventDefault(); return false; } if ((event.ctrlKey || event.metaKey) && key === 'e') { // Ctrl + E modalExport.open(); event.preventDefault(); return false; } if ((event.ctrlKey || event.metaKey) && keyCode === 37) { // Ctrl + LeftArrow let activeDoc = activeDocument; activeDoc--; activeDoc = activeDoc < 0 ? documents.length - 1 : activeDoc % documents.length; switchDocument(activeDoc); event.preventDefault(); return false; } if ((event.ctrlKey || event.metaKey) && keyCode === 39) { // Ctrl + RightArrow let activeDoc = activeDocument; activeDoc++; activeDoc = activeDoc < 0 ? documents.length - 1 : activeDoc % documents.length; switchDocument(activeDoc); event.preventDefault(); return false; } }); 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(); }; CanvasRenderingContext2D.prototype.drawText = function (originalText, x, y, angleOrNull, isSelected) { const text = convertLatexShortcuts(originalText); this.font = '20px Roboto'; const width = this.measureText(text).width; x -= width / 2; // Centers the text if (!!angleOrNull) { 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 if ('advancedFillText' in this) { // this.advancedFillText() } else { x = Math.round(x); y = Math.round(y); this.fillText(text, x, y + 6); if (isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) { x += this.measureText(text.substring(0, caretPos)).width; this.beginPath(); this.moveTo(x, y - 10); this.lineTo(x, y + 10); this.stroke(); this.closePath(); } } }; Node.prototype.addChild = function (name, classList, text) { const elem = document.createElement(name); if (classList instanceof Array) { elem.classList.add(...classList); } else if (!!classList) { elem.classList.add(classList); } if (!!text) { elem.innerText = text; } this.appendChild(elem); return elem; }; function snapNode(targetState) { for (let state of documents[activeDocument].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, }; } function guid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } window.addEventListener('load', () => { const doc = new FSMDocument('Dokument1'); addDocument(doc); switchDocument(doc); tick(); draw(); }); Object.prototype.hashCode = function () { let baseString = ''; Object.values(this).forEach(entry => { if (entry === null) return; if (typeof entry === 'object' && !(entry instanceof Node)) { baseString += entry.hashCode(); } else { baseString += entry; } }); let hash = 0; for (let i = 0; i < baseString.length; i++) { const char = baseString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return hash; };